From 43b847220e156349f70db93bbafd559958134547 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Feb 2022 22:12:37 +0100 Subject: [PATCH 0001/1054] Bump version to 2022.4.0dev0 (#67132) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b1ff5dca8d1..60226d3af08 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 9 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2022.3 + HA_SHORT_VERSION: 2022.4 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index 49f7ed18490..4f2f5ed7478 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 3 +MINOR_VERSION: Final = 4 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/setup.cfg b/setup.cfg index 74ea6e296b5..8787432ba7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.3.0.dev0 +version = 2022.4.0.dev0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From e3e962691ce5aa84a3de71e6d3cd14822ca3e0c0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Feb 2022 17:42:18 -0600 Subject: [PATCH 0002/1054] Fix Sonos radio metadata processing with missing data (#67141) --- homeassistant/components/sonos/media.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index 85d15680a97..f4108b85317 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -169,7 +169,8 @@ class SonosMedia: self.queue_size = int(queue_size) if audio_source == MUSIC_SRC_RADIO: - self.channel = et_uri_md.title + if et_uri_md: + self.channel = et_uri_md.title if ct_md and ct_md.radio_show: radio_show = ct_md.radio_show.split(",")[0] From 6364e81be58f2a6f6a23c5b12c3503a9802e10ea Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 24 Feb 2022 00:17:55 +0000 Subject: [PATCH 0003/1054] [ci skip] Translation update --- .../alarm_control_panel/translations/el.json | 8 ++++---- .../binary_sensor/translations/el.json | 8 ++++---- .../binary_sensor/translations/pt-BR.json | 4 ++-- .../components/broadlink/translations/el.json | 6 +++--- .../components/camera/translations/el.json | 2 +- .../components/canary/translations/el.json | 2 +- .../components/climacell/translations/el.json | 2 +- .../components/climate/translations/el.json | 8 ++++---- .../configurator/translations/el.json | 2 +- .../components/cover/translations/el.json | 6 +++--- .../device_tracker/translations/el.json | 4 ++-- .../dlna_dms/translations/pt-BR.json | 6 +++--- .../components/elkm1/translations/el.json | 2 +- .../components/fan/translations/el.json | 4 ++-- .../components/fritz/translations/ca.json | 3 ++- .../components/fritz/translations/de.json | 3 ++- .../components/fritz/translations/el.json | 3 ++- .../components/fritz/translations/en.json | 11 +++++++++++ .../components/fritz/translations/et.json | 3 ++- .../components/fritz/translations/hu.json | 3 ++- .../components/fritz/translations/ja.json | 3 ++- .../components/fritz/translations/no.json | 3 ++- .../components/fritz/translations/pl.json | 3 ++- .../components/fritz/translations/pt-BR.json | 3 ++- .../components/fritz/translations/ru.json | 3 ++- .../components/fritz/translations/zh-Hant.json | 3 ++- .../components/goalzero/translations/el.json | 8 ++++---- .../components/group/translations/el.json | 10 +++++----- .../components/hangouts/translations/de.json | 1 - .../components/homewizard/translations/el.json | 2 +- .../input_boolean/translations/el.json | 4 ++-- .../components/iss/translations/el.json | 2 +- .../components/kodi/translations/el.json | 4 ++-- .../components/light/translations/el.json | 6 +++--- .../components/lock/translations/el.json | 2 +- .../media_player/translations/el.json | 8 ++++---- .../components/mjpeg/translations/pt-BR.json | 16 ++++++++-------- .../components/nanoleaf/translations/ca.json | 8 ++++++++ .../components/nanoleaf/translations/el.json | 8 ++++++++ .../components/nanoleaf/translations/hu.json | 8 ++++++++ .../components/nanoleaf/translations/pl.json | 8 ++++++++ .../components/netgear/translations/el.json | 2 +- .../components/nightscout/translations/el.json | 2 +- .../components/notify/translations/el.json | 2 +- .../components/nzbget/translations/el.json | 2 +- .../openweathermap/translations/el.json | 4 ++-- .../components/overkiz/translations/el.json | 2 +- .../components/plant/translations/el.json | 2 +- .../components/plex/translations/el.json | 2 +- .../components/powerwall/translations/el.json | 2 +- .../pure_energie/translations/pt-BR.json | 12 ++++++------ .../radio_browser/translations/pt-BR.json | 4 ++-- .../components/remote/translations/el.json | 4 ++-- .../components/risco/translations/el.json | 4 ++-- .../components/roon/translations/el.json | 2 +- .../screenlogic/translations/el.json | 2 +- .../components/script/translations/el.json | 2 +- .../components/sense/translations/pt-BR.json | 2 +- .../components/sensor/translations/el.json | 4 ++-- .../components/shelly/translations/el.json | 6 +++--- .../components/sonarr/translations/el.json | 6 +++--- .../components/sonarr/translations/et.json | 3 ++- .../components/sonarr/translations/hu.json | 1 + .../components/sonarr/translations/ja.json | 1 + .../components/sonarr/translations/no.json | 3 ++- .../components/sonarr/translations/pl.json | 3 ++- .../components/sonarr/translations/ru.json | 3 ++- .../sonarr/translations/zh-Hant.json | 3 ++- .../components/spotify/translations/el.json | 2 +- .../components/switch/translations/el.json | 4 ++-- .../system_health/translations/el.json | 2 +- .../components/timer/translations/el.json | 4 ++-- .../unifiprotect/translations/el.json | 2 +- .../components/updater/translations/el.json | 2 +- .../uptimerobot/translations/el.json | 4 ++-- .../components/vacuum/translations/el.json | 6 +++--- .../components/weather/translations/el.json | 18 +++++++++--------- .../components/wilight/translations/el.json | 2 +- .../components/wiz/translations/el.json | 4 ++-- .../components/yeelight/translations/el.json | 2 +- .../components/zoneminder/translations/el.json | 2 +- .../components/zwave/translations/el.json | 4 ++-- 82 files changed, 203 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/translations/el.json b/homeassistant/components/alarm_control_panel/translations/el.json index d01fe947a8e..099e9b405ac 100644 --- a/homeassistant/components/alarm_control_panel/translations/el.json +++ b/homeassistant/components/alarm_control_panel/translations/el.json @@ -30,15 +30,15 @@ "armed": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", "armed_away": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03bc\u03b1\u03ba\u03c1\u03b9\u03ac", "armed_custom_bypass": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03ae", - "armed_home": "\u03a3\u03c0\u03af\u03c4\u03b9 \u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf", - "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b2\u03c1\u03ac\u03b4\u03c5", + "armed_home": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c3\u03c0\u03af\u03c4\u03b9", + "armed_night": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b2\u03c1\u03ac\u03b4\u03c5", "armed_vacation": "\u039f\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ce\u03bd", "arming": "\u038c\u03c0\u03bb\u03b9\u03c3\u03b7", "disarmed": "\u0391\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", "disarming": "\u0391\u03c6\u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", "pending": "\u0395\u03ba\u03ba\u03c1\u03b5\u03bc\u03ae\u03c2", - "triggered": "\u03a0\u03b1\u03c1\u03b1\u03b2\u03af\u03b1\u03c3\u03b7" + "triggered": "\u03a0\u03c5\u03c1\u03bf\u03b4\u03bf\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5" } }, - "title": "\u03a0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd" + "title": "\u03a0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd" } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/el.json b/homeassistant/components/binary_sensor/translations/el.json index 1cfe13d694b..ec78f1cd575 100644 --- a/homeassistant/components/binary_sensor/translations/el.json +++ b/homeassistant/components/binary_sensor/translations/el.json @@ -127,8 +127,8 @@ "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" }, "battery": { - "off": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03cc\u03c2", - "on": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc\u03c2" + "off": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03ae", + "on": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03ae" }, "battery_charging": { "off": "\u0394\u03b5 \u03c6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9", @@ -147,7 +147,7 @@ "on": "\u039a\u03c1\u03cd\u03bf" }, "connectivity": { - "off": "\u0391\u03c0\u03bf\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", + "off": "\u0391\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2", "on": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03c2" }, "door": { @@ -156,7 +156,7 @@ }, "garage_door": { "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1" + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" }, "gas": { "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 779830e0961..d858dec6664 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -135,8 +135,8 @@ "on": "Carregando" }, "carbon_monoxide": { - "off": "Remover", - "on": "Detectou" + "off": "Normal", + "on": "Detectado" }, "co": { "off": "Limpo", diff --git a/homeassistant/components/broadlink/translations/el.json b/homeassistant/components/broadlink/translations/el.json index 6db366dae32..09db61f3704 100644 --- a/homeassistant/components/broadlink/translations/el.json +++ b/homeassistant/components/broadlink/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "already_in_progress": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03bc\u03b9\u03b1 \u03c1\u03bf\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", @@ -25,14 +25,14 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "reset": { - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5:\n1. \u0395\u03c1\u03b3\u03bf\u03c3\u03c4\u03b1\u03c3\u03b9\u03b1\u03ba\u03ae \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.\n2. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.\n3. \u03a3\u03c4\u03b1\u03bc\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5. \u039c\u03b7\u03bd \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7. \u039a\u03bb\u03b5\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\n4. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae.", + "description": "{name} ({model} \u03c3\u03c4\u03bf {host}) \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7. \u039f\u03b4\u03b7\u03b3\u03af\u03b5\u03c2:\n1. \u0391\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Broadlink.\n2. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.\n3. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf `...` \u03c3\u03c4\u03bf \u03b5\u03c0\u03ac\u03bd\u03c9 \u03b4\u03b5\u03be\u03af \u03bc\u03ad\u03c1\u03bf\u03c2.\n4. \u039c\u03b5\u03c4\u03b1\u03ba\u03b9\u03bd\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03ba\u03ac\u03c4\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1\u03c2.\n5. \u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1.", "title": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" }, "unlock": { "data": { "unlock": "\u039d\u03b1\u03b9, \u03ba\u03ac\u03bd\u03c4\u03bf." }, - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b7. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf Home Assistant. \u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5;", + "description": "{name} ({model} \u03c3\u03c4\u03bf {host}) \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03bf Home Assistant. \u0398\u03b1 \u03b8\u03ad\u03bb\u03b1\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03be\u03b5\u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03c3\u03b5\u03c4\u03b5;", "title": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, "user": { diff --git a/homeassistant/components/camera/translations/el.json b/homeassistant/components/camera/translations/el.json index 56a57402b4d..f2faa8acf8a 100644 --- a/homeassistant/components/camera/translations/el.json +++ b/homeassistant/components/camera/translations/el.json @@ -2,7 +2,7 @@ "state": { "_": { "idle": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", - "recording": "\u039a\u03b1\u03c4\u03b1\u03b3\u03c1\u03ac\u03c6\u03b5\u03b9", + "recording": "\u039a\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae", "streaming": "\u039c\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03a1\u03bf\u03ae\u03c2" } }, diff --git a/homeassistant/components/canary/translations/el.json b/homeassistant/components/canary/translations/el.json index 6b11d642829..62ac5b949fc 100644 --- a/homeassistant/components/canary/translations/el.json +++ b/homeassistant/components/canary/translations/el.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json index 1f9956a75fa..26c6d5e8d40 100644 --- a/homeassistant/components/climacell/translations/el.json +++ b/homeassistant/components/climacell/translations/el.json @@ -26,7 +26,7 @@ "timestep": "\u039b\u03b5\u03c0\u03c4\u03ac \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd NowCast" }, "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd 'nowcast', \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03b4\u03b5\u03bb\u03c4\u03af\u03c9\u03bd.", - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ClimaCell" + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" } } }, diff --git a/homeassistant/components/climate/translations/el.json b/homeassistant/components/climate/translations/el.json index a7aa5386417..b195ee8124c 100644 --- a/homeassistant/components/climate/translations/el.json +++ b/homeassistant/components/climate/translations/el.json @@ -17,13 +17,13 @@ "state": { "_": { "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", - "cool": "\u0394\u03c1\u03bf\u03c3\u03b5\u03c1\u03cc", + "cool": "\u03a8\u03cd\u03be\u03b7", "dry": "\u039e\u03b7\u03c1\u03cc", "fan_only": "\u0391\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03cc\u03bd\u03bf", - "heat": "\u0398\u03b5\u03c1\u03bc\u03cc", - "heat_cool": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7 / \u03a8\u03cd\u03be\u03b7", + "heat": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7", + "heat_cool": "\u0398\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7/\u03a8\u03cd\u03be\u03b7", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" } }, - "title": "\u03a4\u03bf \u03ba\u03bb\u03af\u03bc\u03b1" + "title": "\u039a\u03bb\u03af\u03bc\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/configurator/translations/el.json b/homeassistant/components/configurator/translations/el.json index a8242694284..4a9830847cf 100644 --- a/homeassistant/components/configurator/translations/el.json +++ b/homeassistant/components/configurator/translations/el.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5", + "configure": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7", "configured": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5" } }, diff --git a/homeassistant/components/cover/translations/el.json b/homeassistant/components/cover/translations/el.json index e02c8e3a97b..d026618c731 100644 --- a/homeassistant/components/cover/translations/el.json +++ b/homeassistant/components/cover/translations/el.json @@ -29,11 +29,11 @@ "state": { "_": { "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "closing": "\u039a\u03bb\u03b5\u03af\u03c3\u03b9\u03bc\u03bf", + "closing": "\u039a\u03bb\u03b5\u03af\u03bd\u03b5\u03b9", "open": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc", - "opening": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1", + "opening": "\u0391\u03bd\u03bf\u03af\u03b3\u03b5\u03b9", "stopped": "\u03a3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5" } }, - "title": "\u039a\u03ac\u03bb\u03c5\u03c8\u03b7" + "title": "\u039a\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/el.json b/homeassistant/components/device_tracker/translations/el.json index 7d42d825345..a577138f055 100644 --- a/homeassistant/components/device_tracker/translations/el.json +++ b/homeassistant/components/device_tracker/translations/el.json @@ -12,8 +12,8 @@ "state": { "_": { "home": "\u03a3\u03c0\u03af\u03c4\u03b9", - "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd" + "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd" } }, - "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03c5\u03c4\u03ae" + "title": "\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pt-BR.json b/homeassistant/components/dlna_dms/translations/pt-BR.json index 125a31fe9b5..5672318a2c8 100644 --- a/homeassistant/components/dlna_dms/translations/pt-BR.json +++ b/homeassistant/components/dlna_dms/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "bad_ssdp": "Falta um valor obrigat\u00f3rio nos dados SSDP", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_dms": "O dispositivo n\u00e3o \u00e9 um servidor de m\u00eddia compat\u00edvel" @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Escolha um dispositivo para configurar", "title": "Dispositivos DLNA DMA descobertos" diff --git a/homeassistant/components/elkm1/translations/el.json b/homeassistant/components/elkm1/translations/el.json index 9e7fe27ce72..9f600806d83 100644 --- a/homeassistant/components/elkm1/translations/el.json +++ b/homeassistant/components/elkm1/translations/el.json @@ -45,7 +45,7 @@ "temperature_unit": "\u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf ElkM1.", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0397 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'address[:port]' \u03b3\u03b9\u03b1 'secure' \u03ba\u03b1\u03b9 'non-secure'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '192.168.1.1'. \u0397 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 2101 \u03b3\u03b9\u03b1 '\u03bc\u03b7 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae' \u03ba\u03b1\u03b9 2601 \u03b3\u03b9\u03b1 '\u03b1\u03c3\u03c6\u03b1\u03bb\u03ae'. \u0393\u03b9\u03b1 \u03c4\u03bf \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf, \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae 'tty[:baud]'. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: '/dev/ttyS1'. \u03a4\u03bf baud \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03ba\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 115200.", + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ae \"\u039c\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7\" \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Elk-M1 Control" } } diff --git a/homeassistant/components/fan/translations/el.json b/homeassistant/components/fan/translations/el.json index 10f04bacd1e..b9c4962a7a6 100644 --- a/homeassistant/components/fan/translations/el.json +++ b/homeassistant/components/fan/translations/el.json @@ -17,8 +17,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, "title": "\u0391\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03b1\u03c2" diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index 2e240bb9833..6df025c99bb 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Segons d'espera abans de considerar un dispositiu a 'casa'" + "consider_home": "Segons d'espera abans de considerar un dispositiu a 'casa'", + "old_discovery": "Activa el m\u00e8tode de descobriment antic" } } } diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 47938084f5b..3c391ba534c 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Sekunden, um ein Ger\u00e4t als 'zu Hause' zu betrachten" + "consider_home": "Sekunden, um ein Ger\u00e4t als 'zu Hause' zu betrachten", + "old_discovery": "Alte Erkennungsmethode aktivieren" } } } diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index 443f8b95c93..bf49c098e1e 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b8\u03b5\u03c9\u03c1\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \"\u03c3\u03c0\u03af\u03c4\u03b9\"" + "consider_home": "\u0394\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b8\u03b5\u03c9\u03c1\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03c4\u03bf \"\u03c3\u03c0\u03af\u03c4\u03b9\"", + "old_discovery": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03b1\u03bb\u03b9\u03ac\u03c2 \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2" } } } diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 0a58ee686f3..3314f47278c 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,6 +9,7 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", + "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication" }, "flow_title": "{name}", @@ -29,6 +30,16 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools - mandatory" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 2866ae13336..6562a89ab9a 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Millal m\u00e4\u00e4rata seade olema kodus (sekundites)" + "consider_home": "Millal m\u00e4\u00e4rata seade olema kodus (sekundites)", + "old_discovery": "Luba vana avastamismeetod" } } } diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index 733a4fb1a8e..ef8d8b2b32c 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "M\u00e1sodpercek egy eszk\u00f6z \"otthon\" tart\u00e1s\u00e1ra" + "consider_home": "M\u00e1sodpercek egy eszk\u00f6z \"otthon\" tart\u00e1s\u00e1ra", + "old_discovery": "R\u00e9gi felder\u00edt\u00e9si m\u00f3dszer enged\u00e9lyez\u00e9se" } } } diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 156caabbfa3..84cbdb3af76 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "'\u30db\u30fc\u30e0' \u3067\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u8a0e\u3059\u308b\u79d2\u6570" + "consider_home": "'\u30db\u30fc\u30e0' \u3067\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u8a0e\u3059\u308b\u79d2\u6570", + "old_discovery": "\u53e4\u3044\u691c\u51fa\u65b9\u6cd5\u3092\u6709\u52b9\u306b\u3059\u308b" } } } diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 0838efbf649..7c7a9903b4a 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Sekunder \u00e5 vurdere en enhet hjemme" + "consider_home": "Sekunder \u00e5 vurdere en enhet hjemme", + "old_discovery": "Aktiver gammel s\u00f8kemetode" } } } diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index 5632ae67694..b87f201f70a 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"w domu\"" + "consider_home": "Czas w sekundach, zanim urz\u0105dzenie otrzyma stan \"w domu\"", + "old_discovery": "W\u0142\u0105cz star\u0105 metod\u0119 wykrywania" } } } diff --git a/homeassistant/components/fritz/translations/pt-BR.json b/homeassistant/components/fritz/translations/pt-BR.json index a28f063ab6d..7a27094b0c2 100644 --- a/homeassistant/components/fritz/translations/pt-BR.json +++ b/homeassistant/components/fritz/translations/pt-BR.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Segundos para considerar um dispositivo em 'casa'" + "consider_home": "Segundos para considerar um dispositivo em 'casa'", + "old_discovery": "Ativar m\u00e9todo de descoberta antigo" } } } diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index 54619e22a36..414d7dbea6e 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "old_discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u044b\u0439 \u043c\u0435\u0442\u043e\u0434 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f" } } } diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 861ec6d62ce..a57e0125a49 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "\u8996\u70ba\u5728\u5bb6\u7684\u7b49\u5019\u79d2\u6578" + "consider_home": "\u8996\u70ba\u5728\u5bb6\u7684\u7b49\u5019\u79d2\u6578", + "old_discovery": "\u958b\u555f\u820a\u63a2\u7d22\u6a21\u5f0f" } } } diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json index 31d089f53ec..865106aa33c 100644 --- a/homeassistant/components/goalzero/translations/el.json +++ b/homeassistant/components/goalzero/translations/el.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_host": "\u0391\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf Yeti \u03c0\u03bf\u03c5 \u03c8\u03ac\u03c7\u03bd\u03b5\u03c4\u03b5", - "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "confirm_discovery": { @@ -20,7 +20,7 @@ "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u0391\u03c1\u03c7\u03b9\u03ba\u03ac, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd ip \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u03a4\u03bf DHCP \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c3\u03c6\u03b1\u03bb\u03b9\u03c3\u03c4\u03b5\u03af \u03cc\u03c4\u03b9 \u03b7 ip \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", + "description": "\u03a0\u03c1\u03ce\u03c4\u03b1, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 \u039f\u03b9\u03ba\u03af\u03b1\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index e22d7a788af..25931b229df 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -1,16 +1,16 @@ { "state": { "_": { - "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03ae", "home": "\u03a3\u03c0\u03af\u03c4\u03b9", - "locked": "\u039a\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03bf", - "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", + "locked": "\u039a\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b7", + "not_home": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", "ok": "\u0395\u03bd\u03c4\u03ac\u03be\u03b5\u03b9", "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", - "open": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc", + "open": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae", "problem": "\u03a0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1", - "unlocked": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03bf" + "unlocked": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03b7" } }, "title": "\u039f\u03bc\u03ac\u03b4\u03b1" diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index 42770308346..02481670a09 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -14,7 +14,6 @@ "data": { "2fa": "2FA PIN" }, - "description": "Leer", "title": "2-Faktor-Authentifizierung" }, "user": { diff --git a/homeassistant/components/homewizard/translations/el.json b/homeassistant/components/homewizard/translations/el.json index f3d7c392109..dd85c5e87f5 100644 --- a/homeassistant/components/homewizard/translations/el.json +++ b/homeassistant/components/homewizard/translations/el.json @@ -4,7 +4,7 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "api_not_enabled": "\u03a4\u03bf API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf API \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae HomeWizard Energy App \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", "device_not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 API", "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/input_boolean/translations/el.json b/homeassistant/components/input_boolean/translations/el.json index 41fcd83a349..28148a12a77 100644 --- a/homeassistant/components/input_boolean/translations/el.json +++ b/homeassistant/components/input_boolean/translations/el.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, "title": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03bb\u03bf\u03b3\u03b9\u03ba\u03ae\u03c2 \u03c0\u03c1\u03ac\u03be\u03b7\u03c2" diff --git a/homeassistant/components/iss/translations/el.json b/homeassistant/components/iss/translations/el.json index b662dbea64c..0938fd72c09 100644 --- a/homeassistant/components/iss/translations/el.json +++ b/homeassistant/components/iss/translations/el.json @@ -9,7 +9,7 @@ "data": { "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7;" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u0394\u03b9\u03b5\u03b8\u03bd\u03ae \u0394\u03b9\u03b1\u03c3\u03c4\u03b7\u03bc\u03b9\u03ba\u03cc \u03a3\u03c4\u03b1\u03b8\u03bc\u03cc;" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 \u0394\u03b9\u03b5\u03b8\u03bd\u03bf\u03cd\u03c2 \u0394\u03b9\u03b1\u03c3\u03c4\u03b7\u03bc\u03b9\u03ba\u03bf\u03cd \u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd (ISS);" } } }, diff --git a/homeassistant/components/kodi/translations/el.json b/homeassistant/components/kodi/translations/el.json index e0a3118ee05..39d13c56856 100644 --- a/homeassistant/components/kodi/translations/el.json +++ b/homeassistant/components/kodi/translations/el.json @@ -12,7 +12,7 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "flow_title": "Kodi: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "credentials": { "data": { @@ -29,7 +29,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "port": "\u0398\u03cd\u03c1\u03b1", - "ssl": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 SSL" + "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, "description": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 Kodi. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \"\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03bf\u03c5 Kodi \u03bc\u03ad\u03c3\u03c9 HTTP\" \u03c3\u03c4\u03bf \u03a3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1/\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2/\u0394\u03af\u03ba\u03c4\u03c5\u03bf/\u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b5\u03c2." }, diff --git a/homeassistant/components/light/translations/el.json b/homeassistant/components/light/translations/el.json index 4b5c69c6101..dbba423dd2a 100644 --- a/homeassistant/components/light/translations/el.json +++ b/homeassistant/components/light/translations/el.json @@ -21,9 +21,9 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, - "title": "\u03a6\u03c9\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac" + "title": "\u03a6\u03c9\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/lock/translations/el.json b/homeassistant/components/lock/translations/el.json index a992279f497..976a2988aa8 100644 --- a/homeassistant/components/lock/translations/el.json +++ b/homeassistant/components/lock/translations/el.json @@ -20,5 +20,5 @@ "unlocked": "\u039e\u03b5\u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03c4\u03b7" } }, - "title": "\u039a\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1" + "title": "\u039a\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac" } \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/el.json b/homeassistant/components/media_player/translations/el.json index e3a300af77d..242de3e829a 100644 --- a/homeassistant/components/media_player/translations/el.json +++ b/homeassistant/components/media_player/translations/el.json @@ -19,12 +19,12 @@ "state": { "_": { "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", - "off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "paused": "\u03a3\u03b5 \u03a0\u03b1\u03cd\u03c3\u03b7", - "playing": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2", + "playing": "\u03a0\u03b1\u03af\u03b6\u03b5\u03b9", "standby": "\u039a\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae\u03c2" } }, - "title": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" + "title": "\u03a0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/pt-BR.json b/homeassistant/components/mjpeg/translations/pt-BR.json index f54828ea224..ebd643a94bd 100644 --- a/homeassistant/components/mjpeg/translations/pt-BR.json +++ b/homeassistant/components/mjpeg/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { @@ -14,16 +14,16 @@ "name": "Nome", "password": "Senha", "still_image_url": "URL da imagem est\u00e1tica", - "username": "Nome de usu\u00e1rio", - "verify_ssl": "Verificar certificado SSL" + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" } } } }, "options": { "error": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou ao conectar", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { @@ -33,8 +33,8 @@ "name": "Nome", "password": "Senha", "still_image_url": "URL da imagem est\u00e1tica", - "username": "Nome de usu\u00e1rio", - "verify_ssl": "Verificar certificado SSL" + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" } } } diff --git a/homeassistant/components/nanoleaf/translations/ca.json b/homeassistant/components/nanoleaf/translations/ca.json index d040dac3e6b..e0e1df01c0c 100644 --- a/homeassistant/components/nanoleaf/translations/ca.json +++ b/homeassistant/components/nanoleaf/translations/ca.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Llisca cap avall", + "swipe_left": "Llisca cap a l'esquerra", + "swipe_right": "Llisca cap a la dreta", + "swipe_up": "Llisca cap amunt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/el.json b/homeassistant/components/nanoleaf/translations/el.json index bf3e406a861..a81f0fcf993 100644 --- a/homeassistant/components/nanoleaf/translations/el.json +++ b/homeassistant/components/nanoleaf/translations/el.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03ba\u03ac\u03c4\u03c9", + "swipe_left": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "swipe_right": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03b4\u03b5\u03be\u03b9\u03ac", + "swipe_up": "\u03a3\u03cd\u03c1\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03b1 \u03b5\u03c0\u03ac\u03bd\u03c9" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/hu.json b/homeassistant/components/nanoleaf/translations/hu.json index 176d47cc38f..c67c4f958de 100644 --- a/homeassistant/components/nanoleaf/translations/hu.json +++ b/homeassistant/components/nanoleaf/translations/hu.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "P\u00f6ccint\u00e9s lefel\u00e9", + "swipe_left": "P\u00f6ccint\u00e9s balra", + "swipe_right": "P\u00f6ccint\u00e9s jobbra", + "swipe_up": "P\u00f6ccint\u00e9s felfel\u00e9" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pl.json b/homeassistant/components/nanoleaf/translations/pl.json index 66c7ecd23f1..1419ccd650b 100644 --- a/homeassistant/components/nanoleaf/translations/pl.json +++ b/homeassistant/components/nanoleaf/translations/pl.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "przeci\u0105gni\u0119to w d\u00f3\u0142", + "swipe_left": "przeci\u0105gni\u0119to w lewo", + "swipe_right": "przeci\u0105gni\u0119to w prawo", + "swipe_up": "przeci\u0105gni\u0119to do g\u00f3ry" + } } } \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/el.json b/homeassistant/components/netgear/translations/el.json index 141f8f31ddd..0ac5cb328bc 100644 --- a/homeassistant/components/netgear/translations/el.json +++ b/homeassistant/components/netgear/translations/el.json @@ -15,7 +15,7 @@ "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, - "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1: {port}\n\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}", + "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/nightscout/translations/el.json b/homeassistant/components/nightscout/translations/el.json index aaa8db2b70d..c484d715e76 100644 --- a/homeassistant/components/nightscout/translations/el.json +++ b/homeassistant/components/nightscout/translations/el.json @@ -15,7 +15,7 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" }, - "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).", + "description": "- URL: \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1\u03c2 nightscout \u03c3\u03b1\u03c2. \u0394\u03b7\u03bb\u03b1\u03b4\u03ae: https://myhomeassistant.duckdns.org:5423\n- \u039a\u03bb\u03b5\u03b9\u03b4\u03af API (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc): \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03c5\u03bc\u03ad\u03bd\u03b7 (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/notify/translations/el.json b/homeassistant/components/notify/translations/el.json index e95012f3183..0b85b3aeabf 100644 --- a/homeassistant/components/notify/translations/el.json +++ b/homeassistant/components/notify/translations/el.json @@ -1,3 +1,3 @@ { - "title": "\u039a\u03bf\u03b9\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" + "title": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/el.json b/homeassistant/components/nzbget/translations/el.json index c2d086e8be1..9f2f0612071 100644 --- a/homeassistant/components/nzbget/translations/el.json +++ b/homeassistant/components/nzbget/translations/el.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "NZBGet: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index 1455ed91a53..90b876033f6 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API OpenWeatherMap", + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "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", diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index 2ba577de4c5..9f7ad60cb09 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -9,7 +9,7 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "server_in_maintenance": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7", - "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", + "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", diff --git a/homeassistant/components/plant/translations/el.json b/homeassistant/components/plant/translations/el.json index 9d8fb65979b..af4f00ff57b 100644 --- a/homeassistant/components/plant/translations/el.json +++ b/homeassistant/components/plant/translations/el.json @@ -5,5 +5,5 @@ "problem": "\u03a0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1" } }, - "title": "\u03a7\u03bb\u03c9\u03c1\u03af\u03b4\u03b1" + "title": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c6\u03c5\u03c4\u03ce\u03bd" } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/el.json b/homeassistant/components/plex/translations/el.json index 3f447b48d86..679ef3d3937 100644 --- a/homeassistant/components/plex/translations/el.json +++ b/homeassistant/components/plex/translations/el.json @@ -4,7 +4,7 @@ "all_configured": "\u038c\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Plex \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "token_request_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, diff --git a/homeassistant/components/powerwall/translations/el.json b/homeassistant/components/powerwall/translations/el.json index d3649e44a8d..630dea5bd23 100644 --- a/homeassistant/components/powerwall/translations/el.json +++ b/homeassistant/components/powerwall/translations/el.json @@ -11,7 +11,7 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "wrong_version": "\u03a4\u03bf powerwall \u03c3\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03bf\u03cd \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u03a3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03ae \u03bd\u03b1 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03c5\u03b8\u03b5\u03af." }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ip_address});", diff --git a/homeassistant/components/pure_energie/translations/pt-BR.json b/homeassistant/components/pure_energie/translations/pt-BR.json index 148cd129376..b43c1c285ba 100644 --- a/homeassistant/components/pure_energie/translations/pt-BR.json +++ b/homeassistant/components/pure_energie/translations/pt-BR.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou em conectar" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha ao conectar" }, "error": { - "cannot_connect": "Falhou em conectar" + "cannot_connect": "Falha ao conectar" }, - "flow_title": "{model} ( {host} )", + "flow_title": "{model} ({host})", "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" } }, "zeroconf_confirm": { - "description": "Deseja adicionar medidor Pure Energie (` {model} `) ao Home Assistant?", + "description": "Deseja adicionar medidor Pure Energie (`{model}`) ao Home Assistant?", "title": "Descoberto o dispositivo medidor Pure Energie" } } diff --git a/homeassistant/components/radio_browser/translations/pt-BR.json b/homeassistant/components/radio_browser/translations/pt-BR.json index b25a8cbef92..2b5b6d1ad13 100644 --- a/homeassistant/components/radio_browser/translations/pt-BR.json +++ b/homeassistant/components/radio_browser/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { - "description": "Deseja adicionar o Radio Browser ao Home Assistant?" + "description": "Deseja adicionar o R\u00e1dio Browser ao Home Assistant?" } } } diff --git a/homeassistant/components/remote/translations/el.json b/homeassistant/components/remote/translations/el.json index 8f0221bdd74..431d2e8af79 100644 --- a/homeassistant/components/remote/translations/el.json +++ b/homeassistant/components/remote/translations/el.json @@ -18,9 +18,9 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "off": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" } }, - "title": "\u03a4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2" + "title": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03bf" } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/el.json b/homeassistant/components/risco/translations/el.json index 18799057793..b1c6ddb668e 100644 --- a/homeassistant/components/risco/translations/el.json +++ b/homeassistant/components/risco/translations/el.json @@ -32,8 +32,8 @@ }, "init": { "data": { - "code_arm_required": "\u039d\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc", - "code_disarm_required": "\u039d\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03b1\u03c6\u03bf\u03c0\u03bb\u03b9\u03c3\u03bc\u03cc", + "code_arm_required": "\u039d\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", + "code_disarm_required": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03b3\u03b9\u03b1 \u03b1\u03c6\u03cc\u03c0\u03bb\u03b9\u03c3\u03b7", "scan_interval": "\u03a0\u03cc\u03c3\u03bf \u03c3\u03c5\u03c7\u03bd\u03ac \u03bd\u03b1 \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03bb\u03ae\u03c8\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf Risco (\u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd" diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index 16fca72db26..e88cbed685c 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -16,7 +16,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03c4\u03b7\u03bd IP \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon." + "description": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon, \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03c4\u03b7\u03bd IP \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/screenlogic/translations/el.json b/homeassistant/components/screenlogic/translations/el.json index 26906ac3b29..0bb5a9a6322 100644 --- a/homeassistant/components/screenlogic/translations/el.json +++ b/homeassistant/components/screenlogic/translations/el.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/script/translations/el.json b/homeassistant/components/script/translations/el.json index 9fbc773c9f5..bfa0dbc9644 100644 --- a/homeassistant/components/script/translations/el.json +++ b/homeassistant/components/script/translations/el.json @@ -5,5 +5,5 @@ "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" } }, - "title": "\u0394\u03ad\u03c3\u03bc\u03b7 \u03b5\u03bd\u03b5\u03c1\u03b3\u03b5\u03b9\u03ce\u03bd" + "title": "\u03a3\u03b5\u03bd\u03ac\u03c1\u03b9\u03bf" } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pt-BR.json b/homeassistant/components/sense/translations/pt-BR.json index 5944daf63ca..13ea0e901c7 100644 --- a/homeassistant/components/sense/translations/pt-BR.json +++ b/homeassistant/components/sense/translations/pt-BR.json @@ -14,7 +14,7 @@ "data": { "password": "Senha" }, - "description": "A integra\u00e7\u00e3o do Sense precisa autenticar novamente sua conta {email} .", + "description": "A integra\u00e7\u00e3o do Sense precisa autenticar novamente sua conta {email}.", "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index 25b4e7bd72b..a3bf2cf6df9 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -60,8 +60,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2" } }, "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2" diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json index c4a52891406..e83971e34fe 100644 --- a/homeassistant/components/shelly/translations/el.json +++ b/homeassistant/components/shelly/translations/el.json @@ -9,10 +9,10 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "flow_title": "Shelly: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {host};\n\n\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} \u03c3\u03c4\u03bf {host}; \n\n \u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03bf\u03c5\u03bd \u03c0\u03c1\u03b9\u03bd \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7.\n \u039f\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c4\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03bf\u03cd\u03bd \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03c0\u03bb\u03ad\u03bf\u03bd \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03ae \u03bd\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." }, "credentials": { "data": { @@ -24,7 +24,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "description": "\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b1\u03c4\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." + "description": "\u03a0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7, \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03bf\u03c5\u03bd, \u03c4\u03ce\u03c1\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03be\u03c5\u03c0\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c0\u03ac\u03bd\u03c9 \u03c4\u03b7\u03c2." } } }, diff --git a/homeassistant/components/sonarr/translations/el.json b/homeassistant/components/sonarr/translations/el.json index a348d76f798..22895b3a38a 100644 --- a/homeassistant/components/sonarr/translations/el.json +++ b/homeassistant/components/sonarr/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "reauth_successful": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Sonarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf Sonarr API \u03c0\u03bf\u03c5 \u03c6\u03b9\u03bb\u03bf\u03be\u03b5\u03bd\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {host}", - "title": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Sonarr" + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Sonarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03bc\u03b5 \u03c4\u03bf Sonarr API \u03c0\u03bf\u03c5 \u03c6\u03b9\u03bb\u03bf\u03be\u03b5\u03bd\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {url}", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json index ba8f96413c3..4629e59e68d 100644 --- a/homeassistant/components/sonarr/translations/et.json +++ b/homeassistant/components/sonarr/translations/et.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {host}", + "description": "Sonarr-i sidumine tuleb k\u00e4sitsi taastuvastada Sonarr API abil: {url}", "title": "Autendi Sonarriga uuesti" }, "user": { @@ -22,6 +22,7 @@ "host": "", "port": "", "ssl": "Kasutab SSL serti", + "url": "URL", "verify_ssl": "Kontrolli SSL sertifikaati" } } diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index f5286094b73..c3cc2ac0f6c 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -22,6 +22,7 @@ "host": "C\u00edm", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "url": "URL", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } } diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 64785eed5ec..53a659d9bde 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -22,6 +22,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", + "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 312d4d5e91a..5ee028b8f02 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr-integrasjonen m\u00e5 autentiseres p\u00e5 nytt med Sonarr API vert p\u00e5: {host}", + "description": "Sonarr-integrasjonen m\u00e5 re-autentiseres manuelt med Sonarr API som er vert for: {url}", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { @@ -22,6 +22,7 @@ "host": "Vert", "port": "Port", "ssl": "Bruker et SSL-sertifikat", + "url": "URL", "verify_ssl": "Verifisere SSL-sertifikat" } } diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json index 7985bdf8f7f..e5be700a752 100644 --- a/homeassistant/components/sonarr/translations/pl.json +++ b/homeassistant/components/sonarr/translations/pl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Integracja Sonarr musi by\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 API Sonarr pod adresem: {host}", + "description": "Integracja Sonarr musi by\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 API Sonarr pod adresem: {url}", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { @@ -22,6 +22,7 @@ "host": "Nazwa hosta lub adres IP", "port": "Port", "ssl": "Certyfikat SSL", + "url": "URL", "verify_ssl": "Weryfikacja certyfikatu SSL" } } diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index 3f3b9593a09..531058f3fff 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {host}", + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Sonarr \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {url}", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { @@ -22,6 +22,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } } diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json index 0a107efae6e..4688ec2e438 100644 --- a/homeassistant/components/sonarr/translations/zh-Hant.json +++ b/homeassistant/components/sonarr/translations/zh-Hant.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Sonarr API\uff1a{host}", + "description": "Sonarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Sonarr API\uff1a{url}", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { @@ -22,6 +22,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "url": "\u7db2\u5740", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } } diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json index 099a7ebcd3b..1b9aadb7caf 100644 --- a/homeassistant/components/spotify/translations/el.json +++ b/homeassistant/components/spotify/translations/el.json @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Spotify \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc: {account}", - "title": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Spotify" + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } }, diff --git a/homeassistant/components/switch/translations/el.json b/homeassistant/components/switch/translations/el.json index e2aa65788f1..2c2903fdd6d 100644 --- a/homeassistant/components/switch/translations/el.json +++ b/homeassistant/components/switch/translations/el.json @@ -18,8 +18,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc\u03c2", + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc\u03c2" } }, "title": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2" diff --git a/homeassistant/components/system_health/translations/el.json b/homeassistant/components/system_health/translations/el.json index e0c4c7043f5..825f8420d82 100644 --- a/homeassistant/components/system_health/translations/el.json +++ b/homeassistant/components/system_health/translations/el.json @@ -1,3 +1,3 @@ { - "title": "\u03a5\u03b3\u03b5\u03af\u03b1 \u03a3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + "title": "\u03a5\u03b3\u03b5\u03af\u03b1 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/timer/translations/el.json b/homeassistant/components/timer/translations/el.json index ec4c4ab42e8..82d0c5c6b6e 100644 --- a/homeassistant/components/timer/translations/el.json +++ b/homeassistant/components/timer/translations/el.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc\u03c2", "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", - "paused": "\u03c3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7" + "paused": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7" } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 70c290590b1..c2f5d57d36d 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -18,7 +18,7 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ip_address});", + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({ip_address}); \u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u039a\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 UniFi OS \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5. \u039f\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 Ubiquiti Cloud \u03b4\u03b5\u03bd \u03b8\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03bf\u03c5\u03bd. \u0393\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: {local_user_documentation_url}", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf UniFi Protect" }, "reauth_confirm": { diff --git a/homeassistant/components/updater/translations/el.json b/homeassistant/components/updater/translations/el.json index b3ae655e025..f44dc928c16 100644 --- a/homeassistant/components/updater/translations/el.json +++ b/homeassistant/components/updater/translations/el.json @@ -1,3 +1,3 @@ { - "title": "\u0395\u03c0\u03b9\u03ba\u03b1\u03b9\u03c1\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03ae\u03c2" + "title": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03c4\u03ae\u03c2" } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/el.json b/homeassistant/components/uptimerobot/translations/el.json index 7ade0ad11a5..2f15a945ab0 100644 --- a/homeassistant/components/uptimerobot/translations/el.json +++ b/homeassistant/components/uptimerobot/translations/el.json @@ -17,14 +17,14 @@ "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}.", + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf {intergration}." + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot" } } } diff --git a/homeassistant/components/vacuum/translations/el.json b/homeassistant/components/vacuum/translations/el.json index bb068b11068..7c849d93998 100644 --- a/homeassistant/components/vacuum/translations/el.json +++ b/homeassistant/components/vacuum/translations/el.json @@ -15,12 +15,12 @@ }, "state": { "_": { - "cleaning": "\u039a\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2", + "cleaning": "\u039a\u03b1\u03b8\u03b1\u03c1\u03af\u03b6\u03b5\u03b9", "docked": "\u03a6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9", "error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1", "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", - "off": "\u039c\u03b7 \u0395\u03bd\u03b5\u03c1\u03b3\u03cc", - "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", + "off": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", + "on": "\u03a3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", "paused": "\u03a0\u03b1\u03cd\u03c3\u03b7", "returning": "\u03a0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03b9\u03c3\u03b7" } diff --git a/homeassistant/components/weather/translations/el.json b/homeassistant/components/weather/translations/el.json index 9127056dc1d..0a3a1f013cf 100644 --- a/homeassistant/components/weather/translations/el.json +++ b/homeassistant/components/weather/translations/el.json @@ -3,19 +3,19 @@ "_": { "clear-night": "\u039e\u03b1\u03c3\u03c4\u03b5\u03c1\u03b9\u03ac, \u03bd\u03cd\u03c7\u03c4\u03b1", "cloudy": "\u039d\u03b5\u03c6\u03b5\u03bb\u03ce\u03b4\u03b7\u03c2", - "exceptional": "\u0395\u03be\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc", + "exceptional": "\u0395\u03be\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc\u03c2", "fog": "\u039f\u03bc\u03af\u03c7\u03bb\u03b7", "hail": "\u03a7\u03b1\u03bb\u03ac\u03b6\u03b9", - "lightning": "\u0391\u03c3\u03c4\u03c1\u03b1\u03c0\u03ae", - "lightning-rainy": "\u039a\u03b1\u03c4\u03b1\u03b9\u03b3\u03af\u03b4\u03b1, \u03b2\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc", + "lightning": "\u0391\u03c3\u03c4\u03c1\u03b1\u03c0\u03ad\u03c2", + "lightning-rainy": "\u039a\u03b5\u03c1\u03b1\u03c5\u03bd\u03cc\u03c2, \u03b2\u03c1\u03bf\u03c7\u03ae", "partlycloudy": "\u039c\u03b5\u03c1\u03b9\u03ba\u03ce\u03c2 \u03bd\u03b5\u03c6\u03b5\u03bb\u03ce\u03b4\u03b7\u03c2", "pouring": "\u03a8\u03b9\u03c7\u03b1\u03bb\u03af\u03b6\u03b5\u03b9", - "rainy": "\u0392\u03c1\u03bf\u03c7\u03b5\u03c1\u03ae", - "snowy": "\u03a7\u03b9\u03bf\u03bd\u03ce\u03b4\u03b7\u03c2", - "snowy-rainy": "\u03a7\u03b9\u03bf\u03bd\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf, \u03b2\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc", - "sunny": "\u0397\u03bb\u03b9\u03cc\u03bb\u03bf\u03c5\u03c3\u03c4\u03bf", - "windy": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b5\u03b9\u03c2", - "windy-variant": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b5\u03b9\u03c2" + "rainy": "\u0392\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc\u03c2", + "snowy": "\u03a7\u03b9\u03bf\u03bd\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2", + "snowy-rainy": "\u03a7\u03b9\u03bf\u03bd\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2, \u03b2\u03c1\u03bf\u03c7\u03b5\u03c1\u03cc\u03c2", + "sunny": "\u0397\u03bb\u03b9\u03cc\u03bb\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2", + "windy": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b7\u03c2", + "windy-variant": "\u0398\u03c5\u03b5\u03bb\u03bb\u03ce\u03b4\u03b7\u03c2" } } } \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json index e83a8386341..5bb3130753a 100644 --- a/homeassistant/components/wilight/translations/el.json +++ b/homeassistant/components/wilight/translations/el.json @@ -5,7 +5,7 @@ "not_supported_device": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf WiLight \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd", "not_wilight_device": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 WiLight" }, - "flow_title": "WiLight: {\u03cc\u03bd\u03bf\u03bc\u03b1}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf WiLight {name} ;\n\n \u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9: {components}", diff --git a/homeassistant/components/wiz/translations/el.json b/homeassistant/components/wiz/translations/el.json index 8b0f37864c5..aa137e84fe0 100644 --- a/homeassistant/components/wiz/translations/el.json +++ b/homeassistant/components/wiz/translations/el.json @@ -6,7 +6,7 @@ "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, "error": { - "bulb_time_out": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1. \u038a\u03c3\u03c9\u03c2 \u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03bb\u03ac\u03b8\u03bf\u03c2 IP/host. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03c6\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac!", + "bulb_time_out": "\u0394\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1. \u038a\u03c3\u03c9\u03c2 \u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ae \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03bb\u03ac\u03b8\u03bf\u03c2 IP. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03ac\u03c8\u03c4\u03b5 \u03c4\u03bf \u03c6\u03c9\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac!", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "no_ip": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP.", "no_wiz_light": "\u039f \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 WiZ.", @@ -30,7 +30,7 @@ "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03bd\u03ad\u03bf \u03bb\u03b1\u03bc\u03c0\u03c4\u03ae\u03c1\u03b1:" + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b5\u03bd\u03ae, \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/yeelight/translations/el.json b/homeassistant/components/yeelight/translations/el.json index b125dae0bc6..8b6c0ee2b9c 100644 --- a/homeassistant/components/yeelight/translations/el.json +++ b/homeassistant/components/yeelight/translations/el.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf", "nightlight_switch": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03bd\u03c5\u03c7\u03c4\u03b5\u03c1\u03b9\u03bd\u03bf\u03cd \u03c6\u03c9\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd", "save_on_change": "\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae", "transition": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 (ms)", diff --git a/homeassistant/components/zoneminder/translations/el.json b/homeassistant/components/zoneminder/translations/el.json index c370d342e1f..c5614ab9ec8 100644 --- a/homeassistant/components/zoneminder/translations/el.json +++ b/homeassistant/components/zoneminder/translations/el.json @@ -23,7 +23,7 @@ "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", + "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "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" }, diff --git a/homeassistant/components/zwave/translations/el.json b/homeassistant/components/zwave/translations/el.json index ca7149b26b5..48b5eba30f6 100644 --- a/homeassistant/components/zwave/translations/el.json +++ b/homeassistant/components/zwave/translations/el.json @@ -25,8 +25,8 @@ "sleeping": "\u039a\u03bf\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9" }, "query_stage": { - "dead": "\u039d\u03b5\u03ba\u03c1\u03cc ( {query_stage} )", - "initializing": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 ( {query_stage} )" + "dead": "\u039d\u03b5\u03ba\u03c1\u03cc", + "initializing": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" } } } \ No newline at end of file From fff74c66ae1bb7098e77bf9e52057e95842244b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 16:21:24 -0800 Subject: [PATCH 0004/1054] Fix SQL sensor (#67144) --- homeassistant/components/sql/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 1c8e87051be..1c8514d0d26 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date +import decimal import logging import re @@ -158,7 +159,7 @@ class SQLSensor(SensorEntity): _LOGGER.debug("result = %s", res.items()) data = res[self._column_name] for key, value in res.items(): - if isinstance(value, float): + if isinstance(value, decimal.Decimal): value = float(value) if isinstance(value, date): value = value.isoformat() From a5383e40ebb81e84f37e9e6b8134440f202e923a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 16:22:39 -0800 Subject: [PATCH 0005/1054] Media source to verify domain to avoid KeyError (#67137) --- .../components/media_source/__init__.py | 17 +++++++++++------ tests/components/media_source/test_init.py | 4 ++++ tests/components/netatmo/test_media_source.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index e2bd1b4903b..77b254dcf9d 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -85,11 +85,16 @@ def _get_media_item( ) -> MediaSourceItem: """Return media item.""" if media_content_id: - return MediaSourceItem.from_uri(hass, media_content_id) + item = MediaSourceItem.from_uri(hass, media_content_id) + else: + # We default to our own domain if its only one registered + domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN + return MediaSourceItem(hass, domain, "") - # We default to our own domain if its only one registered - domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN - return MediaSourceItem(hass, domain, "") + if item.domain is not None and item.domain not in hass.data[DOMAIN]: + raise ValueError("Unknown media source") + + return item @bind_hass @@ -106,7 +111,7 @@ async def async_browse_media( try: item = await _get_media_item(hass, media_content_id).async_browse() except ValueError as err: - raise BrowseError("Not a media source item") from err + raise BrowseError(str(err)) from err if content_filter is None or item.children is None: return item @@ -128,7 +133,7 @@ async def async_resolve_media(hass: HomeAssistant, media_content_id: str) -> Pla try: item = _get_media_item(hass, media_content_id) except ValueError as err: - raise Unresolvable("Not a media source item") from err + raise Unresolvable(str(err)) from err return await item.async_resolve() diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index e36ccdac931..319ef295be3 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -98,6 +98,10 @@ async def test_async_unresolve_media(hass): with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media(hass, "invalid") + # Test invalid media source + with pytest.raises(media_source.Unresolvable): + await media_source.async_resolve_media(hass, "media-source://media_source2") + async def test_websocket_browse_media(hass, hass_ws_client): """Test browse media websocket.""" diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index c4741672186..db1a79145b4 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -54,7 +54,7 @@ async def test_async_browse_media(hass): # Test invalid base with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") - assert str(excinfo.value) == "Not a media source item" + assert str(excinfo.value) == "Invalid media source URI" # Test successful listing media = await media_source.async_browse_media( From 79bdd71da707c384bc4fc38234f231c3f7538eaa Mon Sep 17 00:00:00 2001 From: soluga <33458264+soluga@users.noreply.github.com> Date: Thu, 24 Feb 2022 01:29:26 +0100 Subject: [PATCH 0006/1054] Don't try to resolve state if native_value is Null (#67134) --- homeassistant/components/wolflink/sensor.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index f1a94cbbe20..a39b03fbd9f 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -152,10 +152,11 @@ class WolfLinkState(WolfLinkSensor): def native_value(self): """Return the state converting with supported values.""" state = super().native_value - resolved_state = [ - item for item in self.wolf_object.items if item.value == int(state) - ] - if resolved_state: - resolved_name = resolved_state[0].name - return STATES.get(resolved_name, resolved_name) + if state is not None: + resolved_state = [ + item for item in self.wolf_object.items if item.value == int(state) + ] + if resolved_state: + resolved_name = resolved_state[0].name + return STATES.get(resolved_name, resolved_name) return state From a42547c0e5b58ac49952d07edea1b0d42f2c2aa0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Feb 2022 21:15:48 -0800 Subject: [PATCH 0007/1054] Allow get_states to recover (#67146) --- .../components/websocket_api/commands.py | 34 ++++++++++++++++++- .../components/websocket_api/test_commands.py | 13 +++++-- tests/components/websocket_api/test_http.py | 3 +- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 650013dda7f..4ed0a9ada96 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -34,6 +34,10 @@ from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from . import const, decorators, messages from .connection import ActiveConnection @@ -225,7 +229,35 @@ def handle_get_states( if entity_perm(state.entity_id, "read") ] - connection.send_result(msg["id"], states) + # JSON serialize here so we can recover if it blows up due to the + # state machine containing unserializable data. This command is required + # to succeed for the UI to show. + response = messages.result_message(msg["id"], states) + try: + connection.send_message(const.JSON_DUMP(response)) + return + except (ValueError, TypeError): + connection.logger.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(response, dump=const.JSON_DUMP) + ), + ) + del response + + # If we can't serialize, we'll filter out unserializable states + serialized = [] + for state in states: + try: + serialized.append(const.JSON_DUMP(state)) + except (ValueError, TypeError): + # Error is already logged above + pass + + # We now have partially serialized states. Craft some JSON. + response2 = const.JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) + response2 = response2.replace('"TO_REPLACE"', ", ".join(serialized)) + connection.send_message(response2) @decorators.websocket_command({vol.Required("type"): "get_services"}) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 130870f73f0..742d9bddd38 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -587,13 +587,20 @@ async def test_states_filters_visible(hass, hass_admin_user, websocket_client): async def test_get_states_not_allows_nan(hass, websocket_client): """Test get_states command not allows NaN floats.""" - hass.states.async_set("greeting.hello", "world", {"hello": float("NaN")}) + hass.states.async_set("greeting.hello", "world") + hass.states.async_set("greeting.bad", "data", {"hello": float("NaN")}) + hass.states.async_set("greeting.bye", "universe") await websocket_client.send_json({"id": 5, "type": "get_states"}) msg = await websocket_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == const.ERR_UNKNOWN_ERROR + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == [ + hass.states.get("greeting.hello").as_dict(), + hass.states.get("greeting.bye").as_dict(), + ] async def test_subscribe_unsubscribe_events_whitelist( diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 336c79d22b8..c3564d2b21b 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -76,7 +76,8 @@ async def test_non_json_message(hass, websocket_client, caplog): msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == const.TYPE_RESULT - assert not msg["success"] + assert msg["success"] + assert msg["result"] == [] assert ( f"Unable to serialize to JSON. Bad data found at $.result[0](State: test_domain.entity).attributes.bad={bad_data}(" in caplog.text From e431e98fff568a28a18851aaed5bca014fbfd138 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 24 Feb 2022 00:18:14 -0500 Subject: [PATCH 0008/1054] Bump aiopyarr to 22.2.2 (#67149) --- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 9c43bfed282..6a9b00d2041 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.2.1"], + "requirements": ["aiopyarr==22.2.2"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 395b221ea61..9cf25c8e2f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -245,7 +245,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.2.1 +aiopyarr==22.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf79d2d991c..2d577e3d8a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.2.1 +aiopyarr==22.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From f8763aad752e91f62b9b4df03fc1f07247b83053 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 24 Feb 2022 00:27:31 -0500 Subject: [PATCH 0009/1054] SleepIQ Dependency update (#67154) --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 48ada7b14a2..93cd1be3204 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.0.0"], + "requirements": ["asyncsleepiq==1.1.0"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 9cf25c8e2f5..bbdd6a0ee91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,7 +354,7 @@ async-upnp-client==0.23.5 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.0.0 +asyncsleepiq==1.1.0 # homeassistant.components.aten_pe atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d577e3d8a2..4ecabd13b61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -259,7 +259,7 @@ arcam-fmj==0.12.0 async-upnp-client==0.23.5 # homeassistant.components.sleepiq -asyncsleepiq==1.0.0 +asyncsleepiq==1.1.0 # homeassistant.components.aurora auroranoaa==0.0.2 From 5366c0e3e32ae834fc93bf7800f714c956488967 Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Thu, 24 Feb 2022 01:05:45 -0500 Subject: [PATCH 0010/1054] Bump pydexcom to 0.2.3 (#67152) --- homeassistant/components/dexcom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json index b60ea3a576c..25193019f7d 100644 --- a/homeassistant/components/dexcom/manifest.json +++ b/homeassistant/components/dexcom/manifest.json @@ -3,7 +3,7 @@ "name": "Dexcom", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dexcom", - "requirements": ["pydexcom==0.2.2"], + "requirements": ["pydexcom==0.2.3"], "codeowners": ["@gagebenne"], "iot_class": "cloud_polling", "loggers": ["pydexcom"] diff --git a/requirements_all.txt b/requirements_all.txt index bbdd6a0ee91..9b21741348f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1474,7 +1474,7 @@ pydeconz==87 pydelijn==1.0.0 # homeassistant.components.dexcom -pydexcom==0.2.2 +pydexcom==0.2.3 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ecabd13b61..93dacff8619 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -924,7 +924,7 @@ pydaikin==2.7.0 pydeconz==87 # homeassistant.components.dexcom -pydexcom==0.2.2 +pydexcom==0.2.3 # homeassistant.components.zwave pydispatcher==2.0.5 From c9e46d360b7f416a472f4bbab1c85a0da9cbe2fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Feb 2022 20:15:20 -1000 Subject: [PATCH 0011/1054] Use compact encoding for JSON websocket messages (#67148) Co-authored-by: Paulus Schoutsen --- homeassistant/components/websocket_api/commands.py | 4 +++- homeassistant/components/websocket_api/const.py | 4 +++- tests/components/websocket_api/test_messages.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 4ed0a9ada96..abc37dd2a0a 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -480,7 +480,9 @@ async def handle_subscribe_trigger( msg["id"], {"variables": variables, "context": context} ) connection.send_message( - json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False) + json.dumps( + message, cls=ExtendedJSONEncoder, allow_nan=False, separators=(",", ":") + ) ) connection.subscriptions[msg["id"]] = ( diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 9428d6fd87d..6c5615ad253 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -53,4 +53,6 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, allow_nan=False) +JSON_DUMP: Final = partial( + json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") +) diff --git a/tests/components/websocket_api/test_messages.py b/tests/components/websocket_api/test_messages.py index 3ec156e6949..618879f4b7f 100644 --- a/tests/components/websocket_api/test_messages.py +++ b/tests/components/websocket_api/test_messages.py @@ -83,13 +83,13 @@ async def test_message_to_json(caplog): json_str = message_to_json({"id": 1, "message": "xyz"}) - assert json_str == '{"id": 1, "message": "xyz"}' + assert json_str == '{"id":1,"message":"xyz"}' json_str2 = message_to_json({"id": 1, "message": _Unserializeable()}) assert ( json_str2 - == '{"id": 1, "type": "result", "success": false, "error": {"code": "unknown_error", "message": "Invalid JSON in response"}}' + == '{"id":1,"type":"result","success":false,"error":{"code":"unknown_error","message":"Invalid JSON in response"}}' ) assert "Unable to serialize to JSON" in caplog.text From 6bd21f05dc1ebaee322b2898f8ddec55e3977200 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 10:02:42 +0100 Subject: [PATCH 0012/1054] Remove deprecated YAML configuration from PVOutput (#67162) * Remove deprecated YAML configuration from PVOutput * Clean up platform schema --- .../components/pvoutput/config_flow.py | 12 +---- homeassistant/components/pvoutput/const.py | 2 - homeassistant/components/pvoutput/sensor.py | 44 +------------------ tests/components/pvoutput/test_config_flow.py | 31 +------------ tests/components/pvoutput/test_init.py | 31 +------------ 5 files changed, 5 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index 53eabe225f6..4349a79593e 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -7,7 +7,7 @@ from pvo import PVOutput, PVOutputAuthenticationError, PVOutputError import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -83,16 +83,6 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - self.imported_name = config[CONF_NAME] - return await self.async_step_user( - user_input={ - CONF_SYSTEM_ID: config[CONF_SYSTEM_ID], - CONF_API_KEY: config[CONF_API_KEY], - } - ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle initiation of re-authentication with PVOutput.""" self.reauth_entry = self.hass.config_entries.async_get_entry( diff --git a/homeassistant/components/pvoutput/const.py b/homeassistant/components/pvoutput/const.py index dd2aca530ed..084ff809b27 100644 --- a/homeassistant/components/pvoutput/const.py +++ b/homeassistant/components/pvoutput/const.py @@ -21,5 +21,3 @@ ATTR_POWER_CONSUMPTION = "power_consumption" ATTR_EFFICIENCY = "efficiency" CONF_SYSTEM_ID = "system_id" - -DEFAULT_NAME = "PVOutput" diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index ef8537c548e..471f4483a47 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -5,21 +5,17 @@ from collections.abc import Callable from dataclasses import dataclass from pvo import Status, System -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, - CONF_API_KEY, - CONF_NAME, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, @@ -28,10 +24,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -41,20 +35,10 @@ from .const import ( ATTR_POWER_CONSUMPTION, ATTR_POWER_GENERATION, CONF_SYSTEM_ID, - DEFAULT_NAME, DOMAIN, - LOGGER, ) from .coordinator import PVOutputDataUpdateCoordinator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SYSTEM_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - @dataclass class PVOutputSensorEntityDescriptionMixin: @@ -129,32 +113,6 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PVOutput sensor.""" - LOGGER.warning( - "Configuration of the PVOutput platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_SYSTEM_ID: config[CONF_SYSTEM_ID], - CONF_API_KEY: config[CONF_API_KEY], - CONF_NAME: config[CONF_NAME], - }, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 8cd776beea3..444a35565f6 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock from pvo import PVOutputAuthenticationError, PVOutputConnectionError from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -151,33 +151,6 @@ async def test_already_configured( assert result2.get("reason") == "already_configured" -async def test_import_flow( - hass: HomeAssistant, - mock_pvoutput_config_flow: MagicMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_SYSTEM_ID: 1337, - CONF_API_KEY: "tadaaa", - CONF_NAME: "Test", - }, - ) - - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "Test" - assert result.get("data") == { - CONF_SYSTEM_ID: 1337, - CONF_API_KEY: "tadaaa", - } - - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 - - async def test_reauth_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, diff --git a/tests/components/pvoutput/test_init.py b/tests/components/pvoutput/test_init.py index b583e0807e0..87c432cebb2 100644 --- a/tests/components/pvoutput/test_init.py +++ b/tests/components/pvoutput/test_init.py @@ -8,12 +8,9 @@ from pvo import ( ) import pytest -from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.pvoutput.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -82,29 +79,3 @@ async def test_config_entry_authentication_failed( assert "context" in flow assert flow["context"].get("source") == SOURCE_REAUTH assert flow["context"].get("entry_id") == mock_config_entry.entry_id - - -async def test_import_config( - hass: HomeAssistant, - mock_pvoutput_config_flow: MagicMock, - mock_pvoutput: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test PVOutput being set up from config via import.""" - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_SYSTEM_ID: 12345, - CONF_API_KEY: "abcdefghijklmnopqrstuvwxyz", - } - }, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_pvoutput.status.mock_calls) == 1 - assert len(mock_pvoutput.system.mock_calls) == 1 - assert "the PVOutput platform in YAML is deprecated" in caplog.text From 1d03313bf5f8c524f19e08387fbcb675444da0f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 10:03:42 +0100 Subject: [PATCH 0013/1054] Remove deprecated YAML configuration from Whois (#67163) * Remove deprecated YAML configuration from Whois * Clean up platform schema --- homeassistant/components/whois/config_flow.py | 11 +---- homeassistant/components/whois/const.py | 2 - homeassistant/components/whois/sensor.py | 47 ++----------------- tests/components/whois/test_config_flow.py | 25 +--------- tests/components/whois/test_init.py | 22 --------- 5 files changed, 6 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/whois/config_flow.py b/homeassistant/components/whois/config_flow.py index 93a15f7fa39..640daaa314c 100644 --- a/homeassistant/components/whois/config_flow.py +++ b/homeassistant/components/whois/config_flow.py @@ -13,7 +13,7 @@ from whois.exceptions import ( ) from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_DOMAIN, CONF_NAME +from homeassistant.const import CONF_DOMAIN from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -69,12 +69,3 @@ class WhoisFlowHandler(ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - self.imported_name = config[CONF_NAME] - return await self.async_step_user( - user_input={ - CONF_DOMAIN: config[CONF_DOMAIN], - } - ) diff --git a/homeassistant/components/whois/const.py b/homeassistant/components/whois/const.py index 8530d2e558f..3fbbd6ff3ab 100644 --- a/homeassistant/components/whois/const.py +++ b/homeassistant/components/whois/const.py @@ -14,8 +14,6 @@ LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=24) -DEFAULT_NAME = "Whois" - ATTR_EXPIRES = "expires" ATTR_NAME_SERVERS = "name_servers" ATTR_REGISTRAR = "registrar" diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 3d0b25640b3..48efaf7630d 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -6,44 +6,25 @@ from dataclasses import dataclass from datetime import datetime, timezone from typing import cast -import voluptuous as vol from whois import Domain from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_DOMAIN, CONF_NAME, TIME_DAYS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DOMAIN, TIME_DAYS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import ( - ATTR_EXPIRES, - ATTR_NAME_SERVERS, - ATTR_REGISTRAR, - ATTR_UPDATED, - DEFAULT_NAME, - DOMAIN, - LOGGER, -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DOMAIN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) +from .const import ATTR_EXPIRES, ATTR_NAME_SERVERS, ATTR_REGISTRAR, ATTR_UPDATED, DOMAIN @dataclass @@ -152,28 +133,6 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the WHOIS sensor.""" - LOGGER.warning( - "Configuration of the Whois platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_DOMAIN: config[CONF_DOMAIN], CONF_NAME: config[CONF_NAME]}, - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index be73cfe7b7e..4bf6d7e8731 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -10,8 +10,8 @@ from whois.exceptions import ( ) from homeassistant.components.whois.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_DOMAIN, CONF_NAME +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -124,24 +124,3 @@ async def test_already_configured( assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 - - -async def test_import_flow( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, - mock_whois_config_flow: MagicMock, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_DOMAIN: "Example.com", CONF_NAME: "My Example Domain"}, - ) - - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "My Example Domain" - assert result.get("data") == { - CONF_DOMAIN: "example.com", - } - - assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/whois/test_init.py b/tests/components/whois/test_init.py index 3cd9efc801d..efb68a8e800 100644 --- a/tests/components/whois/test_init.py +++ b/tests/components/whois/test_init.py @@ -9,12 +9,9 @@ from whois.exceptions import ( WhoisCommandFailed, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.whois.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -59,22 +56,3 @@ async def test_error_handling( assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert len(mock_whois.mock_calls) == 1 - - -async def test_import_config( - hass: HomeAssistant, - mock_whois: MagicMock, - mock_whois_config_flow: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the Whois being set up from config via import.""" - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - {SENSOR_DOMAIN: {"platform": DOMAIN, CONF_DOMAIN: "home-assistant.io"}}, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_whois.mock_calls) == 1 - assert "the Whois platform in YAML is deprecated" in caplog.text From 85b87ffb8b322047639db4bb2c6abe356c0ad1a9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 10:31:00 +0100 Subject: [PATCH 0014/1054] Remove deprecated APNS integration (#67158) --- homeassistant/components/apns/__init__.py | 1 - homeassistant/components/apns/const.py | 2 - homeassistant/components/apns/manifest.json | 11 - homeassistant/components/apns/notify.py | 268 ------------------ homeassistant/components/apns/services.yaml | 0 homeassistant/components/notify/services.yaml | 22 -- 6 files changed, 304 deletions(-) delete mode 100644 homeassistant/components/apns/__init__.py delete mode 100644 homeassistant/components/apns/const.py delete mode 100644 homeassistant/components/apns/manifest.json delete mode 100644 homeassistant/components/apns/notify.py delete mode 100644 homeassistant/components/apns/services.yaml diff --git a/homeassistant/components/apns/__init__.py b/homeassistant/components/apns/__init__.py deleted file mode 100644 index 9332b0d1ede..00000000000 --- a/homeassistant/components/apns/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The apns component.""" diff --git a/homeassistant/components/apns/const.py b/homeassistant/components/apns/const.py deleted file mode 100644 index a8dc1204aa1..00000000000 --- a/homeassistant/components/apns/const.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Constants for the apns component.""" -DOMAIN = "apns" diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json deleted file mode 100644 index bcefdcf0639..00000000000 --- a/homeassistant/components/apns/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "disabled": "Integration library not compatible with Python 3.10", - "domain": "apns", - "name": "Apple Push Notification Service (APNS)", - "documentation": "https://www.home-assistant.io/integrations/apns", - "requirements": ["apns2==0.3.0"], - "after_dependencies": ["device_tracker"], - "codeowners": [], - "iot_class": "cloud_push", - "loggers": ["apns2", "hyper"] -} diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py deleted file mode 100644 index 8d0dcc334e9..00000000000 --- a/homeassistant/components/apns/notify.py +++ /dev/null @@ -1,268 +0,0 @@ -"""APNS Notification platform.""" -# pylint: disable=import-error -from contextlib import suppress -import logging - -from apns2.client import APNsClient -from apns2.errors import Unregistered -from apns2.payload import Payload -import voluptuous as vol - -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM -from homeassistant.core import ServiceCall -from homeassistant.helpers import template as template_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_state_change - -from .const import DOMAIN - -APNS_DEVICES = "apns.yaml" -CONF_CERTFILE = "cert_file" -CONF_TOPIC = "topic" -CONF_SANDBOX = "sandbox" - -ATTR_PUSH_ID = "push_id" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PLATFORM): "apns", - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_CERTFILE): cv.isfile, - vol.Required(CONF_TOPIC): cv.string, - vol.Optional(CONF_SANDBOX, default=False): cv.boolean, - } -) - -REGISTER_SERVICE_SCHEMA = vol.Schema( - {vol.Required(ATTR_PUSH_ID): cv.string, vol.Optional(ATTR_NAME): cv.string} -) - -_LOGGER = logging.getLogger(__name__) - - -def get_service(hass, config, discovery_info=None): - """Return push service.""" - _LOGGER.warning( - "The Apple Push Notification Service (APNS) integration is deprecated " - "and will be removed in Home Assistant Core 2022.4" - ) - - name = config[CONF_NAME] - cert_file = config[CONF_CERTFILE] - topic = config[CONF_TOPIC] - sandbox = config[CONF_SANDBOX] - - service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) - hass.services.register( - DOMAIN, f"apns_{name}", service.register, schema=REGISTER_SERVICE_SCHEMA - ) - return service - - -class ApnsDevice: - """ - The APNS Device class. - - Stores information about a device that is registered for push - notifications. - """ - - def __init__(self, push_id, name, tracking_device_id=None, disabled=False): - """Initialize APNS Device.""" - self.device_push_id = push_id - self.device_name = name - self.tracking_id = tracking_device_id - self.device_disabled = disabled - - @property - def push_id(self): - """Return the APNS id for the device.""" - return self.device_push_id - - @property - def name(self): - """Return the friendly name for the device.""" - return self.device_name - - @property - def tracking_device_id(self): - """ - Return the device Id. - - The id of a device that is tracked by the device - tracking component. - """ - return self.tracking_id - - @property - def full_tracking_device_id(self): - """ - Return the fully qualified device id. - - The full id of a device that is tracked by the device - tracking component. - """ - return f"{DEVICE_TRACKER_DOMAIN}.{self.tracking_id}" - - @property - def disabled(self): - """Return the state of the service.""" - return self.device_disabled - - def disable(self): - """Disable the device from receiving notifications.""" - self.device_disabled = True - - def __eq__(self, other): - """Return the comparison.""" - if isinstance(other, self.__class__): - return self.push_id == other.push_id and self.name == other.name - return NotImplemented - - def __ne__(self, other): - """Return the comparison.""" - return not self.__eq__(other) - - -def _write_device(out, device): - """Write a single device to file.""" - attributes = [] - if device.name is not None: - attributes.append(f"name: {device.name}") - if device.tracking_device_id is not None: - attributes.append(f"tracking_device_id: {device.tracking_device_id}") - if device.disabled: - attributes.append("disabled: True") - - out.write(device.push_id) - out.write(": {") - if attributes: - separator = ", " - out.write(separator.join(attributes)) - - out.write("}\n") - - -class ApnsNotificationService(BaseNotificationService): - """Implement the notification service for the APNS service.""" - - def __init__(self, hass, app_name, topic, sandbox, cert_file): - """Initialize APNS application.""" - self.hass = hass - self.app_name = app_name - self.sandbox = sandbox - self.certificate = cert_file - self.yaml_path = hass.config.path(f"{app_name}_{APNS_DEVICES}") - self.devices = {} - self.device_states = {} - self.topic = topic - - with suppress(FileNotFoundError): - self.devices = { - str(key): ApnsDevice( - str(key), - value.get("name"), - value.get("tracking_device_id"), - value.get("disabled", False), - ) - for (key, value) in load_yaml_config_file(self.yaml_path).items() - } - - tracking_ids = [ - device.full_tracking_device_id - for (key, device) in self.devices.items() - if device.tracking_device_id is not None - ] - track_state_change(hass, tracking_ids, self.device_state_changed_listener) - - def device_state_changed_listener(self, entity_id, from_s, to_s): - """ - Listen for state change. - - Track device state change if a device has a tracking id specified. - """ - self.device_states[entity_id] = str(to_s.state) - - def write_devices(self): - """Write all known devices to file.""" - with open(self.yaml_path, "w+", encoding="utf8") as out: - for device in self.devices.values(): - _write_device(out, device) - - def register(self, call: ServiceCall) -> None: - """Register a device to receive push messages.""" - push_id = call.data.get(ATTR_PUSH_ID) - - device_name = call.data.get(ATTR_NAME) - current_device = self.devices.get(push_id) - current_tracking_id = ( - None if current_device is None else current_device.tracking_device_id - ) - - device = ApnsDevice(push_id, device_name, current_tracking_id) - - if current_device is None: - self.devices[push_id] = device - with open(self.yaml_path, "a", encoding="utf8") as out: - _write_device(out, device) - return - - if device != current_device: - self.devices[push_id] = device - self.write_devices() - - def send_message(self, message=None, **kwargs): - """Send push message to registered devices.""" - - apns = APNsClient( - self.certificate, use_sandbox=self.sandbox, use_alternative_port=False - ) - - device_state = kwargs.get(ATTR_TARGET) - if (message_data := kwargs.get(ATTR_DATA)) is None: - message_data = {} - - if isinstance(message, str): - rendered_message = message - elif isinstance(message, template_helper.Template): - rendered_message = message.render(parse_result=False) - else: - rendered_message = "" - - payload = Payload( - alert=rendered_message, - badge=message_data.get("badge"), - sound=message_data.get("sound"), - category=message_data.get("category"), - custom=message_data.get("custom", {}), - content_available=message_data.get("content_available", False), - ) - - device_update = False - - for push_id, device in self.devices.items(): - if not device.disabled: - state = None - if device.tracking_device_id is not None: - state = self.device_states.get(device.full_tracking_device_id) - - if device_state is None or state == str(device_state): - try: - apns.send_notification(push_id, payload, topic=self.topic) - except Unregistered: - logging.error("Device %s has unregistered", push_id) - device_update = True - device.disable() - - if device_update: - self.write_devices() - - return True diff --git a/homeassistant/components/apns/services.yaml b/homeassistant/components/apns/services.yaml deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 7284cb68eb6..bc31aef1a6e 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -51,25 +51,3 @@ persistent_notification: example: "Your Garage Door Friend" selector: text: - -apns_register: - name: Register APNS device - description: - Registers a device to receive push notifications via APNS (Apple Push - Notification Service). - fields: - push_id: - name: Push ID - description: - The device token, a 64 character hex string (256 bits). The device token - is provided to you by your client app, which receives the token after - registering itself with the remote notification service. - example: "72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62" - selector: - text: - name: - name: Name - description: A friendly name for the device. - example: "Sam's iPhone" - selector: - text: From 14059c5aa9d942a6d01b48401fc37a5594475624 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 11:25:42 +0100 Subject: [PATCH 0015/1054] Remove deprecated YAML configuration from CPU Speed (#67166) --- .../components/cpuspeed/config_flow.py | 6 --- homeassistant/components/cpuspeed/sensor.py | 39 ++----------------- tests/components/cpuspeed/test_config_flow.py | 23 +---------- tests/components/cpuspeed/test_init.py | 18 --------- 4 files changed, 4 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/cpuspeed/config_flow.py b/homeassistant/components/cpuspeed/config_flow.py index 4b3c39f148a..3c7d529364c 100644 --- a/homeassistant/components/cpuspeed/config_flow.py +++ b/homeassistant/components/cpuspeed/config_flow.py @@ -6,7 +6,6 @@ from typing import Any from cpuinfo import cpuinfo from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -36,8 +35,3 @@ class CPUSpeedFlowHandler(ConfigFlow, domain=DOMAIN): title=self._imported_name or "CPU Speed", data={}, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle a flow initialized by importing a config.""" - self._imported_name = config.get(CONF_NAME) - return await self.async_step_user(user_input={}) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 8d21b365ad6..2c9db7e7300 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -2,17 +2,12 @@ from __future__ import annotations from cpuinfo import cpuinfo -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, FREQUENCY_GIGAHERTZ +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import FREQUENCY_GIGAHERTZ from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from .const import DOMAIN, LOGGER ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" @@ -21,34 +16,6 @@ ATTR_ARCH = "arch" HZ_ACTUAL = "hz_actual" HZ_ADVERTISED = "hz_advertised" -DEFAULT_NAME = "CPU speed" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the CPU speed sensor.""" - LOGGER.warning( - "Configuration of the CPU Speed platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.4; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_NAME: config[CONF_NAME]}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 14563c82bff..8f12092f389 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -3,8 +3,7 @@ from unittest.mock import AsyncMock, MagicMock from homeassistant.components.cpuspeed.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -62,26 +61,6 @@ async def test_already_configured( assert len(mock_cpuinfo_config_flow.mock_calls) == 0 -async def test_import_flow( - hass: HomeAssistant, - mock_cpuinfo_config_flow: MagicMock, - mock_setup_entry: AsyncMock, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_NAME: "Frenck's CPU"}, - ) - - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == "Frenck's CPU" - assert result.get("data") == {} - - assert len(mock_setup_entry.mock_calls) == 1 - assert len(mock_cpuinfo_config_flow.mock_calls) == 1 - - async def test_not_compatible( hass: HomeAssistant, mock_cpuinfo_config_flow: MagicMock, diff --git a/tests/components/cpuspeed/test_init.py b/tests/components/cpuspeed/test_init.py index 2352e411b8e..cdb86ba2f46 100644 --- a/tests/components/cpuspeed/test_init.py +++ b/tests/components/cpuspeed/test_init.py @@ -4,10 +4,8 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.cpuspeed.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -48,19 +46,3 @@ async def test_config_entry_not_compatible( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR assert len(mock_cpuinfo.mock_calls) == 1 assert "is not compatible with your system" in caplog.text - - -async def test_import_config( - hass: HomeAssistant, - mock_cpuinfo: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the CPU Speed being set up from config via import.""" - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": DOMAIN}} - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_cpuinfo.mock_calls) == 3 - assert "the CPU Speed platform in YAML is deprecated" in caplog.text From a12870081ea8db1a63f227c071f3e2cb67f7d795 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Thu, 24 Feb 2022 13:04:21 +0100 Subject: [PATCH 0016/1054] Remove deprecated yaml config from bmw_connected_drive (#66965) Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 23 ++------------ .../bmw_connected_drive/config_flow.py | 4 --- .../components/bmw_connected_drive/const.py | 1 - .../bmw_connected_drive/test_config_flow.py | 31 ------------------- 4 files changed, 3 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 400ab504c22..232788b21d7 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -11,7 +11,7 @@ from bimmer_connected.vehicle import ConnectedDriveVehicle import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_ID, CONF_NAME, @@ -33,7 +33,6 @@ import homeassistant.util.dt as dt_util from .const import ( ATTRIBUTION, CONF_ACCOUNT, - CONF_ALLOWED_REGIONS, CONF_READ_ONLY, DATA_ENTRIES, DATA_HASS_CONFIG, @@ -44,16 +43,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "bmw_connected_drive" ATTR_VIN = "vin" -ACCOUNT_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), - vol.Optional(CONF_READ_ONLY): cv.boolean, - } -) - -CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SERVICE_SCHEMA = vol.Schema( vol.Any( @@ -91,17 +81,10 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the BMW Connected Drive component from configuration.yaml.""" + # Store full yaml config in data for platform.NOTIFY hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][DATA_HASS_CONFIG] = config - if DOMAIN in config: - for entry_config in config[DOMAIN].values(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config - ) - ) - return True diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 3b07830c077..fec25390ff4 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -74,10 +74,6 @@ class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import.""" - return await self.async_step_user(user_input) - @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 0f79a16b702..f7908910803 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -12,7 +12,6 @@ ATTR_DIRECTION = "direction" CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] CONF_READ_ONLY = "read_only" -CONF_USE_LOCATION = "use_location" CONF_ACCOUNT = "account" diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index b0bc3ce292c..644da56a91d 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -68,8 +68,6 @@ async def test_full_user_flow_implementation(hass): "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", return_value=[], ), patch( - "homeassistant.components.bmw_connected_drive.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -82,32 +80,6 @@ async def test_full_user_flow_implementation(hass): assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_full_config_flow_implementation(hass): - """Test registering an integration and finishing flow works.""" - with patch( - "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", - return_value=[], - ), patch( - "homeassistant.components.bmw_connected_drive.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.bmw_connected_drive.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=FIXTURE_USER_INPUT, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == FIXTURE_IMPORT_ENTRY[CONF_USERNAME] - assert result["data"] == FIXTURE_IMPORT_ENTRY - - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -117,8 +89,6 @@ async def test_options_flow_implementation(hass): "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", return_value=[], ), patch( - "homeassistant.components.bmw_connected_drive.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -143,5 +113,4 @@ async def test_options_flow_implementation(hass): CONF_READ_ONLY: False, } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 9131fb39fe64f77f12447678ab2b7d29278ec74b Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 24 Feb 2022 13:35:45 +0100 Subject: [PATCH 0017/1054] Allow "slave" parameter in modbus service calls (#66874) * Allow "slave" parameter in modbus service calls. --- homeassistant/components/modbus/const.py | 1 + homeassistant/components/modbus/modbus.py | 16 +++++++++++++--- homeassistant/components/modbus/services.yaml | 18 +++++++++--------- tests/components/modbus/test_init.py | 12 ++++++++++-- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 934d14012f8..82c5245da80 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -69,6 +69,7 @@ UDP = "udp" ATTR_ADDRESS = "address" ATTR_HUB = "hub" ATTR_UNIT = "unit" +ATTR_SLAVE = "slave" ATTR_VALUE = "value" ATTR_STATE = "state" ATTR_TEMPERATURE = "temperature" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 20083bb3d1c..9d73f0afcc3 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -39,6 +39,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ADDRESS, ATTR_HUB, + ATTR_SLAVE, ATTR_STATE, ATTR_UNIT, ATTR_VALUE, @@ -156,7 +157,11 @@ async def async_modbus_setup( async def async_write_register(service: ServiceCall) -> None: """Write Modbus registers.""" - unit = int(float(service.data[ATTR_UNIT])) + unit = 0 + if ATTR_UNIT in service.data: + unit = int(float(service.data[ATTR_UNIT])) + if ATTR_SLAVE in service.data: + unit = int(float(service.data[ATTR_SLAVE])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] hub = hub_collect[ @@ -173,7 +178,11 @@ async def async_modbus_setup( async def async_write_coil(service: ServiceCall) -> None: """Write Modbus coil.""" - unit = service.data[ATTR_UNIT] + unit = 0 + if ATTR_UNIT in service.data: + unit = int(float(service.data[ATTR_UNIT])) + if ATTR_SLAVE in service.data: + unit = int(float(service.data[ATTR_SLAVE])) address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] hub = hub_collect[ @@ -195,7 +204,8 @@ async def async_modbus_setup( schema=vol.Schema( { vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, - vol.Required(ATTR_UNIT): cv.positive_int, + vol.Exclusive(ATTR_SLAVE, "unit"): cv.positive_int, + vol.Exclusive(ATTR_UNIT, "unit"): cv.positive_int, vol.Required(ATTR_ADDRESS): cv.positive_int, vol.Required(x_write[2]): vol.Any( cv.positive_int, vol.All(cv.ensure_list, [x_write[3]]) diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 87e8b98fa21..373954e25df 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -14,13 +14,13 @@ write_coil: name: State description: State to write. required: true - example: false + example: "0 or [1,0]" selector: object: - unit: - name: Unit - description: Address of the modbus unit. - required: true + slave: + name: Slave + description: Address of the modbus unit/slave. + required: false selector: number: min: 1 @@ -44,10 +44,10 @@ write_register: number: min: 0 max: 65535 - unit: - name: Unit - description: Address of the modbus unit. - required: true + slave: + name: Slave + description: Address of the modbus unit/slave. + required: false selector: number: min: 1 diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 9bd0a2caa6d..ba5a9291d81 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -25,6 +25,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, + ATTR_SLAVE, ATTR_STATE, ATTR_UNIT, ATTR_VALUE, @@ -484,8 +485,15 @@ SERVICE = "service" {VALUE: ModbusException("fail write_"), DATA: "Pymodbus:"}, ], ) +@pytest.mark.parametrize( + "do_unit", + [ + ATTR_UNIT, + ATTR_SLAVE, + ], +) async def test_pb_service_write( - hass, do_write, do_return, caplog, mock_modbus_with_pymodbus + hass, do_write, do_return, do_unit, caplog, mock_modbus_with_pymodbus ): """Run test for service write_register.""" @@ -498,7 +506,7 @@ async def test_pb_service_write( data = { ATTR_HUB: TEST_MODBUS_NAME, - ATTR_UNIT: 17, + do_unit: 17, ATTR_ADDRESS: 16, do_write[DATA]: do_write[VALUE], } From 636e4ed90b135a77bd9dd3955aa34be41b6e036f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 14:47:20 +0100 Subject: [PATCH 0018/1054] Remove deprecated Time of Flight integration (#67167) --- .coveragerc | 1 - homeassistant/components/tof/__init__.py | 1 - homeassistant/components/tof/manifest.json | 10 -- homeassistant/components/tof/sensor.py | 120 --------------------- requirements_all.txt | 3 - script/gen_requirements_all.py | 1 - 6 files changed, 136 deletions(-) delete mode 100644 homeassistant/components/tof/__init__.py delete mode 100644 homeassistant/components/tof/manifest.json delete mode 100644 homeassistant/components/tof/sensor.py diff --git a/.coveragerc b/.coveragerc index 360bd5f6911..ec22cbbfc67 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1244,7 +1244,6 @@ omit = homeassistant/components/tmb/sensor.py homeassistant/components/todoist/calendar.py homeassistant/components/todoist/const.py - homeassistant/components/tof/sensor.py homeassistant/components/tolo/__init__.py homeassistant/components/tolo/binary_sensor.py homeassistant/components/tolo/button.py diff --git a/homeassistant/components/tof/__init__.py b/homeassistant/components/tof/__init__.py deleted file mode 100644 index 0e72aca724b..00000000000 --- a/homeassistant/components/tof/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json deleted file mode 100644 index e530c67b930..00000000000 --- a/homeassistant/components/tof/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "tof", - "name": "Time of Flight", - "documentation": "https://www.home-assistant.io/integrations/tof", - "requirements": ["VL53L1X2==0.1.5"], - "dependencies": ["rpi_gpio"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["VL53L1X2"] -} diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py deleted file mode 100644 index 2934aa744aa..00000000000 --- a/homeassistant/components/tof/sensor.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" -from __future__ import annotations - -import asyncio -from functools import partial -import logging - -from VL53L1X2 import VL53L1X # pylint: disable=import-error -import voluptuous as vol - -from homeassistant.components import rpi_gpio -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME, LENGTH_MILLIMETERS -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_XSHUT = "xshut" - -DEFAULT_NAME = "VL53L1X" -DEFAULT_I2C_ADDRESS = 0x29 -DEFAULT_I2C_BUS = 1 -DEFAULT_XSHUT = 16 -DEFAULT_RANGE = 2 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - vol.Optional(CONF_XSHUT, default=DEFAULT_XSHUT): cv.positive_int, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def init_tof_0(xshut, sensor): - """XSHUT port LOW resets the device.""" - sensor.open() - rpi_gpio.setup_output(xshut) - rpi_gpio.write_output(xshut, 0) - - -def init_tof_1(xshut): - """XSHUT port HIGH enables the device.""" - rpi_gpio.setup_output(xshut) - rpi_gpio.write_output(xshut, 1) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Reset and initialize the VL53L1X ToF Sensor from STMicroelectronics.""" - _LOGGER.warning( - "The Time of Flight integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config.get(CONF_NAME) - bus_number = config.get(CONF_I2C_BUS) - i2c_address = config.get(CONF_I2C_ADDRESS) - unit = LENGTH_MILLIMETERS - xshut = config.get(CONF_XSHUT) - - sensor = await hass.async_add_executor_job(partial(VL53L1X, bus_number)) - await hass.async_add_executor_job(init_tof_0, xshut, sensor) - await asyncio.sleep(0.01) - await hass.async_add_executor_job(init_tof_1, xshut) - await asyncio.sleep(0.01) - - dev = [VL53L1XSensor(sensor, name, unit, i2c_address)] - - async_add_entities(dev, True) - - -class VL53L1XSensor(SensorEntity): - """Implementation of VL53L1X sensor.""" - - def __init__(self, vl53l1x_sensor, name, unit, i2c_address): - """Initialize the sensor.""" - self._name = name - self._unit_of_measurement = unit - self.vl53l1x_sensor = vl53l1x_sensor - self.i2c_address = i2c_address - self._state = None - self.init = True - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self) -> int: - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return self._unit_of_measurement - - def update(self): - """Get the latest measurement and update state.""" - if self.init: - self.vl53l1x_sensor.add_sensor(self.i2c_address, self.i2c_address) - self.init = False - self.vl53l1x_sensor.start_ranging(self.i2c_address, DEFAULT_RANGE) - self.vl53l1x_sensor.update(self.i2c_address) - self.vl53l1x_sensor.stop_ranging(self.i2c_address) - self._state = self.vl53l1x_sensor.distance diff --git a/requirements_all.txt b/requirements_all.txt index 9b21741348f..300bf91f289 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -77,9 +77,6 @@ TravisPy==0.3.5 # homeassistant.components.twitter TwitterAPI==2.7.5 -# homeassistant.components.tof -# VL53L1X2==0.1.5 - # homeassistant.components.onvif WSDiscovery==2.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fe8962e4f1e..6ebea07ae4b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -44,7 +44,6 @@ COMMENT_REQUIREMENTS = ( "smbus-cffi", "tensorflow", "tf-models-official", - "VL53L1X2", ) COMMENT_REQUIREMENTS_NORMALIZED = { From cbdfff25ca510a5d7dff9a1cb39ce2da8afc7d65 Mon Sep 17 00:00:00 2001 From: rforro Date: Thu, 24 Feb 2022 14:48:15 +0100 Subject: [PATCH 0019/1054] Presets for single ZONNSMART TRV (#67157) * Presets for single ZONNSMART TRV * added zonnsmart climate tests * black8 fix --- homeassistant/components/zha/climate.py | 65 ++++++++++++ tests/components/zha/test_climate.py | 126 ++++++++++++++++++++++++ 2 files changed, 191 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index d7e36c52517..4d91b84ec53 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -771,3 +771,68 @@ class StelproFanHeater(Thermostat): def hvac_modes(self) -> tuple[str, ...]: """Return only the heat mode, because the device can't be turned off.""" return (HVAC_MODE_HEAT,) + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers={ + "_TZE200_hue3yfsn", + }, +) +class ZONNSMARTThermostat(Thermostat): + """ + ZONNSMART Thermostat implementation. + + Notice that this device uses two holiday presets (2: HolidayMode, + 3: HolidayModeTemp), but only one of them can be set. + """ + + PRESET_HOLIDAY = "holiday" + PRESET_FROST = "frost protect" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize ZHA Thermostat instance.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._presets = [ + PRESET_NONE, + self.PRESET_HOLIDAY, + PRESET_SCHEDULE, + self.PRESET_FROST, + ] + self._supported_flags |= SUPPORT_PRESET_MODE + + async def async_attribute_updated(self, record): + """Handle attribute update from device.""" + if record.attr_name == "operation_preset": + if record.value == 0: + self._preset = PRESET_SCHEDULE + if record.value == 1: + self._preset = PRESET_NONE + if record.value == 2: + self._preset = self.PRESET_HOLIDAY + if record.value == 3: + self._preset = self.PRESET_HOLIDAY + if record.value == 4: + self._preset = self.PRESET_FROST + await super().async_attribute_updated(record) + + async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + """Set the preset mode.""" + mfg_code = self._zha_device.manufacturer_code + if not enable: + return await self._thrm.write_attributes( + {"operation_preset": 1}, manufacturer=mfg_code + ) + if preset == PRESET_SCHEDULE: + return await self._thrm.write_attributes( + {"operation_preset": 0}, manufacturer=mfg_code + ) + if preset == self.PRESET_HOLIDAY: + return await self._thrm.write_attributes( + {"operation_preset": 3}, manufacturer=mfg_code + ) + if preset == self.PRESET_FROST: + return await self._thrm.write_attributes( + {"operation_preset": 4}, manufacturer=mfg_code + ) + return False diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 8ff787af7bd..9f856ca1df6 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -137,9 +137,25 @@ CLIMATE_MOES = { SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], } } + +CLIMATE_ZONNSMART = { + 1: { + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, + SIG_EP_INPUT: [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + 61148, + ], + SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} + MANUF_SINOPE = "Sinope Technologies" MANUF_ZEN = "Zen Within" MANUF_MOES = "_TZE200_ckud7u2l" +MANUF_ZONNSMART = "_TZE200_hue3yfsn" ZCL_ATTR_PLUG = { "abs_min_heat_setpoint_limit": 800, @@ -232,6 +248,17 @@ async def device_climate_moes(device_climate_mock): ) +@pytest.fixture +async def device_climate_zonnsmart(device_climate_mock): + """ZONNSMART thermostat.""" + + return await device_climate_mock( + CLIMATE_ZONNSMART, + manuf=MANUF_ZONNSMART, + quirk=zhaquirks.tuya.ts0601_trv.ZonnsmartTV01_ZG, + ) + + def test_sequence_mappings(): """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" @@ -1326,3 +1353,102 @@ async def test_set_moes_operation_mode(hass, device_climate_moes): state = hass.states.get(entity_id) assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMPLEX + + +async def test_set_zonnsmart_preset(hass, device_climate_zonnsmart): + """Test setting preset from homeassistant for zonnsmart trv.""" + + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_zonnsmart, hass) + thrm_cluster = device_climate_zonnsmart.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 0 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "holiday"}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 1 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 3 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "frost protect"}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 1 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "operation_preset": 4 + } + + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "operation_preset": 1 + } + + +async def test_set_zonnsmart_operation_mode(hass, device_climate_zonnsmart): + """Test setting preset from trv for zonnsmart trv.""" + + entity_id = await find_entity_id(Platform.CLIMATE, device_climate_zonnsmart, hass) + thrm_cluster = device_climate_zonnsmart.device.endpoints[1].thermostat + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 0}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_SCHEDULE + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 1}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 2}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "holiday" + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 3}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "holiday" + + await send_attributes_report(hass, thrm_cluster, {"operation_preset": 4}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "frost protect" From 7068c46f8f356acef234b3589788b2644a56d7fc Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 24 Feb 2022 17:59:20 +0200 Subject: [PATCH 0020/1054] Use SSDP dataclass in webostv tests (#67181) --- tests/components/webostv/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index b2b20677513..a874532ff51 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -302,7 +302,7 @@ async def test_ssdp_update_uuid(hass, client): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - assert entry.unique_id == MOCK_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN][5:] + assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] async def test_ssdp_not_update_uuid(hass, client): @@ -326,9 +326,9 @@ async def test_ssdp_not_update_uuid(hass, client): async def test_form_abort_uuid_configured(hass, client): """Test abort if uuid is already configured, verify host update.""" - entry = await setup_webostv(hass, MOCK_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN][5:]) + entry = await setup_webostv(hass, MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:]) assert client - assert entry.unique_id == MOCK_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN][5:] + assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] assert entry.data[CONF_HOST] == HOST result = await hass.config_entries.flow.async_init( From fb4de7211bbffc9465987fabc97dc05d1566ac5a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 08:36:36 -0800 Subject: [PATCH 0021/1054] Make Google sync_seralize a callback (#67155) --- .../components/google_assistant/helpers.py | 104 ++++++++---------- .../components/google_assistant/smart_home.py | 24 ++-- .../google_assistant/test_helpers.py | 25 ++--- .../google_assistant/test_smart_home.py | 2 +- 4 files changed, 70 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index a348299e282..b1096d44d74 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -20,10 +20,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Context, HomeAssistant, State, callback -from homeassistant.helpers import start -from homeassistant.helpers.area_registry import AreaEntry -from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers import area_registry, device_registry, entity_registry, start from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store @@ -48,51 +45,33 @@ SYNC_DELAY = 15 _LOGGER = logging.getLogger(__name__) -async def _get_entity_and_device( +@callback +def _get_registry_entries( hass: HomeAssistant, entity_id: str -) -> tuple[RegistryEntry, DeviceEntry] | None: - """Fetch the entity and device entries for a entity_id.""" - dev_reg, ent_reg = await gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), - ) +) -> tuple[device_registry.DeviceEntry, area_registry.AreaEntry]: + """Get registry entries.""" + ent_reg = entity_registry.async_get(hass) + dev_reg = device_registry.async_get(hass) + area_reg = area_registry.async_get(hass) - if not (entity_entry := ent_reg.async_get(entity_id)): - return None, None - device_entry = dev_reg.devices.get(entity_entry.device_id) - return entity_entry, device_entry + if (entity_entry := ent_reg.async_get(entity_id)) and entity_entry.device_id: + device_entry = dev_reg.devices.get(entity_entry.device_id) + else: + device_entry = None - -async def _get_area( - hass: HomeAssistant, - entity_entry: RegistryEntry | None, - device_entry: DeviceEntry | None, -) -> AreaEntry | None: - """Calculate the area for an entity.""" if entity_entry and entity_entry.area_id: area_id = entity_entry.area_id elif device_entry and device_entry.area_id: area_id = device_entry.area_id else: - return None + area_id = None - area_reg = await hass.helpers.area_registry.async_get_registry() - return area_reg.areas.get(area_id) + if area_id is not None: + area_entry = area_reg.async_get_area(area_id) + else: + area_entry = None - -async def _get_device_info(device_entry: DeviceEntry | None) -> dict[str, str] | None: - """Retrieve the device info for a device.""" - if not device_entry: - return None - - device_info = {} - if device_entry.manufacturer: - device_info["manufacturer"] = device_entry.manufacturer - if device_entry.model: - device_info["model"] = device_entry.model - if device_entry.sw_version: - device_info["swVersion"] = device_entry.sw_version - return device_info + return device_entry, area_entry class AbstractConfig(ABC): @@ -559,60 +538,71 @@ class GoogleEntity: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - async def sync_serialize(self, agent_user_id): + def sync_serialize(self, agent_user_id, instance_uuid): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ state = self.state - + traits = self.traits() entity_config = self.config.entity_config.get(state.entity_id, {}) name = (entity_config.get(CONF_NAME) or state.name).strip() - domain = state.domain - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - entity_entry, device_entry = await _get_entity_and_device( - self.hass, state.entity_id - ) - traits = self.traits() - - device_type = get_google_type(domain, device_class) + # Find entity/device/area registry entries + device_entry, area_entry = _get_registry_entries(self.hass, self.entity_id) + # Build the device info device = { "id": state.entity_id, "name": {"name": name}, "attributes": {}, "traits": [trait.name for trait in traits], "willReportState": self.config.should_report_state, - "type": device_type, + "type": get_google_type( + state.domain, state.attributes.get(ATTR_DEVICE_CLASS) + ), } - # use aliases + # Add aliases if aliases := entity_config.get(CONF_ALIASES): device["name"]["nicknames"] = [name] + aliases + # Add local SDK info if enabled if self.config.is_local_sdk_active and self.should_expose_local(): device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.get_local_webhook_id(agent_user_id), "httpPort": self.hass.http.server_port, "httpSSL": self.hass.config.api.use_ssl, - "uuid": await self.hass.helpers.instance_id.async_get(), + "uuid": instance_uuid, "baseUrl": get_url(self.hass, prefer_external=True), "proxyDeviceId": agent_user_id, } + # Add trait sync attributes for trt in traits: device["attributes"].update(trt.sync_attributes()) + # Add roomhint if room := entity_config.get(CONF_ROOM_HINT): device["roomHint"] = room - else: - area = await _get_area(self.hass, entity_entry, device_entry) - if area and area.name: - device["roomHint"] = area.name + elif area_entry and area_entry.name: + device["roomHint"] = area_entry.name - if device_info := await _get_device_info(device_entry): + # Add deviceInfo + if not device_entry: + return device + + device_info = {} + + if device_entry.manufacturer: + device_info["manufacturer"] = device_entry.manufacturer + if device_entry.model: + device_info["model"] = device_entry.model + if device_entry.sw_version: + device_info["swVersion"] = device_entry.sw_version + + if device_info: device["deviceInfo"] = device_info return device diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 5f38194e3e3..f2aea247ecd 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -4,6 +4,7 @@ from itertools import product import logging from homeassistant.const import ATTR_ENTITY_ID, __version__ +from homeassistant.helpers import instance_id from homeassistant.util.decorator import Registry from .const import ( @@ -86,22 +87,17 @@ async def async_devices_sync(hass, data, payload): await data.config.async_connect_agent_user(agent_user_id) entities = async_get_entities(hass, data.config) - results = await asyncio.gather( - *( - entity.sync_serialize(agent_user_id) - for entity in entities - if entity.should_expose() - ), - return_exceptions=True, - ) - + instance_uuid = await instance_id.async_get(hass) devices = [] - for entity, result in zip(entities, results): - if isinstance(result, Exception): - _LOGGER.error("Error serializing %s", entity.entity_id, exc_info=result) - else: - devices.append(result) + for entity in entities: + if not entity.should_expose(): + continue + + try: + devices.append(entity.sync_serialize(agent_user_id, instance_uuid)) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error serializing %s", entity.entity_id) response = {"agentUserId": agent_user_id, "devices": devices} diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index dc29e5df4ab..4490f0e3963 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -47,30 +47,29 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): ) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) - serialized = await entity.sync_serialize(None) + serialized = entity.sync_serialize(None, "mock-uuid") assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): - serialized = await entity.sync_serialize("mock-user-id") - assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] - assert serialized["customData"] == { - "httpPort": 1234, - "httpSSL": False, - "proxyDeviceId": "mock-user-id", - "webhookId": "mock-webhook-id", - "baseUrl": "https://hostname:1234", - "uuid": "abcdef", - } + serialized = entity.sync_serialize("mock-user-id", "abcdef") + assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] + assert serialized["customData"] == { + "httpPort": 1234, + "httpSSL": False, + "proxyDeviceId": "mock-user-id", + "webhookId": "mock-webhook-id", + "baseUrl": "https://hostname:1234", + "uuid": "abcdef", + } for device_type in NOT_EXPOSE_LOCAL: with patch( "homeassistant.components.google_assistant.helpers.get_google_type", return_value=device_type, ): - serialized = await entity.sync_serialize(None) + serialized = entity.sync_serialize(None, "mock-uuid") assert "otherDeviceIds" not in serialized assert "customData" not in serialized diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3398fdca926..c3bbd9336f4 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -875,7 +875,7 @@ async def test_serialize_input_boolean(hass): state = State("input_boolean.bla", "on") # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = await entity.sync_serialize(None) + result = entity.sync_serialize(None, "mock-uuid") assert result == { "id": "input_boolean.bla", "attributes": {}, From 21f3e5ef13661f68404d79a7d1fed0b84a0c5f93 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 18:14:38 +0100 Subject: [PATCH 0022/1054] Fix MQTT config entry deprecation warnings (#67174) --- homeassistant/components/mqtt/__init__.py | 77 +++++++++++------------ 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c7652fe97b9..1982d1f3df5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -179,6 +179,40 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( required=True, ) +CONFIG_SCHEMA_BASE = vol.Schema( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL): vol.Any( + "auto", "1.0", "1.1", "1.2" + ), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) CONFIG_SCHEMA = vol.Schema( { @@ -191,44 +225,7 @@ CONFIG_SCHEMA = vol.Schema( cv.deprecated(CONF_TLS_VERSION), # Deprecated June 2020 cv.deprecated(CONF_USERNAME), # Deprecated in HA Core 2022.3 cv.deprecated(CONF_WILL_MESSAGE), # Deprecated in HA Core 2022.3 - vol.Schema( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional( - CONF_TLS_VERSION, default=DEFAULT_TLS_PROTOCOL - ): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional( - CONF_WILL_MESSAGE, default=DEFAULT_WILL - ): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional( - CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH - ): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } - ), + CONFIG_SCHEMA_BASE, ) }, extra=vol.ALLOW_EXTRA, @@ -619,7 +616,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" # If user didn't have configuration.yaml config, generate defaults if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() override = {k: entry.data[k] for k in shared_keys} @@ -811,7 +808,7 @@ class MQTT: self = hass.data[DATA_MQTT] if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) self.conf = _merge_config(entry, conf) await self.async_disconnect() From 12dbcca078e8f08afadff390bb1822cadba759fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 18:22:48 +0100 Subject: [PATCH 0023/1054] Remove deprecated BeagleBone Black GPIO integration (#67160) --- .coveragerc | 1 - homeassistant/components/bbb_gpio/__init__.py | 63 --------------- .../components/bbb_gpio/binary_sensor.py | 79 ------------------ .../components/bbb_gpio/manifest.json | 9 --- homeassistant/components/bbb_gpio/switch.py | 80 ------------------- requirements_all.txt | 3 - 6 files changed, 235 deletions(-) delete mode 100644 homeassistant/components/bbb_gpio/__init__.py delete mode 100644 homeassistant/components/bbb_gpio/binary_sensor.py delete mode 100644 homeassistant/components/bbb_gpio/manifest.json delete mode 100644 homeassistant/components/bbb_gpio/switch.py diff --git a/.coveragerc b/.coveragerc index ec22cbbfc67..3262c2703eb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -101,7 +101,6 @@ omit = homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py homeassistant/components/beewi_smartclim/sensor.py - homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py homeassistant/components/bh1750/sensor.py diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py deleted file mode 100644 index 511292cfb6f..00000000000 --- a/homeassistant/components/bbb_gpio/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Support for controlling GPIO pins of a Beaglebone Black.""" -import logging - -from Adafruit_BBIO import GPIO # pylint: disable=import-error - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "bbb_gpio" - -_LOGGER = logging.getLogger(__name__) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the BeagleBone Black GPIO component.""" - _LOGGER.warning( - "The BeagleBone Black GPIO integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - GPIO.cleanup() - - def prepare_gpio(event): - """Stuff to do when Home Assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - return True - - -def setup_output(pin): - """Set up a GPIO as output.""" - - GPIO.setup(pin, GPIO.OUT) - - -def setup_input(pin, pull_mode): - """Set up a GPIO as input.""" - - GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) - - -def write_output(pin, value): - """Write a value to a GPIO.""" - - GPIO.output(pin, value) - - -def read_input(pin): - """Read a value from a GPIO.""" - - return GPIO.input(pin) is GPIO.HIGH - - -def edge_detect(pin, event_callback, bounce): - """Add detection for RISING and FALLING events.""" - - GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py deleted file mode 100644 index 360d9d84376..00000000000 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Support for binary sensor using Beaglebone Black GPIO.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_PINS = "pins" -CONF_BOUNCETIME = "bouncetime" -CONF_INVERT_LOGIC = "invert_logic" -CONF_PULL_MODE = "pull_mode" - -DEFAULT_BOUNCETIME = 50 -DEFAULT_INVERT_LOGIC = False -DEFAULT_PULL_MODE = "UP" - -PIN_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]), - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Beaglebone Black GPIO devices.""" - pins = config[CONF_PINS] - - binary_sensors = [] - - for pin, params in pins.items(): - binary_sensors.append(BBBGPIOBinarySensor(pin, params)) - add_entities(binary_sensors) - - -class BBBGPIOBinarySensor(BinarySensorEntity): - """Representation of a binary sensor that uses Beaglebone Black GPIO.""" - - _attr_should_poll = False - - def __init__(self, pin, params): - """Initialize the Beaglebone Black binary sensor.""" - self._pin = pin - self._attr_name = params[CONF_NAME] or DEVICE_DEFAULT_NAME - self._bouncetime = params[CONF_BOUNCETIME] - self._pull_mode = params[CONF_PULL_MODE] - self._invert_logic = params[CONF_INVERT_LOGIC] - - bbb_gpio.setup_input(self._pin, self._pull_mode) - self._state = bbb_gpio.read_input(self._pin) - - def read_gpio(pin): - """Read state from GPIO.""" - self._state = bbb_gpio.read_input(self._pin) - self.schedule_update_ha_state() - - bbb_gpio.edge_detect(self._pin, read_gpio, self._bouncetime) - - @property - def is_on(self) -> bool: - """Return the state of the entity.""" - return self._state != self._invert_logic diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json deleted file mode 100644 index c57530a9bf8..00000000000 --- a/homeassistant/components/bbb_gpio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "bbb_gpio", - "name": "BeagleBone Black GPIO", - "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", - "requirements": ["Adafruit_BBIO==1.1.1"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["Adafruit_BBIO"] -} diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py deleted file mode 100644 index fc830d2a1a8..00000000000 --- a/homeassistant/components/bbb_gpio/switch.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Allows to configure a switch using BeagleBone Black GPIO.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import bbb_gpio -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_PINS = "pins" -CONF_INITIAL = "initial" -CONF_INVERT_LOGIC = "invert_logic" - -PIN_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL, default=False): cv.boolean, - vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BeagleBone Black GPIO devices.""" - pins = config[CONF_PINS] - - switches = [] - for pin, params in pins.items(): - switches.append(BBBGPIOSwitch(pin, params)) - add_entities(switches) - - -class BBBGPIOSwitch(SwitchEntity): - """Representation of a BeagleBone Black GPIO.""" - - _attr_should_poll = False - - def __init__(self, pin, params): - """Initialize the pin.""" - self._pin = pin - self._attr_name = params[CONF_NAME] or DEVICE_DEFAULT_NAME - self._state = params[CONF_INITIAL] - self._invert_logic = params[CONF_INVERT_LOGIC] - - bbb_gpio.setup_output(self._pin) - - if self._state is False: - bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) - else: - bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) - - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn the device on.""" - bbb_gpio.write_output(self._pin, 0 if self._invert_logic else 1) - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - bbb_gpio.write_output(self._pin, 1 if self._invert_logic else 0) - self._state = False - self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 300bf91f289..f5def50096e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,9 +10,6 @@ Adafruit-GPIO==1.0.3 # homeassistant.components.sht31 Adafruit-SHT31==1.0.2 -# homeassistant.components.bbb_gpio -# Adafruit_BBIO==1.1.1 - # homeassistant.components.adax Adax-local==0.1.3 From d495bded5c0ae447bfeb1824269b547aa306949e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 18:23:32 +0100 Subject: [PATCH 0024/1054] Remove deprecated BH1750 integration (#67161) --- .coveragerc | 1 - homeassistant/components/bh1750/__init__.py | 1 - homeassistant/components/bh1750/manifest.json | 9 -- homeassistant/components/bh1750/sensor.py | 138 ------------------ requirements_all.txt | 2 - 5 files changed, 151 deletions(-) delete mode 100644 homeassistant/components/bh1750/__init__.py delete mode 100644 homeassistant/components/bh1750/manifest.json delete mode 100644 homeassistant/components/bh1750/sensor.py diff --git a/.coveragerc b/.coveragerc index 3262c2703eb..78a07b8d916 100644 --- a/.coveragerc +++ b/.coveragerc @@ -103,7 +103,6 @@ omit = homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py - homeassistant/components/bh1750/sensor.py homeassistant/components/bitcoin/sensor.py homeassistant/components/bizkaibus/sensor.py homeassistant/components/blink/__init__.py diff --git a/homeassistant/components/bh1750/__init__.py b/homeassistant/components/bh1750/__init__.py deleted file mode 100644 index ce7ecc65366..00000000000 --- a/homeassistant/components/bh1750/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The bh1750 component.""" diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json deleted file mode 100644 index 807f7a9e05f..00000000000 --- a/homeassistant/components/bh1750/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "bh1750", - "name": "BH1750", - "documentation": "https://www.home-assistant.io/integrations/bh1750", - "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["i2csense", "smbus"] -} diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py deleted file mode 100644 index d6239f90d43..00000000000 --- a/homeassistant/components/bh1750/sensor.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Support for BH1750 light sensor.""" -from __future__ import annotations - -from functools import partial -import logging - -from i2csense.bh1750 import BH1750 # pylint: disable=import-error -import smbus -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.const import CONF_NAME, LIGHT_LUX -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_OPERATION_MODE = "operation_mode" -CONF_SENSITIVITY = "sensitivity" -CONF_DELAY = "measurement_delay_ms" -CONF_MULTIPLIER = "multiplier" - -# Operation modes for BH1750 sensor (from the datasheet). Time typically 120ms -# In one time measurements, device is set to Power Down after each sample. -CONTINUOUS_LOW_RES_MODE = "continuous_low_res_mode" -CONTINUOUS_HIGH_RES_MODE_1 = "continuous_high_res_mode_1" -CONTINUOUS_HIGH_RES_MODE_2 = "continuous_high_res_mode_2" -ONE_TIME_LOW_RES_MODE = "one_time_low_res_mode" -ONE_TIME_HIGH_RES_MODE_1 = "one_time_high_res_mode_1" -ONE_TIME_HIGH_RES_MODE_2 = "one_time_high_res_mode_2" -OPERATION_MODES = { - CONTINUOUS_LOW_RES_MODE: (0x13, True), # 4lx resolution - CONTINUOUS_HIGH_RES_MODE_1: (0x10, True), # 1lx resolution. - CONTINUOUS_HIGH_RES_MODE_2: (0x11, True), # 0.5lx resolution. - ONE_TIME_LOW_RES_MODE: (0x23, False), # 4lx resolution. - ONE_TIME_HIGH_RES_MODE_1: (0x20, False), # 1lx resolution. - ONE_TIME_HIGH_RES_MODE_2: (0x21, False), # 0.5lx resolution. -} - -DEFAULT_NAME = "BH1750 Light Sensor" -DEFAULT_I2C_ADDRESS = "0x23" -DEFAULT_I2C_BUS = 1 -DEFAULT_MODE = CONTINUOUS_HIGH_RES_MODE_1 -DEFAULT_DELAY_MS = 120 -DEFAULT_SENSITIVITY = 69 # from 31 to 254 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.string, - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - vol.Optional(CONF_OPERATION_MODE, default=DEFAULT_MODE): vol.In( - OPERATION_MODES - ), - vol.Optional(CONF_SENSITIVITY, default=DEFAULT_SENSITIVITY): cv.positive_int, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY_MS): cv.positive_int, - vol.Optional(CONF_MULTIPLIER, default=1.0): vol.Range(min=0.1, max=10), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BH1750 sensor.""" - _LOGGER.warning( - "The BH1750 integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config[CONF_NAME] - bus_number = config[CONF_I2C_BUS] - i2c_address = config[CONF_I2C_ADDRESS] - operation_mode = config[CONF_OPERATION_MODE] - - bus = smbus.SMBus(bus_number) - - sensor = await hass.async_add_executor_job( - partial( - BH1750, - bus, - i2c_address, - operation_mode=operation_mode, - measurement_delay=config[CONF_DELAY], - sensitivity=config[CONF_SENSITIVITY], - logger=_LOGGER, - ) - ) - if not sensor.sample_ok: - _LOGGER.error("BH1750 sensor not detected at %s", i2c_address) - return - - dev = [BH1750Sensor(sensor, name, LIGHT_LUX, config[CONF_MULTIPLIER])] - _LOGGER.info( - "Setup of BH1750 light sensor at %s in mode %s is complete", - i2c_address, - operation_mode, - ) - - async_add_entities(dev, True) - - -class BH1750Sensor(SensorEntity): - """Implementation of the BH1750 sensor.""" - - _attr_device_class = SensorDeviceClass.ILLUMINANCE - - def __init__(self, bh1750_sensor, name, unit, multiplier=1.0): - """Initialize the sensor.""" - self._attr_name = name - self._attr_native_unit_of_measurement = unit - self._multiplier = multiplier - self.bh1750_sensor = bh1750_sensor - - async def async_update(self): - """Get the latest data from the BH1750 and update the states.""" - await self.hass.async_add_executor_job(self.bh1750_sensor.update) - if self.bh1750_sensor.sample_ok and self.bh1750_sensor.light_level >= 0: - self._attr_native_value = int( - round(self.bh1750_sensor.light_level * self._multiplier) - ) - else: - _LOGGER.warning( - "Bad Update of sensor.%s: %s", self.name, self.bh1750_sensor.light_level - ) diff --git a/requirements_all.txt b/requirements_all.txt index f5def50096e..966866f3403 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -867,7 +867,6 @@ hydrawiser==0.2 # homeassistant.components.hyperion hyperion-py==0.7.4 -# homeassistant.components.bh1750 # homeassistant.components.bme280 # homeassistant.components.htu21d # i2csense==0.0.4 @@ -2214,7 +2213,6 @@ smart-meter-texas==0.4.7 # homeassistant.components.smarthab smarthab==0.21 -# homeassistant.components.bh1750 # homeassistant.components.bme280 # homeassistant.components.bme680 # homeassistant.components.envirophat From 2af5b2c84562584075ec7276ec6bf860ac15bb2b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:25:38 +0100 Subject: [PATCH 0025/1054] Fix return type for entity update methods (#67184) --- homeassistant/components/dunehd/media_player.py | 3 +-- homeassistant/components/fireservicerota/switch.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 437b5b66a99..252cf82df42 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -93,11 +93,10 @@ class DuneHDPlayerEntity(MediaPlayerEntity): self._state: dict[str, Any] = {} self._unique_id = unique_id - def update(self) -> bool: + def update(self) -> None: """Update internal status of the entity.""" self._state = self._player.update_state() self.__update_title() - return True @property def state(self) -> str | None: diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py index e625ac5deb5..48ec1a77c54 100644 --- a/homeassistant/components/fireservicerota/switch.py +++ b/homeassistant/components/fireservicerota/switch.py @@ -136,16 +136,15 @@ class ResponseSwitch(SwitchEntity): """Handle updated incident data from the client.""" self.async_schedule_update_ha_state(True) - async def async_update(self) -> bool: + async def async_update(self) -> None: """Update FireServiceRota response data.""" data = await self._client.async_response_update() if not data or "status" not in data: - return False + return self._state = data["status"] == "acknowledged" self._state_attributes = data self._state_icon = data["status"] _LOGGER.debug("Set state of entity 'Response Switch' to '%s'", self._state) - return True From dd927adba9c8df15b5b2a4ca322ecea4b8576dde Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 18:26:32 +0100 Subject: [PATCH 0026/1054] Remove deprecated Blinkt! integration (#67165) --- .coveragerc | 1 - .github/workflows/wheels.yml | 1 - homeassistant/components/blinkt/__init__.py | 1 - homeassistant/components/blinkt/light.py | 103 ------------------ homeassistant/components/blinkt/manifest.json | 8 -- requirements_all.txt | 3 - script/gen_requirements_all.py | 1 - 7 files changed, 118 deletions(-) delete mode 100644 homeassistant/components/blinkt/__init__.py delete mode 100644 homeassistant/components/blinkt/light.py delete mode 100644 homeassistant/components/blinkt/manifest.json diff --git a/.coveragerc b/.coveragerc index 78a07b8d916..4c2564a2425 100644 --- a/.coveragerc +++ b/.coveragerc @@ -112,7 +112,6 @@ omit = homeassistant/components/blink/const.py homeassistant/components/blink/sensor.py homeassistant/components/blinksticklight/light.py - homeassistant/components/blinkt/light.py homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* homeassistant/components/bluesound/* diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index a0d6396ec30..30f8e0bdac9 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -137,7 +137,6 @@ jobs: sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} sed -i "s|# raspihats|raspihats|g" ${requirement_file} sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} - sed -i "s|# blinkt|blinkt|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} diff --git a/homeassistant/components/blinkt/__init__.py b/homeassistant/components/blinkt/__init__.py deleted file mode 100644 index 0f61a211559..00000000000 --- a/homeassistant/components/blinkt/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The blinkt component.""" diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py deleted file mode 100644 index 5b720e13697..00000000000 --- a/homeassistant/components/blinkt/light.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Support for Blinkt! lights on Raspberry Pi.""" -from __future__ import annotations - -import importlib -import logging - -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - LightEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -SUPPORT_BLINKT = SUPPORT_BRIGHTNESS | SUPPORT_COLOR - -DEFAULT_NAME = "blinkt" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Blinkt Light platform.""" - _LOGGER.warning( - "The Blinkt! integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - blinkt = importlib.import_module("blinkt") - - # ensure that the lights are off when exiting - blinkt.set_clear_on_exit() - - name = config[CONF_NAME] - - add_entities( - [BlinktLight(blinkt, name, index) for index in range(blinkt.NUM_PIXELS)] - ) - - -class BlinktLight(LightEntity): - """Representation of a Blinkt! Light.""" - - _attr_supported_features = SUPPORT_BLINKT - _attr_should_poll = False - _attr_assumed_state = True - - def __init__(self, blinkt, name, index): - """Initialize a Blinkt Light. - - Default brightness and white color. - """ - self._blinkt = blinkt - self._attr_name = f"{name}_{index}" - self._index = index - self._attr_is_on = False - self._attr_brightness = 255 - self._attr_hs_color = [0, 0] - - def turn_on(self, **kwargs): - """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_HS_COLOR in kwargs: - self._attr_hs_color = kwargs[ATTR_HS_COLOR] - if ATTR_BRIGHTNESS in kwargs: - self._attr_brightness = kwargs[ATTR_BRIGHTNESS] - - percent_bright = self.brightness / 255 - rgb_color = color_util.color_hs_to_RGB(*self.hs_color) - self._blinkt.set_pixel( - self._index, rgb_color[0], rgb_color[1], rgb_color[2], percent_bright - ) - - self._blinkt.show() - - self._attr_is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - self._blinkt.set_pixel(self._index, 0, 0, 0, 0) - self._blinkt.show() - self._attr_is_on = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json deleted file mode 100644 index ac659f78e11..00000000000 --- a/homeassistant/components/blinkt/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "blinkt", - "name": "Blinkt!", - "documentation": "https://www.home-assistant.io/integrations/blinkt", - "requirements": ["blinkt==0.1.0"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/requirements_all.txt b/requirements_all.txt index 966866f3403..68de5e3830e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,9 +413,6 @@ blinkpy==0.18.0 # homeassistant.components.blinksticklight blinkstick==1.2.0 -# homeassistant.components.blinkt -# blinkt==0.1.0 - # homeassistant.components.bitcoin blockchain==1.4.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6ebea07ae4b..4ffca14b825 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -18,7 +18,6 @@ COMMENT_REQUIREMENTS = ( "avion", "beacontools", "beewi_smartclim", # depends on bluepy - "blinkt", "bluepy", "bme280spi", "bme680", From 4dc6aab17ee783cafcdfde94d4e8a54a6a2d3fe3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Feb 2022 09:03:59 -1000 Subject: [PATCH 0027/1054] Move camera to after deps for HomeKit (#67190) --- homeassistant/components/homekit/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 9981b3a1109..bde540d6372 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -8,8 +8,8 @@ "PyQRCode==1.2.1", "base36==0.1.1" ], - "dependencies": ["http", "camera", "ffmpeg", "network"], - "after_dependencies": ["zeroconf"], + "dependencies": ["ffmpeg", "http", "network"], + "after_dependencies": ["camera", "zeroconf"], "codeowners": ["@bdraco"], "zeroconf": ["_homekit._tcp.local."], "config_flow": true, From 00c6e30988f0a94166f96e70238bbaf9bbfede0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Feb 2022 09:07:17 -1000 Subject: [PATCH 0028/1054] Fix ElkM1 systems that do not use password authentication (#67194) --- homeassistant/components/elkm1/__init__.py | 23 +++++++--- homeassistant/components/elkm1/config_flow.py | 24 ++++++++-- homeassistant/components/elkm1/const.py | 2 +- homeassistant/components/elkm1/discovery.py | 2 + tests/components/elkm1/test_config_flow.py | 44 +++++++++++++++++++ 5 files changed, 86 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 8b0dd26fc32..04a26f2822b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -228,7 +228,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Setting up elkm1 %s", conf["host"]) - if not entry.unique_id or ":" not in entry.unique_id and is_ip_address(host): + if (not entry.unique_id or ":" not in entry.unique_id) and is_ip_address(host): + _LOGGER.debug( + "Unique id for %s is missing during setup, trying to fill from discovery", + host, + ) if device := await async_discover_device(hass, host): async_update_entry_from_discovery(hass, entry, device) @@ -276,7 +280,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, conf[CONF_HOST] + elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME]) ): return False except asyncio.TimeoutError as exc: @@ -327,7 +331,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_wait_for_elk_to_sync( - elk: elkm1.Elk, login_timeout: int, sync_timeout: int, conf_host: str + elk: elkm1.Elk, + login_timeout: int, + sync_timeout: int, + password_auth: bool, ) -> bool: """Wait until the elk has finished sync. Can fail login or timeout.""" @@ -353,15 +360,21 @@ async def async_wait_for_elk_to_sync( success = True elk.add_handler("login", login_status) elk.add_handler("sync_complete", sync_complete) - events = ((login_event, login_timeout), (sync_event, sync_timeout)) + events = [] + if password_auth: + events.append(("login", login_event, login_timeout)) + events.append(("sync_complete", sync_event, sync_timeout)) - for event, timeout in events: + for name, event, timeout in events: + _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) try: async with async_timeout.timeout(timeout): await event.wait() except asyncio.TimeoutError: + _LOGGER.debug("Timed out waiting for %s event", name) elk.disconnect() raise + _LOGGER.debug("Received %s event", name) return success diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 2453958b3de..a21cf186005 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify +from homeassistant.util.network import is_ip_address from . import async_wait_for_elk_to_sync from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT @@ -80,7 +81,9 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, url): + if not await async_wait_for_elk_to_sync( + elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid) + ): raise InvalidAuth short_mac = _short_mac(mac) if mac else None @@ -124,6 +127,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device = ElkSystem( discovery_info.macaddress, discovery_info.ip, 0 ) + _LOGGER.debug("Elk discovered from dhcp: %s", self._discovered_device) return await self._async_handle_discovery() async def async_step_integration_discovery( @@ -135,6 +139,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info["ip_address"], discovery_info["port"], ) + _LOGGER.debug( + "Elk discovered from integration discovery: %s", self._discovered_device + ) return await self._async_handle_discovery() async def _async_handle_discovery(self) -> FlowResult: @@ -304,11 +311,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Handle import.""" - if device := await async_discover_device( - self.hass, urlparse(user_input[CONF_HOST]).hostname + _LOGGER.debug("Elk is importing from yaml") + url = _make_url_from_data(user_input) + + if self._url_already_configured(url): + return self.async_abort(reason="address_already_configured") + + host = urlparse(url).hostname + _LOGGER.debug( + "Importing is trying to fill unique id from discovery for %s", host + ) + if is_ip_address(host) and ( + device := await async_discover_device(self.hass, host) ): await self.async_set_unique_id(dr.format_mac(device.mac_address)) self._abort_if_unique_id_configured() + return (await self._async_create_or_error(user_input, True))[1] def _url_already_configured(self, url): diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 80d594fce0a..fd4856bd5d5 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_CODE, CONF_ZONE DOMAIN = "elkm1" -LOGIN_TIMEOUT = 15 +LOGIN_TIMEOUT = 20 CONF_AUTO_CONFIGURE = "auto_configure" CONF_AREA = "area" diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 7055f3958e9..326698c3686 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -29,9 +29,11 @@ def async_update_entry_from_discovery( ) -> bool: """Update a config entry from a discovery.""" if not entry.unique_id or ":" not in entry.unique_id: + _LOGGER.debug("Adding unique id from discovery: %s", device) return hass.config_entries.async_update_entry( entry, unique_id=dr.format_mac(device.mac_address) ) + _LOGGER.debug("Unique id is already present from discovery: %s", device) return False diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index d8a0feea670..49402d7b4d5 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -652,6 +652,50 @@ async def test_form_import_device_discovered(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_import_existing(hass): + """Test we abort on existing import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": f"elks://{MOCK_IP_ADDRESS}", + "username": "friend", + "password": "love", + "temperature_unit": "C", + "auto_configure": False, + "keypad": { + "enabled": True, + "exclude": [], + "include": [[1, 1], [2, 2], [3, 3]], + }, + "output": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "zone": { + "enabled": True, + "exclude": [[15, 15], [28, 208]], + "include": [], + }, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "address_already_configured" + + @pytest.mark.parametrize( "source, data", [ From ba6493d66f32cb5c423851154cfc625c054d2f3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 22:46:59 +0100 Subject: [PATCH 0029/1054] Remove deprecated Orange Pi GPIO integration (#67177) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/orangepi_gpio/__init__.py | 59 -------------- .../components/orangepi_gpio/binary_sensor.py | 77 ------------------- .../components/orangepi_gpio/const.py | 59 -------------- .../components/orangepi_gpio/manifest.json | 9 --- requirements_all.txt | 3 - 7 files changed, 209 deletions(-) delete mode 100644 homeassistant/components/orangepi_gpio/__init__.py delete mode 100644 homeassistant/components/orangepi_gpio/binary_sensor.py delete mode 100644 homeassistant/components/orangepi_gpio/const.py delete mode 100644 homeassistant/components/orangepi_gpio/manifest.json diff --git a/.coveragerc b/.coveragerc index 4c2564a2425..a36c9579e1d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -859,7 +859,6 @@ omit = homeassistant/components/openweathermap/weather_update_coordinator.py homeassistant/components/opnsense/* homeassistant/components/opple/light.py - homeassistant/components/orangepi_gpio/* homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py diff --git a/CODEOWNERS b/CODEOWNERS index 283bd6442b1..643c287e04a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -689,7 +689,6 @@ homeassistant/components/openweathermap/* @fabaff @freekode @nzapponi tests/components/openweathermap/* @fabaff @freekode @nzapponi homeassistant/components/opnsense/* @mtreinish tests/components/opnsense/* @mtreinish -homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/oru/* @bvlaicu homeassistant/components/overkiz/* @imicknl @vlebourl @tetienne tests/components/overkiz/* @imicknl @vlebourl @tetienne diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py deleted file mode 100644 index 1b0d7a46648..00000000000 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Support for controlling GPIO pins of a Orange Pi.""" -import logging - -from OPi import GPIO - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -from .const import PIN_MODES - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "orangepi_gpio" - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Orange Pi GPIO component.""" - _LOGGER.warning( - "The Orange Pi GPIO integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - GPIO.cleanup() - - def prepare_gpio(event): - """Stuff to do when Home Assistant starts.""" - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - return True - - -def setup_mode(mode): - """Set GPIO pin mode.""" - _LOGGER.debug("Setting GPIO pin mode as %s", PIN_MODES[mode]) - GPIO.setmode(PIN_MODES[mode]) - - -def setup_input(port): - """Set up a GPIO as input.""" - _LOGGER.debug("Setting up GPIO pin %i as input", port) - GPIO.setup(port, GPIO.IN) - - -def read_input(port): - """Read a value from a GPIO.""" - _LOGGER.debug("Reading GPIO pin %i", port) - return GPIO.input(port) - - -def edge_detect(port, event_callback): - """Add detection for RISING and FALLING events.""" - _LOGGER.debug("Add callback for GPIO pin %i", port) - GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback) diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py deleted file mode 100644 index 0be4ec48394..00000000000 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Support for binary sensor using Orange Pi GPIO.""" -from __future__ import annotations - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import edge_detect, read_input, setup_input, setup_mode -from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Orange Pi GPIO platform.""" - binary_sensors = [] - invert_logic = config[CONF_INVERT_LOGIC] - pin_mode = config[CONF_PIN_MODE] - ports = config[CONF_PORTS] - - setup_mode(pin_mode) - - for port_num, port_name in ports.items(): - binary_sensors.append( - OPiGPIOBinarySensor(hass, port_name, port_num, invert_logic) - ) - async_add_entities(binary_sensors) - - -class OPiGPIOBinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses Orange Pi GPIO.""" - - def __init__(self, hass, name, port, invert_logic): - """Initialize the Orange Pi binary sensor.""" - self._name = name - self._port = port - self._invert_logic = invert_logic - self._state = None - - async def async_added_to_hass(self): - """Run when entity about to be added to hass.""" - - def gpio_edge_listener(port): - """Update GPIO when edge change is detected.""" - self.schedule_update_ha_state(True) - - def setup_entity(): - setup_input(self._port) - edge_detect(self._port, gpio_edge_listener) - self.schedule_update_ha_state(True) - - await self.hass.async_add_executor_job(setup_entity) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update state with new GPIO data.""" - self._state = read_input(self._port) diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py deleted file mode 100644 index f663fbc1ef4..00000000000 --- a/homeassistant/components/orangepi_gpio/const.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Constants for Orange Pi GPIO.""" - -from nanopi import duo, neocore2 -from orangepi import ( - lite, - lite2, - one, - oneplus, - pc, - pc2, - pcplus, - pi3, - pi4, - pi4B, - plus2e, - prime, - r1, - winplus, - zero, - zeroplus, - zeroplus2, -) -import voluptuous as vol - -from homeassistant.helpers import config_validation as cv - -CONF_INVERT_LOGIC = "invert_logic" -CONF_PIN_MODE = "pin_mode" -CONF_PORTS = "ports" -DEFAULT_INVERT_LOGIC = False -PIN_MODES = { - "duo": duo.BOARD, - "lite": lite.BOARD, - "lite2": lite2.BOARD, - "neocore2": neocore2.BOARD, - "one": one.BOARD, - "oneplus": oneplus.BOARD, - "pc": pc.BOARD, - "pc2": pc2.BOARD, - "pcplus": pcplus.BOARD, - "pi3": pi3.BOARD, - "pi4": pi4.BOARD, - "pi4B": pi4B.BOARD, - "plus2e": plus2e.BOARD, - "prime": prime.BOARD, - "r1": r1.BOARD, - "winplus": winplus.BOARD, - "zero": zero.BOARD, - "zeroplus": zeroplus.BOARD, - "zeroplus2": zeroplus2.BOARD, -} - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PORT_SCHEMA = { - vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES.keys()), - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, -} diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json deleted file mode 100644 index b4cda33ee80..00000000000 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "orangepi_gpio", - "name": "Orange Pi GPIO", - "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", - "requirements": ["OPi.GPIO==0.5.2"], - "codeowners": ["@pascallj"], - "iot_class": "local_push", - "loggers": ["OPi", "nanopi", "orangepi"] -} diff --git a/requirements_all.txt b/requirements_all.txt index 68de5e3830e..1662428acef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -19,9 +19,6 @@ HAP-python==4.4.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 -# homeassistant.components.orangepi_gpio -OPi.GPIO==0.5.2 - # homeassistant.components.flick_electric PyFlick==0.0.2 From 5eec425e2c54309ad4d385361769dace324aa8ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 22:50:17 +0100 Subject: [PATCH 0030/1054] Remove deprecated MH-Z19 CO2 Sensor integration (#67186) --- homeassistant/components/mhz19/__init__.py | 1 - homeassistant/components/mhz19/manifest.json | 9 - homeassistant/components/mhz19/sensor.py | 169 ------------------- requirements_all.txt | 1 - requirements_test_all.txt | 4 - tests/components/mhz19/__init__.py | 1 - tests/components/mhz19/test_sensor.py | 125 -------------- 7 files changed, 310 deletions(-) delete mode 100644 homeassistant/components/mhz19/__init__.py delete mode 100644 homeassistant/components/mhz19/manifest.json delete mode 100644 homeassistant/components/mhz19/sensor.py delete mode 100644 tests/components/mhz19/__init__.py delete mode 100644 tests/components/mhz19/test_sensor.py diff --git a/homeassistant/components/mhz19/__init__.py b/homeassistant/components/mhz19/__init__.py deleted file mode 100644 index 5fa9bbb69e8..00000000000 --- a/homeassistant/components/mhz19/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The mhz19 component.""" diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json deleted file mode 100644 index 349fba8c7a2..00000000000 --- a/homeassistant/components/mhz19/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "mhz19", - "name": "MH-Z19 CO2 Sensor", - "documentation": "https://www.home-assistant.io/integrations/mhz19", - "requirements": ["pmsensor==0.4"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["pmsensor"] -} diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py deleted file mode 100644 index f9237fd2c5b..00000000000 --- a/homeassistant/components/mhz19/sensor.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Support for CO2 sensor connected to a serial port.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -from pmsensor import co2sensor -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONCENTRATION_PARTS_PER_MILLION, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_SERIAL_DEVICE = "serial_device" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - -DEFAULT_NAME = "CO2 Sensor" - -ATTR_CO2_CONCENTRATION = "co2_concentration" - -SENSOR_TEMPERATURE = "temperature" -SENSOR_CO2 = "co2" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMPERATURE, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_CO2, - name="CO2", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO2, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_SERIAL_DEVICE): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_CO2]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the available CO2 sensors.""" - _LOGGER.warning( - "The MH-Z19 CO2 Sensor integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - try: - co2sensor.read_mh_z19(config.get(CONF_SERIAL_DEVICE)) - except OSError as err: - _LOGGER.error( - "Could not open serial connection to %s (%s)", - config.get(CONF_SERIAL_DEVICE), - err, - ) - return - - data = MHZClient(co2sensor, config.get(CONF_SERIAL_DEVICE)) - name = config[CONF_NAME] - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MHZ19Sensor(data, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities, True) - - -class MHZ19Sensor(SensorEntity): - """Representation of an CO2 sensor.""" - - def __init__(self, mhz_client, name, description: SensorEntityDescription): - """Initialize a new PM sensor.""" - self.entity_description = description - self._mhz_client = mhz_client - self._ppm = None - self._temperature = None - - self._attr_name = f"{name}: {description.name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - if self.entity_description.key == SENSOR_CO2: - return self._ppm - return self._temperature - - def update(self): - """Read from sensor and update the state.""" - self._mhz_client.update() - data = self._mhz_client.data - self._temperature = data.get(SENSOR_TEMPERATURE) - self._ppm = data.get(SENSOR_CO2) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - result = {} - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMPERATURE and self._ppm is not None: - result[ATTR_CO2_CONCENTRATION] = self._ppm - elif sensor_type == SENSOR_CO2 and self._temperature is not None: - result[ATTR_TEMPERATURE] = self._temperature - return result - - -class MHZClient: - """Get the latest data from the MH-Z sensor.""" - - def __init__(self, co2sens, serial): - """Initialize the sensor.""" - self.co2sensor = co2sens - self._serial = serial - self.data = {} - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data the MH-Z19 sensor.""" - self.data = {} - try: - result = self.co2sensor.read_mh_z19_with_temperature(self._serial) - if result is None: - return - co2, temperature = result - - except OSError as err: - _LOGGER.error( - "Could not open serial connection to %s (%s)", self._serial, err - ) - return - - if temperature is not None: - self.data[SENSOR_TEMPERATURE] = temperature - if co2 is not None and 0 < co2 <= 5000: - self.data[SENSOR_CO2] = co2 diff --git a/requirements_all.txt b/requirements_all.txt index 1662428acef..8ec3e428c66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1260,7 +1260,6 @@ plugwise==0.16.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 -# homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93dacff8619..ca17fbd8889 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -801,10 +801,6 @@ plugwise==0.16.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 -# homeassistant.components.mhz19 -# homeassistant.components.serial_pm -pmsensor==0.4 - # homeassistant.components.poolsense poolsense==0.0.8 diff --git a/tests/components/mhz19/__init__.py b/tests/components/mhz19/__init__.py deleted file mode 100644 index a35660a3726..00000000000 --- a/tests/components/mhz19/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the mhz19 component.""" diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py deleted file mode 100644 index fd494d6c099..00000000000 --- a/tests/components/mhz19/test_sensor.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Tests for MH-Z19 sensor.""" -from unittest.mock import DEFAULT, Mock, patch - -from pmsensor import co2sensor -from pmsensor.co2sensor import read_mh_z19_with_temperature - -import homeassistant.components.mhz19.sensor as mhz19 -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component - - -async def test_setup_missing_config(hass): - """Test setup with configuration missing required entries.""" - with assert_setup_component(0): - assert await async_setup_component( - hass, DOMAIN, {"sensor": {"platform": "mhz19"}} - ) - - -@patch("pmsensor.co2sensor.read_mh_z19", side_effect=OSError("test error")) -async def test_setup_failed_connect(mock_co2, hass): - """Test setup when connection error occurs.""" - assert not mhz19.setup_platform( - hass, - {"platform": "mhz19", mhz19.CONF_SERIAL_DEVICE: "test.serial"}, - None, - ) - - -async def test_setup_connected(hass): - """Test setup when connection succeeds.""" - with patch.multiple( - "pmsensor.co2sensor", - read_mh_z19=DEFAULT, - read_mh_z19_with_temperature=DEFAULT, - ): - read_mh_z19_with_temperature.return_value = None - mock_add = Mock() - mhz19.setup_platform( - hass, - { - "platform": "mhz19", - "name": "name", - "monitored_conditions": ["co2", "temperature"], - mhz19.CONF_SERIAL_DEVICE: "test.serial", - }, - mock_add, - ) - assert mock_add.call_count == 1 - - -@patch( - "pmsensor.co2sensor.read_mh_z19_with_temperature", - side_effect=OSError("test error"), -) -async def aiohttp_client_update_oserror(mock_function): - """Test MHZClient when library throws OSError.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - client.update() - assert {} == client.data - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(5001, 24)) -async def aiohttp_client_update_ppm_overflow(mock_function): - """Test MHZClient when ppm is too high.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - client.update() - assert client.data.get("co2") is None - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def aiohttp_client_update_good_read(mock_function): - """Test MHZClient when ppm is too high.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - client.update() - assert {"temperature": 24, "co2": 1000} == client.data - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_co2_sensor(mock_function, hass): - """Test CO2 sensor.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - sensor = mhz19.MHZ19Sensor(client, "name", mhz19.SENSOR_TYPES[1]) - sensor.hass = hass - sensor.update() - - assert sensor.name == "name: CO2" - assert sensor.state == 1000 - assert sensor.native_unit_of_measurement == CONCENTRATION_PARTS_PER_MILLION - assert sensor.should_poll - assert sensor.extra_state_attributes == {"temperature": 24} - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor(mock_function, hass): - """Test temperature sensor.""" - client = mhz19.MHZClient(co2sensor, "test.serial") - sensor = mhz19.MHZ19Sensor(client, "name", mhz19.SENSOR_TYPES[0]) - sensor.hass = hass - sensor.update() - - assert sensor.name == "name: Temperature" - assert sensor.state == 24 - assert sensor.native_unit_of_measurement == TEMP_CELSIUS - assert sensor.should_poll - assert sensor.extra_state_attributes == {"co2_concentration": 1000} - - -@patch("pmsensor.co2sensor.read_mh_z19_with_temperature", return_value=(1000, 24)) -async def test_temperature_sensor_f(mock_function, hass): - """Test temperature sensor.""" - with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): - client = mhz19.MHZClient(co2sensor, "test.serial") - sensor = mhz19.MHZ19Sensor(client, "name", mhz19.SENSOR_TYPES[0]) - sensor.hass = hass - sensor.update() - - assert sensor.state == 75 From ae073d132c613d77defe253dde930f6e2d82ab7c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Feb 2022 22:54:01 +0100 Subject: [PATCH 0031/1054] Remove deprecated Bosch BME280 Environmental Sensor integration (#67185) --- .coveragerc | 3 - homeassistant/components/bme280/__init__.py | 112 ------------- homeassistant/components/bme280/const.py | 60 ------- homeassistant/components/bme280/manifest.json | 13 -- homeassistant/components/bme280/sensor.py | 149 ------------------ requirements_all.txt | 5 - script/gen_requirements_all.py | 1 - 7 files changed, 343 deletions(-) delete mode 100644 homeassistant/components/bme280/__init__.py delete mode 100644 homeassistant/components/bme280/const.py delete mode 100644 homeassistant/components/bme280/manifest.json delete mode 100644 homeassistant/components/bme280/sensor.py diff --git a/.coveragerc b/.coveragerc index a36c9579e1d..e378f0291cc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -116,9 +116,6 @@ omit = homeassistant/components/bloomsky/* homeassistant/components/bluesound/* homeassistant/components/bluetooth_tracker/* - homeassistant/components/bme280/__init__.py - homeassistant/components/bme280/const.py - homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmp280/sensor.py homeassistant/components/bmw_connected_drive/__init__.py diff --git a/homeassistant/components/bme280/__init__.py b/homeassistant/components/bme280/__init__.py deleted file mode 100644 index 59ad42da944..00000000000 --- a/homeassistant/components/bme280/__init__.py +++ /dev/null @@ -1,112 +0,0 @@ -"""The bme280 component.""" -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.typing import ConfigType - -from .const import ( - CONF_DELTA_TEMP, - CONF_FILTER_MODE, - CONF_I2C_ADDRESS, - CONF_I2C_BUS, - CONF_OPERATION_MODE, - CONF_OVERSAMPLING_HUM, - CONF_OVERSAMPLING_PRES, - CONF_OVERSAMPLING_TEMP, - CONF_SPI_BUS, - CONF_SPI_DEV, - CONF_T_STANDBY, - DEFAULT_DELTA_TEMP, - DEFAULT_FILTER_MODE, - DEFAULT_I2C_ADDRESS, - DEFAULT_I2C_BUS, - DEFAULT_MONITORED, - DEFAULT_NAME, - DEFAULT_OPERATION_MODE, - DEFAULT_OVERSAMPLING_HUM, - DEFAULT_OVERSAMPLING_PRES, - DEFAULT_OVERSAMPLING_TEMP, - DEFAULT_SCAN_INTERVAL, - DEFAULT_T_STANDBY, - DOMAIN, - SENSOR_KEYS, -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SPI_BUS): vol.Coerce(int), - vol.Optional(CONF_SPI_DEV): vol.Coerce(int), - vol.Optional( - CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS - ): cv.string, - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce( - int - ), - vol.Optional( - CONF_DELTA_TEMP, default=DEFAULT_DELTA_TEMP - ): vol.Coerce(float), - vol.Optional( - CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED - ): vol.All(cv.ensure_list, [vol.In(SENSOR_KEYS)]), - vol.Optional( - CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP - ): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES - ): vol.Coerce(int), - vol.Optional( - CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM - ): vol.Coerce(int), - vol.Optional( - CONF_OPERATION_MODE, default=DEFAULT_OPERATION_MODE - ): vol.Coerce(int), - vol.Optional( - CONF_T_STANDBY, default=DEFAULT_T_STANDBY - ): vol.Coerce(int), - vol.Optional( - CONF_FILTER_MODE, default=DEFAULT_FILTER_MODE - ): vol.Coerce(int), - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up BME280 component.""" - _LOGGER.warning( - "The Bosch BME280 Environmental Sensor integration is deprecated and " - "will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0019, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - bme280_config = config[DOMAIN] - for bme280_conf in bme280_config: - discovery_info = {SENSOR_DOMAIN: bme280_conf} - hass.async_create_task( - discovery.async_load_platform( - hass, SENSOR_DOMAIN, DOMAIN, discovery_info, config - ) - ) - return True diff --git a/homeassistant/components/bme280/const.py b/homeassistant/components/bme280/const.py deleted file mode 100644 index 1bb0828dd1e..00000000000 --- a/homeassistant/components/bme280/const.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Constants for the BME280 component.""" -from __future__ import annotations - -from datetime import timedelta - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS - -# Common -DOMAIN = "bme280" -CONF_OVERSAMPLING_TEMP = "oversampling_temperature" -CONF_OVERSAMPLING_PRES = "oversampling_pressure" -CONF_OVERSAMPLING_HUM = "oversampling_humidity" -CONF_T_STANDBY = "time_standby" -CONF_FILTER_MODE = "filter_mode" -DEFAULT_NAME = "BME280 Sensor" -DEFAULT_OVERSAMPLING_TEMP = 1 -DEFAULT_OVERSAMPLING_PRES = 1 -DEFAULT_OVERSAMPLING_HUM = 1 -DEFAULT_T_STANDBY = 5 -DEFAULT_FILTER_MODE = 0 -DEFAULT_SCAN_INTERVAL = 300 -SENSOR_TEMP = "temperature" -SENSOR_HUMID = "humidity" -SENSOR_PRESS = "pressure" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMP, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_HUMID, - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), - SensorEntityDescription( - key=SENSOR_PRESS, - name="Pressure", - native_unit_of_measurement="mb", - device_class=SensorDeviceClass.PRESSURE, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) -# SPI -CONF_SPI_DEV = "spi_dev" -CONF_SPI_BUS = "spi_bus" -# I2C -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_DELTA_TEMP = "delta_temperature" -CONF_OPERATION_MODE = "operation_mode" -DEFAULT_OPERATION_MODE = 3 # Normal mode (forced mode: 2) -DEFAULT_I2C_ADDRESS = "0x76" -DEFAULT_I2C_BUS = 1 -DEFAULT_DELTA_TEMP = 0.0 diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json deleted file mode 100644 index 8a283b40f5f..00000000000 --- a/homeassistant/components/bme280/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "bme280", - "name": "Bosch BME280 Environmental Sensor", - "documentation": "https://www.home-assistant.io/integrations/bme280", - "requirements": [ - "i2csense==0.0.4", - "smbus-cffi==0.5.1", - "bme280spi==0.2.0" - ], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["bme280spi", "i2csense", "smbus"] -} diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py deleted file mode 100644 index 1ceae298e25..00000000000 --- a/homeassistant/components/bme280/sensor.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Support for BME280 temperature, humidity and pressure sensor.""" -from __future__ import annotations - -from functools import partial -import logging - -from bme280spi import BME280 as BME280_spi # pylint: disable=import-error -from i2csense.bme280 import BME280 as BME280_i2c # pylint: disable=import-error -import smbus - -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import ( - CONF_DELTA_TEMP, - CONF_FILTER_MODE, - CONF_I2C_ADDRESS, - CONF_I2C_BUS, - CONF_OPERATION_MODE, - CONF_OVERSAMPLING_HUM, - CONF_OVERSAMPLING_PRES, - CONF_OVERSAMPLING_TEMP, - CONF_SPI_BUS, - CONF_SPI_DEV, - CONF_T_STANDBY, - DOMAIN, - MIN_TIME_BETWEEN_UPDATES, - SENSOR_HUMID, - SENSOR_PRESS, - SENSOR_TEMP, - SENSOR_TYPES, -) - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BME280 sensor.""" - if discovery_info is None: - return - sensor_conf = discovery_info[SENSOR_DOMAIN] - name = sensor_conf[CONF_NAME] - scan_interval = max(sensor_conf[CONF_SCAN_INTERVAL], MIN_TIME_BETWEEN_UPDATES) - if CONF_SPI_BUS in sensor_conf and CONF_SPI_DEV in sensor_conf: - spi_dev = sensor_conf[CONF_SPI_DEV] - spi_bus = sensor_conf[CONF_SPI_BUS] - _LOGGER.debug("BME280 sensor initialize at %s.%s", spi_bus, spi_dev) - sensor = await hass.async_add_executor_job( - partial( - BME280_spi, - t_mode=sensor_conf[CONF_OVERSAMPLING_TEMP], - p_mode=sensor_conf[CONF_OVERSAMPLING_PRES], - h_mode=sensor_conf[CONF_OVERSAMPLING_HUM], - standby=sensor_conf[CONF_T_STANDBY], - filter=sensor_conf[CONF_FILTER_MODE], - spi_bus=sensor_conf[CONF_SPI_BUS], - spi_dev=sensor_conf[CONF_SPI_DEV], - ) - ) - if not sensor.sample_ok: - _LOGGER.error("BME280 sensor not detected at %s.%s", spi_bus, spi_dev) - return - else: - i2c_address = sensor_conf[CONF_I2C_ADDRESS] - bus = smbus.SMBus(sensor_conf[CONF_I2C_BUS]) - sensor = await hass.async_add_executor_job( - partial( - BME280_i2c, - bus, - i2c_address, - osrs_t=sensor_conf[CONF_OVERSAMPLING_TEMP], - osrs_p=sensor_conf[CONF_OVERSAMPLING_PRES], - osrs_h=sensor_conf[CONF_OVERSAMPLING_HUM], - mode=sensor_conf[CONF_OPERATION_MODE], - t_sb=sensor_conf[CONF_T_STANDBY], - filter_mode=sensor_conf[CONF_FILTER_MODE], - delta_temp=sensor_conf[CONF_DELTA_TEMP], - ) - ) - if not sensor.sample_ok: - _LOGGER.error("BME280 sensor not detected at %s", i2c_address) - return - - async def async_update_data(): - await hass.async_add_executor_job(sensor.update) - if not sensor.sample_ok: - raise UpdateFailed(f"Bad update of sensor {name}") - return sensor - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=DOMAIN, - update_method=async_update_data, - update_interval=scan_interval, - ) - await coordinator.async_refresh() - monitored_conditions = sensor_conf[CONF_MONITORED_CONDITIONS] - entities = [ - BME280Sensor(name, coordinator, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - async_add_entities(entities, True) - - -class BME280Sensor(CoordinatorEntity, SensorEntity): - """Implementation of the BME280 sensor.""" - - def __init__(self, name, coordinator, description: SensorEntityDescription): - """Initialize the sensor.""" - super().__init__(coordinator) - self.entity_description = description - self._attr_name = f"{name} {description.name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMP: - temperature = round(self.coordinator.data.temperature, 1) - state = temperature - elif sensor_type == SENSOR_HUMID: - state = round(self.coordinator.data.humidity, 1) - elif sensor_type == SENSOR_PRESS: - state = round(self.coordinator.data.pressure, 1) - return state - - @property - def should_poll(self) -> bool: - """Return False if entity should not poll.""" - return False diff --git a/requirements_all.txt b/requirements_all.txt index 8ec3e428c66..9d87591110b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,9 +417,6 @@ blockchain==1.4.4 # homeassistant.components.miflora # bluepy==1.3.0 -# homeassistant.components.bme280 -# bme280spi==0.2.0 - # homeassistant.components.bme680 # bme680==1.0.5 @@ -861,7 +858,6 @@ hydrawiser==0.2 # homeassistant.components.hyperion hyperion-py==0.7.4 -# homeassistant.components.bme280 # homeassistant.components.htu21d # i2csense==0.0.4 @@ -2206,7 +2202,6 @@ smart-meter-texas==0.4.7 # homeassistant.components.smarthab smarthab==0.21 -# homeassistant.components.bme280 # homeassistant.components.bme680 # homeassistant.components.envirophat # homeassistant.components.htu21d diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4ffca14b825..c853fb8f678 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -19,7 +19,6 @@ COMMENT_REQUIREMENTS = ( "beacontools", "beewi_smartclim", # depends on bluepy "bluepy", - "bme280spi", "bme680", "decora", "decora_wifi", From d64777359de3780be6e362a230350334a541dcef Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 24 Feb 2022 23:21:36 +0100 Subject: [PATCH 0032/1054] Set zwave_js siren to unknown state if value is None (#67172) --- homeassistant/components/zwave_js/siren.py | 4 ++- tests/components/zwave_js/test_siren.py | 39 +++++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index 4ef89b9f4cd..a3fe3dfd595 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -68,8 +68,10 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): self._attr_supported_features |= SUPPORT_TONES @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return whether device is on.""" + if self.info.primary_value.value is None: + return None return bool(self.info.primary_value.value) async def async_set_value( diff --git a/tests/components/zwave_js/test_siren.py b/tests/components/zwave_js/test_siren.py index ebe437eb981..b1f7724316a 100644 --- a/tests/components/zwave_js/test_siren.py +++ b/tests/components/zwave_js/test_siren.py @@ -3,7 +3,7 @@ from zwave_js_server.event import Event from homeassistant.components.siren import ATTR_TONE, ATTR_VOLUME_LEVEL from homeassistant.components.siren.const import ATTR_AVAILABLE_TONES -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN SIREN_ENTITY = "siren.indoor_siren_6_2" @@ -65,7 +65,7 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): state = hass.states.get(SIREN_ENTITY) assert state - assert state.state == STATE_OFF + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_AVAILABLE_TONES) == { 0: "off", 1: "01DING~1 (5 sec)", @@ -117,6 +117,29 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): client.async_send_command.reset_mock() + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Sound Switch", + "commandClass": 121, + "endpoint": 2, + "property": "toneId", + "newValue": 255, + "prevValue": None, + "propertyName": "toneId", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(SIREN_ENTITY) + assert state.state == STATE_ON + # Test turn on with specific tone name and volume level await hass.services.async_call( "siren", @@ -133,7 +156,7 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == TONE_ID_VALUE_ID + assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} assert args["value"] == 1 assert args["options"] == {"volume": 50} @@ -155,7 +178,7 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == TONE_ID_VALUE_ID + assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} assert args["value"] == 1 assert args["options"] == {"volume": 50} @@ -173,7 +196,7 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == node.node_id - assert args["valueId"] == TONE_ID_VALUE_ID + assert args["valueId"] == {**TONE_ID_VALUE_ID, "value": 255} assert args["value"] == 0 client.async_send_command.reset_mock() @@ -190,8 +213,8 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): "commandClass": 121, "endpoint": 2, "property": "toneId", - "newValue": 255, - "prevValue": 0, + "newValue": 0, + "prevValue": 255, "propertyName": "toneId", }, }, @@ -199,4 +222,4 @@ async def test_siren(hass, client, aeotec_zw164_siren, integration): node.receive_event(event) state = hass.states.get(SIREN_ENTITY) - assert state.state == STATE_ON + assert state.state == STATE_OFF From 2a8dc38d2d9307bdcf1f254c15a97a6d6257489d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 14:54:46 -0800 Subject: [PATCH 0033/1054] Move media_source to after_deps (#67195) --- homeassistant/components/dlna_dms/manifest.json | 5 +++-- homeassistant/components/motioneye/manifest.json | 15 ++++----------- homeassistant/components/nest/manifest.json | 3 ++- tests/components/motioneye/test_media_source.py | 7 +++++++ tests/components/nest/test_media_source.py | 6 ++++++ 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index feee4b6e903..84cfc2e69fd 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -4,7 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "requirements": ["async-upnp-client==0.23.5"], - "dependencies": ["media_source", "ssdp"], + "dependencies": ["ssdp"], + "after_dependencies": ["media_source"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", @@ -26,4 +27,4 @@ "codeowners": ["@chishm"], "iot_class": "local_polling", "quality_scale": "platinum" -} \ No newline at end of file +} diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 0eb4dc57d9d..5c1dbb376a0 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -3,17 +3,10 @@ "name": "motionEye", "documentation": "https://www.home-assistant.io/integrations/motioneye", "config_flow": true, - "dependencies": [ - "http", - "media_source", - "webhook" - ], - "requirements": [ - "motioneye-client==0.3.12" - ], - "codeowners": [ - "@dermotduffy" - ], + "dependencies": ["http", "webhook"], + "after_dependencies": ["media_source"], + "requirements": ["motioneye-client==0.3.12"], + "codeowners": ["@dermotduffy"], "iot_class": "local_polling", "loggers": ["motioneye_client"] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 6e7ac1257fa..6968b401561 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,8 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "media_source"], + "dependencies": ["ffmpeg", "http"], + "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], "codeowners": ["@allenporter"], diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index ddc50fb7702..6c0e46b34c6 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -12,6 +12,7 @@ from homeassistant.components.media_source.models import PlayMedia from homeassistant.components.motioneye.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from . import ( TEST_CAMERA_DEVICE_IDENTIFIER, @@ -67,6 +68,12 @@ TEST_IMAGES = { _LOGGER = logging.getLogger(__name__) +@pytest.fixture(autouse=True) +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + async def test_async_browse_media_success(hass: HomeAssistant) -> None: """Test successful browse media.""" diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 015a14fb92c..2361049ecc1 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -90,6 +90,12 @@ def frame_image_data(frame_i, total_frames): return img +@pytest.fixture(autouse=True) +async def setup_media_source(hass) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + @pytest.fixture def mp4() -> io.BytesIO: """Generate test mp4 clip.""" From 5e366d93fccbe254d3f6ebe43b3d16ca35deb5e1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 25 Feb 2022 00:17:52 +0000 Subject: [PATCH 0034/1054] [ci skip] Translation update --- .../accuweather/translations/es.json | 2 +- .../binary_sensor/translations/he.json | 4 ++++ .../components/cast/translations/es.json | 2 +- .../components/dlna_dms/translations/bg.json | 12 ++++++++++ .../components/dlna_dms/translations/es.json | 12 ++++++++++ .../components/dlna_dms/translations/he.json | 20 ++++++++++++++++ .../components/dlna_dms/translations/it.json | 24 +++++++++++++++++++ .../components/fritz/translations/it.json | 3 ++- .../components/homekit/translations/es.json | 5 ++++ .../components/mysensors/translations/it.json | 4 ++-- .../mysensors/translations/zh-Hant.json | 4 ++-- .../components/nanoleaf/translations/de.json | 8 +++++++ .../components/nanoleaf/translations/et.json | 8 +++++++ .../components/nanoleaf/translations/it.json | 8 +++++++ .../nanoleaf/translations/pt-BR.json | 8 +++++++ .../components/nanoleaf/translations/ru.json | 8 +++++++ .../nanoleaf/translations/zh-Hant.json | 8 +++++++ .../number/translations/zh-Hant.json | 2 +- .../panasonic_viera/translations/es.json | 4 ++-- .../components/plex/translations/es.json | 2 +- .../radio_browser/translations/es.json | 7 ++++++ .../radio_browser/translations/he.json | 7 ++++++ .../remote/translations/zh-Hant.json | 2 +- .../components/sense/translations/es.json | 10 ++++++++ .../components/sense/translations/he.json | 9 ++++++- .../components/sense/translations/it.json | 16 ++++++++++++- .../components/sleepiq/translations/es.json | 3 +++ .../components/sonarr/translations/bg.json | 3 ++- .../components/sonarr/translations/es.json | 1 + .../components/sonarr/translations/he.json | 1 + .../components/sonarr/translations/it.json | 3 ++- .../tuya/translations/select.es.json | 10 +++++++- .../vacuum/translations/zh-Hant.json | 2 +- 33 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/dlna_dms/translations/bg.json create mode 100644 homeassistant/components/dlna_dms/translations/es.json create mode 100644 homeassistant/components/dlna_dms/translations/he.json create mode 100644 homeassistant/components/dlna_dms/translations/it.json create mode 100644 homeassistant/components/radio_browser/translations/es.json create mode 100644 homeassistant/components/radio_browser/translations/he.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index aa24b5ff975..9d0396fddb3 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -16,7 +16,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.", + "description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados por defecto. Los puedes habilitar en el registro de entidades despu\u00e9s de configurar la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.", "title": "AccuWeather" } } diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 3866d6a93d6..293a4f3f264 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -134,6 +134,10 @@ "off": "\u05dc\u05d0 \u05e0\u05d8\u05e2\u05df", "on": "\u05e0\u05d8\u05e2\u05df" }, + "carbon_monoxide": { + "off": "\u05e0\u05e7\u05d9", + "on": "\u05d6\u05d5\u05d4\u05d4" + }, "co": { "off": "\u05e0\u05e7\u05d9", "on": "\u05d6\u05d5\u05d4\u05d4" diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index dad23682dac..d38067588a8 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -12,7 +12,7 @@ "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." }, "description": "Introduce la configuraci\u00f3n de Google Cast.", - "title": "Google Cast" + "title": "Configuraci\u00f3n de Google Cast" }, "confirm": { "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" diff --git a/homeassistant/components/dlna_dms/translations/bg.json b/homeassistant/components/dlna_dms/translations/bg.json new file mode 100644 index 00000000000..b43da9ecb18 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/es.json b/homeassistant/components/dlna_dms/translations/es.json new file mode 100644 index 00000000000..b4bc90a666a --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos en la red" + }, + "step": { + "user": { + "description": "Escoge un dispositivo a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/he.json b/homeassistant/components/dlna_dms/translations/he.json new file mode 100644 index 00000000000..cfe995b8921 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/it.json b/homeassistant/components/dlna_dms/translations/it.json new file mode 100644 index 00000000000..a8c0d50c0cf --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "bad_ssdp": "Ai dati SSDP manca un valore richiesto", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "not_dms": "Il dispositivo non \u00e8 un server multimediale supportato" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Seleziona un dispositivo da configurare", + "title": "Dispositivi DLNA DMA rilevati" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index 0169d275205..6e594556708 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Secondi per considerare un dispositivo \"a casa\"" + "consider_home": "Secondi per considerare un dispositivo \"a casa\"", + "old_discovery": "Abilita il vecchio metodo di rilevamento" } } } diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 6008d399d64..cc1925b8095 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -35,6 +35,11 @@ "description": "Verifique todas las c\u00e1maras que admiten transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." }, + "exclude": { + "data": { + "entities": "Entidades" + } + }, "include_exclude": { "data": { "entities": "Entidades", diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 95837352502..56ce395aa4c 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -14,7 +14,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", - "not_a_number": "Per favore inserisci un numero", + "not_a_number": "Digita un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", "unknown": "Errore imprevisto" @@ -34,7 +34,7 @@ "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", - "not_a_number": "Per favore inserisci un numero", + "not_a_number": "Digita un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index 234a2bd0b30..4853b124856 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -14,7 +14,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", - "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", + "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" @@ -34,7 +34,7 @@ "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "not_a_number": "\u8acb\u8f38\u5165\u865f\u78bc", + "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/nanoleaf/translations/de.json b/homeassistant/components/nanoleaf/translations/de.json index fa1b9d98057..e29b3b76a46 100644 --- a/homeassistant/components/nanoleaf/translations/de.json +++ b/homeassistant/components/nanoleaf/translations/de.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Nach unten wischen", + "swipe_left": "Nach links wischen", + "swipe_right": "Nach rechts wischen", + "swipe_up": "Nach oben wischen" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/et.json b/homeassistant/components/nanoleaf/translations/et.json index 15690ab3072..d080167306a 100644 --- a/homeassistant/components/nanoleaf/translations/et.json +++ b/homeassistant/components/nanoleaf/translations/et.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Nipsa alla", + "swipe_left": "Nipsa vasakule", + "swipe_right": "Nipsa paremale", + "swipe_up": "Nipsa \u00fcles" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/it.json b/homeassistant/components/nanoleaf/translations/it.json index 1e7517d8ada..ccd36acd887 100644 --- a/homeassistant/components/nanoleaf/translations/it.json +++ b/homeassistant/components/nanoleaf/translations/it.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Scorri verso il basso", + "swipe_left": "Scorri verso sinistra", + "swipe_right": "Scorri verso destra", + "swipe_up": "Scorri verso l'alto" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pt-BR.json b/homeassistant/components/nanoleaf/translations/pt-BR.json index 3946a794711..98e60a4bf27 100644 --- a/homeassistant/components/nanoleaf/translations/pt-BR.json +++ b/homeassistant/components/nanoleaf/translations/pt-BR.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Deslize para baixo", + "swipe_left": "Deslize para a esquerda", + "swipe_right": "Desliza para a direita", + "swipe_up": "Deslize para cima" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/ru.json b/homeassistant/components/nanoleaf/translations/ru.json index 884ace3dedc..93f40d654d9 100644 --- a/homeassistant/components/nanoleaf/translations/ru.json +++ b/homeassistant/components/nanoleaf/translations/ru.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u0421\u0432\u0430\u0439\u043f \u0432\u043d\u0438\u0437", + "swipe_left": "\u0421\u0432\u0430\u0439\u043f \u0432\u043b\u0435\u0432\u043e", + "swipe_right": "\u0421\u0432\u0430\u0439\u043f \u0432\u043f\u0440\u0430\u0432\u043e", + "swipe_up": "\u0421\u0432\u0430\u0439\u043f \u0432\u0432\u0435\u0440\u0445" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/zh-Hant.json b/homeassistant/components/nanoleaf/translations/zh-Hant.json index cc5d1c08a8b..c18331f858c 100644 --- a/homeassistant/components/nanoleaf/translations/zh-Hant.json +++ b/homeassistant/components/nanoleaf/translations/zh-Hant.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u5411\u4e0b\u6ed1", + "swipe_left": "\u5411\u5de6\u6ed1", + "swipe_right": "\u5411\u53f3\u6ed1", + "swipe_up": "\u5411\u4e0a\u6ed1" + } } } \ No newline at end of file diff --git a/homeassistant/components/number/translations/zh-Hant.json b/homeassistant/components/number/translations/zh-Hant.json index d36f751682d..d724acc8f5e 100644 --- a/homeassistant/components/number/translations/zh-Hant.json +++ b/homeassistant/components/number/translations/zh-Hant.json @@ -4,5 +4,5 @@ "set_value": "{entity_name} \u8a2d\u5b9a\u503c" } }, - "title": "\u865f\u78bc" + "title": "\u6578\u5b57" } \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/es.json b/homeassistant/components/panasonic_viera/translations/es.json index cb8021f36d5..d14cc6b878b 100644 --- a/homeassistant/components/panasonic_viera/translations/es.json +++ b/homeassistant/components/panasonic_viera/translations/es.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_pin_code": "El c\u00f3digo PIN que ha introducido no es v\u00e1lido" + "invalid_pin_code": "El c\u00f3digo PIN que has introducido no es v\u00e1lido" }, "step": { "pairing": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduzca el PIN que aparece en su Televisor", + "description": "Introduce el PIN que aparece en tu Televisor", "title": "Emparejamiento" }, "user": { diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 71365b9e2f2..c31dcf26e07 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -9,7 +9,7 @@ "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { - "faulty_credentials": "La autorizaci\u00f3n fall\u00f3, verifica el token", + "faulty_credentials": "La autorizaci\u00f3n ha fallado, verifica el token", "host_or_token": "Debes proporcionar al menos uno de Host o Token", "no_servers": "No hay servidores vinculados a la cuenta Plex", "not_found": "No se ha encontrado el servidor Plex", diff --git a/homeassistant/components/radio_browser/translations/es.json b/homeassistant/components/radio_browser/translations/es.json new file mode 100644 index 00000000000..6bb377d2d28 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solamente una configuraci\u00f3n es posible." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/he.json b/homeassistant/components/radio_browser/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json index 80f59008dc3..e8a07049e29 100644 --- a/homeassistant/components/remote/translations/zh-Hant.json +++ b/homeassistant/components/remote/translations/zh-Hant.json @@ -22,5 +22,5 @@ "on": "\u958b\u5553" } }, - "title": "\u9059\u63a7" + "title": "\u9059\u63a7\u5668" } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index cdc833eeebf..ca77c97900b 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -9,6 +9,11 @@ "unknown": "Error inesperado" }, "step": { + "reauth_validate": { + "data": { + "password": "Contrase\u00f1a" + } + }, "user": { "data": { "email": "Correo electr\u00f3nico", @@ -16,6 +21,11 @@ "timeout": "Timeout" }, "title": "Conectar a tu Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n" + } } } } diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json index c4e87259193..b46ec81b6d5 100644 --- a/homeassistant/components/sense/translations/he.json +++ b/homeassistant/components/sense/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -9,6 +10,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_validate": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "email": "\u05d3\u05d5\u05d0\"\u05dc", diff --git a/homeassistant/components/sense/translations/it.json b/homeassistant/components/sense/translations/it.json index 66c09d294c1..f769de0c3d0 100644 --- a/homeassistant/components/sense/translations/it.json +++ b/homeassistant/components/sense/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_validate": { + "data": { + "password": "Password" + }, + "description": "L'integrazione Sense deve autenticare nuovamente il tuo account {email}.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "email": "Email", @@ -16,6 +24,12 @@ "timeout": "Tempo scaduto" }, "title": "Connettiti al tuo Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Codice di verifica" + }, + "title": "Autenticazione a pi\u00f9 fattori Sense" } } } diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index 6b8cd4ac642..8715dc6b7aa 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, "error": { "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index 29dc5bfd9a9..c6ab1b205e9 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -18,7 +18,8 @@ "data": { "api_key": "API \u043a\u043b\u044e\u0447", "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442" + "port": "\u041f\u043e\u0440\u0442", + "url": "URL" } } } diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index cee9f6661c5..b2f073297d9 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -22,6 +22,7 @@ "host": "Host", "port": "Puerto", "ssl": "Utiliza un certificado SSL", + "url": "URL", "verify_ssl": "Verificar certificado SSL" } } diff --git a/homeassistant/components/sonarr/translations/he.json b/homeassistant/components/sonarr/translations/he.json index d033613184e..53b98ae4139 100644 --- a/homeassistant/components/sonarr/translations/he.json +++ b/homeassistant/components/sonarr/translations/he.json @@ -20,6 +20,7 @@ "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4", "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } } diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index 9dc6582c150..ba78810d928 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {host}", + "description": "L'integrazione di Sonarr deve essere nuovamente autenticata manualmente con l'API Sonarr ospitata su: {url}", "title": "Autentica nuovamente l'integrazione" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Porta", "ssl": "Utilizza un certificato SSL", + "url": "URL", "verify_ssl": "Verifica il certificato SSL" } } diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index adc306feae4..02201f09dad 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -29,7 +29,15 @@ }, "tuya__humidifier_level": { "level_1": "Nivel 1", - "level_10": "Nivel 10" + "level_10": "Nivel 10", + "level_2": "Nivel 2", + "level_3": "Nivel 3", + "level_4": "Nivel 4", + "level_5": "Nivel 5", + "level_6": "Nivel 6", + "level_7": "Nivel 7", + "level_8": "Nivel 8", + "level_9": "Nivel 9" }, "tuya__humidifier_spray_mode": { "health": "Salud", diff --git a/homeassistant/components/vacuum/translations/zh-Hant.json b/homeassistant/components/vacuum/translations/zh-Hant.json index 0f141b0f225..1382ef7f1a0 100644 --- a/homeassistant/components/vacuum/translations/zh-Hant.json +++ b/homeassistant/components/vacuum/translations/zh-Hant.json @@ -25,5 +25,5 @@ "returning": "\u8fd4\u56de\u5145\u96fb" } }, - "title": "\u5438\u5875\u5668" + "title": "\u6383\u5730\u6a5f\u5668\u4eba" } \ No newline at end of file From ff7510f96c43563e631b9c74da23b92b898a5621 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 16:30:53 -0800 Subject: [PATCH 0035/1054] Bump aiohue to 4.3.0 (#67202) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 193d2b7fa81..bf6e7f06abd 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.2.1"], + "requirements": ["aiohue==4.3.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 9d87591110b..b5e42ca8b39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -182,7 +182,7 @@ aiohomekit==0.7.14 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.1 +aiohue==4.3.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca17fbd8889..51ffe6ce4f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ aiohomekit==0.7.14 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.2.1 +aiohue==4.3.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 From 7842d12b75254595ef19b1e37bcf6409dbce1621 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 24 Feb 2022 21:28:36 -0600 Subject: [PATCH 0036/1054] 20220224.0 (#67204) --- homeassistant/components/frontend/manifest.json | 5 ++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ad728fd3604..f9ad2bd428d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220223.0" + "home-assistant-frontend==20220224.0" ], "dependencies": [ "api", @@ -13,8 +13,7 @@ "diagnostics", "http", "lovelace", - "onboarding", - "search", + "onboarding", "search", "system_log", "websocket_api" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 176e25c414f..1bd905e6abd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.53.1 -home-assistant-frontend==20220223.0 +home-assistant-frontend==20220224.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index b5e42ca8b39..616b84f92c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -825,7 +825,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220223.0 +home-assistant-frontend==20220224.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51ffe6ce4f4..3187f6f9224 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220223.0 +home-assistant-frontend==20220224.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From ad4409bcb0e5f213ed9a2ddc1c5cdb93f6f50637 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 25 Feb 2022 01:39:30 -0500 Subject: [PATCH 0037/1054] Don't add extra entities for zwave_js controller (#67209) * Don't add extra entities for zwave_js controller * Revert reformat of controller_state * fix indentation issues * fix indentation issues --- homeassistant/components/zwave_js/__init__.py | 22 ++-- tests/components/zwave_js/conftest.py | 14 +++ .../fixtures/aeon_smart_switch_6_state.json | 3 +- .../aeotec_radiator_thermostat_state.json | 3 +- .../fixtures/aeotec_zw164_siren_state.json | 3 +- .../fixtures/bulb_6_multi_color_state.json | 3 +- .../fixtures/chain_actuator_zws12_state.json | 3 +- .../fixtures/climate_danfoss_lc_13_state.json | 3 +- .../climate_eurotronic_spirit_z_state.json | 3 +- .../climate_heatit_z_trm2fx_state.json | 3 +- .../climate_heatit_z_trm3_no_value_state.json | 3 +- .../fixtures/climate_heatit_z_trm3_state.json | 3 +- ...setpoint_on_different_endpoints_state.json | 3 +- ..._ct100_plus_different_endpoints_state.json | 3 +- ...ate_radio_thermostat_ct100_plus_state.json | 3 +- ...ostat_ct101_multiple_temp_units_state.json | 3 +- .../fixtures/controller_node_state.json | 104 ++++++++++++++++++ .../cover_aeotec_nano_shutter_state.json | 3 +- .../fixtures/cover_fibaro_fgr222_state.json | 3 +- .../fixtures/cover_iblinds_v2_state.json | 3 +- .../fixtures/cover_qubino_shutter_state.json | 3 +- .../zwave_js/fixtures/cover_zw062_state.json | 3 +- .../fixtures/eaton_rf9640_dimmer_state.json | 3 +- .../fixtures/ecolink_door_sensor_state.json | 3 +- .../zwave_js/fixtures/fan_ge_12730_state.json | 3 +- .../zwave_js/fixtures/fan_generic_state.json | 3 +- .../zwave_js/fixtures/fan_hs_fc200_state.json | 3 +- .../fixtures/fortrezz_ssa1_siren_state.json | 3 +- .../fixtures/fortrezz_ssa3_siren_state.json | 3 +- .../ge_in_wall_dimmer_switch_state.json | 3 +- .../fixtures/hank_binary_switch_state.json | 3 +- .../fixtures/inovelli_lzw36_state.json | 3 +- .../light_color_null_values_state.json | 3 +- .../fixtures/lock_august_asl03_state.json | 3 +- .../fixtures/lock_id_lock_as_id150_state.json | 3 +- ...pp_electric_strike_lock_control_state.json | 3 +- .../fixtures/lock_schlage_be469_state.json | 3 +- .../nortek_thermostat_added_event.json | 3 +- .../fixtures/nortek_thermostat_state.json | 3 +- .../fixtures/null_name_check_state.json | 3 +- .../fixtures/srt321_hrt4_zw_state.json | 3 +- .../vision_security_zl7432_state.json | 3 +- .../wallmote_central_scene_state.json | 3 +- .../zwave_js/fixtures/zen_31_state.json | 3 +- .../fixtures/zp3111-5_not_ready_state.json | 3 +- .../zwave_js/fixtures/zp3111-5_state.json | 3 +- tests/components/zwave_js/test_button.py | 13 +++ tests/components/zwave_js/test_init.py | 2 + tests/components/zwave_js/test_sensor.py | 17 ++- 49 files changed, 247 insertions(+), 54 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/controller_node_state.json diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 5a0a7bbf29f..5d294931e66 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -293,17 +293,19 @@ async def async_setup_entry( # noqa: C901 async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" - # Create a node status sensor for each device - await async_setup_platform(SENSOR_DOMAIN) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node - ) + # No need for a ping button or node status sensor for controller nodes + if not node.is_controller_node: + # Create a node status sensor for each device + await async_setup_platform(SENSOR_DOMAIN) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node + ) - # Create a ping button for each device - await async_setup_platform(BUTTON_DOMAIN) - async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node - ) + # Create a ping button for each device + await async_setup_platform(BUTTON_DOMAIN) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_ping_button_entity", node + ) # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 2535daaf114..9696c922fb3 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -181,6 +181,12 @@ def controller_state_fixture(): return json.loads(load_fixture("zwave_js/controller_state.json")) +@pytest.fixture(name="controller_node_state", scope="session") +def controller_node_state_fixture(): + """Load the controller node state fixture data.""" + return json.loads(load_fixture("zwave_js/controller_node_state.json")) + + @pytest.fixture(name="version_state", scope="session") def version_state_fixture(): """Load the version state fixture data.""" @@ -535,6 +541,14 @@ def mock_client_fixture(controller_state, version_state, log_config_state): yield client +@pytest.fixture(name="controller_node") +def controller_node_fixture(client, controller_node_state): + """Mock a controller node.""" + node = Node(client, copy.deepcopy(controller_node_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="multisensor_6") def multisensor_6_fixture(client, multisensor_6_state): """Mock a multisensor 6 node.""" diff --git a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json index 1eb2baf3a02..bf547556ac8 100644 --- a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json +++ b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json @@ -1245,5 +1245,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json index 646363e3313..cbd11c66870 100644 --- a/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_radiator_thermostat_state.json @@ -616,5 +616,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json index 5616abd6e0f..59e4fdfc9fb 100644 --- a/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json +++ b/tests/components/zwave_js/fixtures/aeotec_zw164_siren_state.json @@ -3752,5 +3752,6 @@ } ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0103:0x00a4:1.3", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json index 668d1b9dce9..b0dba3c6d05 100644 --- a/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json +++ b/tests/components/zwave_js/fixtures/bulb_6_multi_color_state.json @@ -645,5 +645,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json index a726175fa38..2b8477b597f 100644 --- a/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json +++ b/tests/components/zwave_js/fixtures/chain_actuator_zws12_state.json @@ -397,5 +397,6 @@ }, "value": 0 } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index 8574674714f..206a32df664 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -432,5 +432,6 @@ "1.1" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json index afa216cac32..1241e0b35d7 100644 --- a/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json +++ b/tests/components/zwave_js/fixtures/climate_eurotronic_spirit_z_state.json @@ -712,5 +712,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json index 2526e346a53..8c655d503ed 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm2fx_state.json @@ -1440,5 +1440,6 @@ "isSecure": false } ], - "interviewStage": "Complete" + "interviewStage": "Complete", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json index 50886b504a7..75d8bb99e55 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_no_value_state.json @@ -1246,5 +1246,6 @@ "isSecure": true } ], - "interviewStage": "Complete" + "interviewStage": "Complete", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json index 98c185fd8d5..0ac4c6ab696 100644 --- a/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json +++ b/tests/components/zwave_js/fixtures/climate_heatit_z_trm3_state.json @@ -1173,5 +1173,6 @@ }, "value": 25.5 } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json index 52f2168fd83..8bfe3a3f7af 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_mode_and_setpoint_on_different_endpoints_state.json @@ -826,5 +826,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index f940dd210aa..398371a7445 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -1083,5 +1083,6 @@ "version": 3, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json index 3805394dbce..b81acf66b80 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_state.json @@ -851,5 +851,6 @@ }, "value": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json index 5feaa247f2e..5c8a12a6832 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct101_multiple_temp_units_state.json @@ -958,5 +958,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/controller_node_state.json b/tests/components/zwave_js/fixtures/controller_node_state.json new file mode 100644 index 00000000000..1f3c71971bc --- /dev/null +++ b/tests/components/zwave_js/fixtures/controller_node_state.json @@ -0,0 +1,104 @@ +{ + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z\u2010Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false +} diff --git a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json index b5373f38ec4..7959378a7ad 100644 --- a/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_aeotec_nano_shutter_state.json @@ -494,5 +494,6 @@ } ], "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0371:0x0003:0x008d:3.1", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json index 59dff945846..6d4defbd42c 100644 --- a/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json +++ b/tests/components/zwave_js/fixtures/cover_fibaro_fgr222_state.json @@ -1129,5 +1129,6 @@ "commandsDroppedRX": 1, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json index 42200c2f1d6..4d10577a2d1 100644 --- a/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json +++ b/tests/components/zwave_js/fixtures/cover_iblinds_v2_state.json @@ -353,5 +353,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json index bde7c90e1e4..913f24d41ae 100644 --- a/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json +++ b/tests/components/zwave_js/fixtures/cover_qubino_shutter_state.json @@ -896,5 +896,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/cover_zw062_state.json b/tests/components/zwave_js/fixtures/cover_zw062_state.json index a0ccd4de9c5..47aafdfd0a4 100644 --- a/tests/components/zwave_js/fixtures/cover_zw062_state.json +++ b/tests/components/zwave_js/fixtures/cover_zw062_state.json @@ -917,5 +917,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json index 3edb0494c37..a1806a99ce0 100644 --- a/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json +++ b/tests/components/zwave_js/fixtures/eaton_rf9640_dimmer_state.json @@ -775,5 +775,6 @@ "value": 0, "ccVersion": 3 } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json index ac32a9f99bb..444b7eafc67 100644 --- a/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json +++ b/tests/components/zwave_js/fixtures/ecolink_door_sensor_state.json @@ -325,6 +325,7 @@ "2.0" ] } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json index ddbff0f3ffa..fa4c96d439a 100644 --- a/tests/components/zwave_js/fixtures/fan_ge_12730_state.json +++ b/tests/components/zwave_js/fixtures/fan_ge_12730_state.json @@ -427,5 +427,6 @@ "3.10" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fan_generic_state.json b/tests/components/zwave_js/fixtures/fan_generic_state.json index d09848fb759..fc89976d14a 100644 --- a/tests/components/zwave_js/fixtures/fan_generic_state.json +++ b/tests/components/zwave_js/fixtures/fan_generic_state.json @@ -348,5 +348,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json index f83a1193c22..edab052af5b 100644 --- a/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json +++ b/tests/components/zwave_js/fixtures/fan_hs_fc200_state.json @@ -10502,5 +10502,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 2 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json index d8973f2688e..8c88082718c 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa1_siren_state.json @@ -346,5 +346,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 2 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json index fb31f838667..aa0e05dd47f 100644 --- a/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json +++ b/tests/components/zwave_js/fixtures/fortrezz_ssa3_siren_state.json @@ -351,5 +351,6 @@ "commandsDroppedTX": 0, "timeoutResponse": 1 }, - "highestSecurityClass": -1 + "highestSecurityClass": -1, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json index d47896a980a..4cbf9ef1ce4 100644 --- a/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json +++ b/tests/components/zwave_js/fixtures/ge_in_wall_dimmer_switch_state.json @@ -638,5 +638,6 @@ "mandatoryControlledCCs": [] }, "interviewStage": "Complete", - "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26" + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0063:0x4944:0x3038:5.26", + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/hank_binary_switch_state.json b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json index d27338db5a9..926285e5359 100644 --- a/tests/components/zwave_js/fixtures/hank_binary_switch_state.json +++ b/tests/components/zwave_js/fixtures/hank_binary_switch_state.json @@ -720,5 +720,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json index bfa56891413..11e88eff8be 100644 --- a/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json +++ b/tests/components/zwave_js/fixtures/inovelli_lzw36_state.json @@ -1952,5 +1952,6 @@ } } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/light_color_null_values_state.json b/tests/components/zwave_js/fixtures/light_color_null_values_state.json index 213b873f85c..b244913070c 100644 --- a/tests/components/zwave_js/fixtures/light_color_null_values_state.json +++ b/tests/components/zwave_js/fixtures/light_color_null_values_state.json @@ -685,5 +685,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json index b22c21e4777..642682766df 100644 --- a/tests/components/zwave_js/fixtures/lock_august_asl03_state.json +++ b/tests/components/zwave_js/fixtures/lock_august_asl03_state.json @@ -446,5 +446,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json index f5e66b7e7a6..5bd4cfc8080 100644 --- a/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json +++ b/tests/components/zwave_js/fixtures/lock_id_lock_as_id150_state.json @@ -2915,5 +2915,6 @@ "version": 1, "isSecure": true } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json index 2b4a3a88984..dc6e9e40d7c 100644 --- a/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json +++ b/tests/components/zwave_js/fixtures/lock_popp_electric_strike_lock_control_state.json @@ -564,5 +564,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json index fedee0f9cf1..64f83a43e0d 100644 --- a/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json +++ b/tests/components/zwave_js/fixtures/lock_schlage_be469_state.json @@ -2103,5 +2103,6 @@ }, "value": 0 } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json index 598650b863c..39c04216a04 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_added_event.json @@ -250,7 +250,8 @@ "label": "Dimming duration" } } - ] + ], + "isControllerNode": false }, "result": {} } diff --git a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json index a3b34aeedf0..a99303af259 100644 --- a/tests/components/zwave_js/fixtures/nortek_thermostat_state.json +++ b/tests/components/zwave_js/fixtures/nortek_thermostat_state.json @@ -1275,5 +1275,6 @@ "label": "Dimming duration" } } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/null_name_check_state.json b/tests/components/zwave_js/fixtures/null_name_check_state.json index fe63eaee207..8905e47b155 100644 --- a/tests/components/zwave_js/fixtures/null_name_check_state.json +++ b/tests/components/zwave_js/fixtures/null_name_check_state.json @@ -410,5 +410,6 @@ "version": 3, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json index a2fdaa99561..d1db5664f76 100644 --- a/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json +++ b/tests/components/zwave_js/fixtures/srt321_hrt4_zw_state.json @@ -258,5 +258,6 @@ "2.0" ] } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json index d37e82ea3af..f7abbffb590 100644 --- a/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json +++ b/tests/components/zwave_js/fixtures/vision_security_zl7432_state.json @@ -429,5 +429,6 @@ "version": 1, "isSecure": false } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json index f5560dd7e78..af5314002fa 100644 --- a/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json +++ b/tests/components/zwave_js/fixtures/wallmote_central_scene_state.json @@ -694,5 +694,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zen_31_state.json b/tests/components/zwave_js/fixtures/zen_31_state.json index 7407607e086..3b1278da0b9 100644 --- a/tests/components/zwave_js/fixtures/zen_31_state.json +++ b/tests/components/zwave_js/fixtures/zen_31_state.json @@ -2803,5 +2803,6 @@ "version": 1, "isSecure": true } - ] + ], + "isControllerNode": false } \ No newline at end of file diff --git a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json index f892eb5570e..272f6118830 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_not_ready_state.json @@ -64,5 +64,6 @@ "commandsDroppedRX": 0, "commandsDroppedTX": 0, "timeoutResponse": 0 - } + }, + "isControllerNode": false } diff --git a/tests/components/zwave_js/fixtures/zp3111-5_state.json b/tests/components/zwave_js/fixtures/zp3111-5_state.json index 8de7dd2b713..e652653d946 100644 --- a/tests/components/zwave_js/fixtures/zp3111-5_state.json +++ b/tests/components/zwave_js/fixtures/zp3111-5_state.json @@ -702,5 +702,6 @@ "commandsDroppedTX": 0, "timeoutResponse": 0 }, - "highestSecurityClass": -1 + "highestSecurityClass": -1, + "isControllerNode": false } diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 9b5ac66b06f..29858e0eb97 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -1,13 +1,16 @@ """Test the Z-Wave JS button entities.""" from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.zwave_js.const import DOMAIN, SERVICE_REFRESH_VALUE +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.entity_registry import async_get async def test_ping_entity( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, + controller_node, integration, caplog, ): @@ -44,3 +47,13 @@ async def test_ping_entity( ) assert "There is no value to refresh for this entity" in caplog.text + + # Assert a node ping button entity is not created for the controller + node = client.driver.controller.nodes[1] + assert node.is_controller_node + assert ( + async_get(hass).async_get_entity_id( + DOMAIN, "sensor", f"{get_valueless_base_unique_id(client, node)}.ping" + ) + is None + ) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 1003316f1e5..1b3a2d1204f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -934,6 +934,7 @@ async def test_replace_same_node( "commandsDroppedTX": 0, "timeoutResponse": 0, }, + "isControllerNode": False, }, "result": {}, }, @@ -1052,6 +1053,7 @@ async def test_replace_different_node( "commandsDroppedTX": 0, "timeoutResponse": 0, }, + "isControllerNode": False, }, "result": {}, }, diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 891a417551e..1d41e145a95 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.zwave_js.const import ( SERVICE_REFRESH_VALUE, SERVICE_RESET_METER, ) +from homeassistant.components.zwave_js.helpers import get_valueless_base_unique_id from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -155,7 +156,9 @@ async def test_config_parameter_sensor(hass, lock_id_lock_as_id150, integration) assert entity_entry.disabled -async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integration): +async def test_node_status_sensor( + hass, client, controller_node, lock_id_lock_as_id150, integration +): """Test node status sensor is created and gets updated on node state changes.""" NODE_STATUS_ENTITY = "sensor.z_wave_module_for_id_lock_150_and_101_node_status" node = lock_id_lock_as_id150 @@ -201,6 +204,18 @@ async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integrati await client.disconnect() assert hass.states.get(NODE_STATUS_ENTITY).state != STATE_UNAVAILABLE + # Assert a node status sensor entity is not created for the controller + node = client.driver.controller.nodes[1] + assert node.is_controller_node + assert ( + ent_reg.async_get_entity_id( + DOMAIN, + "sensor", + f"{get_valueless_base_unique_id(client, node)}.node_status", + ) + is None + ) + async def test_node_status_sensor_not_ready( hass, From 8a74295d6f9140e448c47d55aa3939c44630b2b7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 25 Feb 2022 07:53:22 +0100 Subject: [PATCH 0038/1054] Add support for rfxtrx sirens and chimes (#66416) * Add support for sirens and chimes * Fixup testing * Fixup comments * Hook up existing off delay * Add docs for off delay. * Rename mixin --- homeassistant/components/rfxtrx/__init__.py | 2 + homeassistant/components/rfxtrx/siren.py | 245 ++++++++++++++++++++ tests/components/rfxtrx/test_siren.py | 138 +++++++++++ 3 files changed, 385 insertions(+) create mode 100644 homeassistant/components/rfxtrx/siren.py create mode 100644 tests/components/rfxtrx/test_siren.py diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 2dcfe639a64..edf79ce15a5 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -49,6 +49,7 @@ from .const import ( DOMAIN = "rfxtrx" DEFAULT_SIGNAL_REPETITIONS = 1 +DEFAULT_OFF_DELAY = 2.0 SIGNAL_EVENT = f"{DOMAIN}_event" @@ -81,6 +82,7 @@ PLATFORMS = [ Platform.LIGHT, Platform.BINARY_SENSOR, Platform.COVER, + Platform.SIREN, ] diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py new file mode 100644 index 00000000000..78c970c4934 --- /dev/null +++ b/homeassistant/components/rfxtrx/siren.py @@ -0,0 +1,245 @@ +"""Support for RFXtrx sirens.""" +from __future__ import annotations + +from typing import Any + +import RFXtrx as rfxtrxmod + +from homeassistant.components.siren import ( + SUPPORT_TONES, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SirenEntity, +) +from homeassistant.components.siren.const import ATTR_TONE +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later + +from . import ( + DEFAULT_OFF_DELAY, + DEFAULT_SIGNAL_REPETITIONS, + DeviceTuple, + RfxtrxCommandEntity, + async_setup_platform_entry, +) +from .const import CONF_OFF_DELAY, CONF_SIGNAL_REPETITIONS + +SUPPORT_RFXTRX = SUPPORT_TURN_ON | SUPPORT_TONES + +SECURITY_PANIC_ON = "Panic" +SECURITY_PANIC_OFF = "End Panic" +SECURITY_PANIC_ALL = {SECURITY_PANIC_ON, SECURITY_PANIC_OFF} + + +def supported(event: rfxtrxmod.RFXtrxEvent): + """Return whether an event supports sirens.""" + device = event.device + + if isinstance(device, rfxtrxmod.ChimeDevice): + return True + + if isinstance(device, rfxtrxmod.SecurityDevice) and isinstance( + event, rfxtrxmod.SensorEvent + ): + if event.values["Sensor Status"] in SECURITY_PANIC_ALL: + return True + + return False + + +def get_first_key(data: dict[int, str], entry: str) -> int: + """Find a key based on the items value.""" + return next((key for key, value in data.items() if value == entry)) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up config entry.""" + + def _constructor( + event: rfxtrxmod.RFXtrxEvent, + auto: rfxtrxmod.RFXtrxEvent | None, + device_id: DeviceTuple, + entity_info: dict, + ): + """Construct a entity from an event.""" + device = event.device + + if isinstance(device, rfxtrxmod.ChimeDevice): + return [ + RfxtrxChime( + event.device, + device_id, + entity_info.get( + CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS + ), + entity_info.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY), + auto, + ) + ] + + if isinstance(device, rfxtrxmod.SecurityDevice) and isinstance( + event, rfxtrxmod.SensorEvent + ): + if event.values["Sensor Status"] in SECURITY_PANIC_ALL: + return [ + RfxtrxSecurityPanic( + event.device, + device_id, + entity_info.get( + CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS + ), + entity_info.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY), + auto, + ) + ] + + await async_setup_platform_entry( + hass, config_entry, async_add_entities, supported, _constructor + ) + + +class RfxtrxOffDelayMixin(Entity): + """Mixin to support timeouts on data. + + Many 433 devices only send data when active. They will + repeatedly (every x seconds) send a command to indicate + being active and stop sending this command when inactive. + This mixin allow us to keep track of the timeout once + they go inactive. + """ + + _timeout: CALLBACK_TYPE | None = None + _off_delay: float | None = None + + def _setup_timeout(self): + @callback + def _done(_): + self._timeout = None + self.async_write_ha_state() + + if self._off_delay: + self._timeout = async_call_later(self.hass, self._off_delay, _done) + + def _cancel_timeout(self): + if self._timeout: + self._timeout() + self._timeout = None + + +class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): + """Representation of a RFXtrx chime.""" + + _device: rfxtrxmod.ChimeDevice + + def __init__( + self, device, device_id, signal_repetitions=1, off_delay=None, event=None + ): + """Initialize the entity.""" + super().__init__(device, device_id, signal_repetitions, event) + self._attr_available_tones = list(self._device.COMMANDS.values()) + self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TONES + self._default_tone = next(iter(self._device.COMMANDS)) + self._off_delay = off_delay + + @property + def is_on(self): + """Return true if device is on.""" + return self._timeout is not None + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self._cancel_timeout() + + if tone := kwargs.get(ATTR_TONE): + command = get_first_key(self._device.COMMANDS, tone) + else: + command = self._default_tone + + await self._async_send(self._device.send_command, command) + + self._setup_timeout() + + self.async_write_ha_state() + + def _apply_event(self, event: rfxtrxmod.ControlEvent): + """Apply a received event.""" + super()._apply_event(event) + + sound = event.values.get("Sound") + if sound is not None: + self._cancel_timeout() + self._setup_timeout() + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if self._event_applies(event, device_id): + self._apply_event(event) + + self.async_write_ha_state() + + +class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): + """Representation of a security device.""" + + _device: rfxtrxmod.SecurityDevice + + def __init__( + self, device, device_id, signal_repetitions=1, off_delay=None, event=None + ): + """Initialize the entity.""" + super().__init__(device, device_id, signal_repetitions, event) + self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF + self._on_value = get_first_key(self._device.STATUS, SECURITY_PANIC_ON) + self._off_value = get_first_key(self._device.STATUS, SECURITY_PANIC_OFF) + self._off_delay = off_delay + + @property + def is_on(self): + """Return true if device is on.""" + return self._timeout is not None + + async def async_turn_on(self, **kwargs: Any): + """Turn the device on.""" + self._cancel_timeout() + + await self._async_send(self._device.send_status, self._on_value) + + self._setup_timeout() + + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._cancel_timeout() + + await self._async_send(self._device.send_status, self._off_value) + + self.async_write_ha_state() + + def _apply_event(self, event: rfxtrxmod.SensorEvent): + """Apply a received event.""" + super()._apply_event(event) + + status = event.values.get("Sensor Status") + + if status == SECURITY_PANIC_ON: + self._cancel_timeout() + self._setup_timeout() + elif status == SECURITY_PANIC_OFF: + self._cancel_timeout() + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if self._event_applies(event, device_id): + self._apply_event(event) + + self.async_write_ha_state() diff --git a/tests/components/rfxtrx/test_siren.py b/tests/components/rfxtrx/test_siren.py new file mode 100644 index 00000000000..98859b84109 --- /dev/null +++ b/tests/components/rfxtrx/test_siren.py @@ -0,0 +1,138 @@ +"""The tests for the Rfxtrx siren platform.""" +from unittest.mock import call + +from homeassistant.components.rfxtrx import DOMAIN + +from .conftest import create_rfx_test_cfg + +from tests.common import MockConfigEntry + + +async def test_one_chime(hass, rfxtrx, timestep): + """Test with 1 entity.""" + entry_data = create_rfx_test_cfg( + devices={"0a16000000000000000000": {"signal_repetitions": 1, "off_delay": 2.0}} + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + entity_id = "siren.byron_sx_00_00" + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "Byron SX 00:00" + + await hass.services.async_call( + "siren", "turn_on", {"entity_id": entity_id}, blocking=True + ) + state = hass.states.get(entity_id) + assert state.state == "on" + + await timestep(5) + + state = hass.states.get(entity_id) + assert state.state == "off" + + await hass.services.async_call( + "siren", "turn_on", {"entity_id": entity_id, "tone": "Sound 1"}, blocking=True + ) + state = hass.states.get(entity_id) + assert state.state == "on" + + await timestep(3) + + state = hass.states.get(entity_id) + assert state.state == "off" + + await rfxtrx.signal("0a16000000000000000000") + state = hass.states.get(entity_id) + assert state.state == "on" + + await timestep(3) + + state = hass.states.get(entity_id) + assert state.state == "off" + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x07\x16\x00\x00\x00\x00\x00\x00")), + call(bytearray(b"\x07\x16\x00\x00\x00\x00\x01\x00")), + ] + + +async def test_one_security1(hass, rfxtrx, timestep): + """Test with 1 entity.""" + entry_data = create_rfx_test_cfg( + devices={"08200300a109000670": {"signal_repetitions": 1, "off_delay": 2.0}} + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + entity_id = "siren.kd101_smoke_detector_a10900_32" + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == "off" + assert state.attributes.get("friendly_name") == "KD101 Smoke Detector a10900:32" + + await hass.services.async_call( + "siren", "turn_on", {"entity_id": entity_id}, blocking=True + ) + state = hass.states.get(entity_id) + assert state.state == "on" + + await hass.services.async_call( + "siren", "turn_off", {"entity_id": entity_id}, blocking=True + ) + state = hass.states.get(entity_id) + assert state.state == "off" + + await hass.services.async_call( + "siren", "turn_on", {"entity_id": entity_id}, blocking=True + ) + state = hass.states.get(entity_id) + assert state.state == "on" + + await timestep(11) + + state = hass.states.get(entity_id) + assert state.state == "off" + + await rfxtrx.signal("08200300a109000670") + state = hass.states.get(entity_id) + assert state.state == "on" + + await rfxtrx.signal("08200300a109000770") + state = hass.states.get(entity_id) + assert state.state == "off" + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x20\x03\x00\xa1\x09\x00\x06\x00")), + call(bytearray(b"\x08\x20\x03\x01\xa1\x09\x00\x07\x00")), + call(bytearray(b"\x08\x20\x03\x02\xa1\x09\x00\x06\x00")), + ] + + +async def test_discover_siren(hass, rfxtrx_automatic): + """Test with discovery.""" + rfxtrx = rfxtrx_automatic + + await rfxtrx.signal("0a16000000000000000000") + state = hass.states.get("siren.byron_sx_00_00") + assert state + assert state.state == "on" + assert state.attributes.get("friendly_name") == "Byron SX 00:00" + + await rfxtrx.signal("0a16010000000000000000") + state = hass.states.get("siren.byron_mp001_00_00") + assert state + assert state.state == "on" + assert state.attributes.get("friendly_name") == "Byron MP001 00:00" From 406fbca4bce3c3be9d84a4b59eb40f4a9726a3ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Feb 2022 23:40:28 -0800 Subject: [PATCH 0039/1054] Add media source support to Kodi (#67203) --- homeassistant/components/kodi/browse_media.py | 12 ++++++++- homeassistant/components/kodi/manifest.json | 1 + homeassistant/components/kodi/media_player.py | 26 ++++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 1b0c5d521c9..e0fdb0f73fd 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,7 +1,9 @@ """Support for media browsing.""" import asyncio +import contextlib import logging +from homeassistant.components import media_source from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, @@ -184,7 +186,7 @@ async def item_payload(item, get_thumbnail_url=None): ) -async def library_payload(): +async def library_payload(hass): """ Create response payload to describe contents of a specific library. @@ -222,6 +224,14 @@ async def library_payload(): ) ) + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media(hass, None) + # If domain is None, it's overview of available sources + if item.domain is None: + library_info.children.extend(item.children) + else: + library_info.children.append(item) + return library_info diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 3a39a7870a3..86034ea9cfc 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,6 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", + "after_dependencies": ["media_source"], "requirements": ["pykodi==0.2.7"], "codeowners": ["@OnFreund", "@cgtobi"], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 5067ee84826..56b0abb6a15 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -5,6 +5,7 @@ from datetime import timedelta from functools import wraps import logging import re +from typing import Any import urllib.parse import jsonrpc_base @@ -12,7 +13,11 @@ from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -24,6 +29,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_SEASON, MEDIA_TYPE_TRACK, MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, @@ -691,8 +697,15 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.media_seek(position) @cmd - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + media_type_lower = media_type.lower() if media_type_lower == MEDIA_TYPE_CHANNEL: @@ -700,7 +713,7 @@ class KodiEntity(MediaPlayerEntity): elif media_type_lower == MEDIA_TYPE_PLAYLIST: await self._kodi.play_playlist(int(media_id)) elif media_type_lower == "directory": - await self._kodi.play_directory(str(media_id)) + await self._kodi.play_directory(media_id) elif media_type_lower in [ MEDIA_TYPE_ARTIST, MEDIA_TYPE_ALBUM, @@ -719,7 +732,9 @@ class KodiEntity(MediaPlayerEntity): {MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)} ) else: - await self._kodi.play_file(str(media_id)) + media_id = async_process_play_media_url(self.hass, media_id) + + await self._kodi.play_file(media_id) @cmd async def async_set_shuffle(self, shuffle): @@ -898,7 +913,10 @@ class KodiEntity(MediaPlayerEntity): ) if media_content_type in [None, "library"]: - return await library_payload() + return await library_payload(self.hass) + + if media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media(self.hass, media_content_id) payload = { "search_type": media_content_type, From ad6c3d37be9a719a8cb4314c160ce5e3c4738396 Mon Sep 17 00:00:00 2001 From: kevdliu <1766838+kevdliu@users.noreply.github.com> Date: Fri, 25 Feb 2022 03:44:17 -0500 Subject: [PATCH 0040/1054] Take Abode camera snapshot before fetching latest image (#67150) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/abode/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 9885ccb54ef..1eb6f859d0c 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -88,6 +88,8 @@ class AbodeCamera(AbodeDevice, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Get a camera image.""" + if not self.capture(): + return None self.refresh_image() if self._response: From 21715c5f8ae67606758b25f7119a9952fc85faef Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 25 Feb 2022 19:59:16 +1100 Subject: [PATCH 0041/1054] Bump the Twinkly dependency to fix the excessive debug output (#67207) --- homeassistant/components/twinkly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json index 871cd27166d..e3b97e9385b 100644 --- a/homeassistant/components/twinkly/manifest.json +++ b/homeassistant/components/twinkly/manifest.json @@ -2,7 +2,7 @@ "domain": "twinkly", "name": "Twinkly", "documentation": "https://www.home-assistant.io/integrations/twinkly", - "requirements": ["ttls==1.4.2"], + "requirements": ["ttls==1.4.3"], "codeowners": ["@dr1rrb", "@Robbie1221"], "config_flow": true, "dhcp": [{ "hostname": "twinkly_*" }], diff --git a/requirements_all.txt b/requirements_all.txt index 616b84f92c7..916b320acca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2364,7 +2364,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.2 +ttls==1.4.3 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3187f6f9224..286473ee5aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1456,7 +1456,7 @@ total_connect_client==2022.2.1 transmissionrpc==0.11 # homeassistant.components.twinkly -ttls==1.4.2 +ttls==1.4.3 # homeassistant.components.tuya tuya-iot-py-sdk==0.6.6 From 684f01f4664ad490a314ae983194c0f439445a16 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 25 Feb 2022 10:22:48 +0100 Subject: [PATCH 0042/1054] Add tests for LCN cover platform (#64832) --- .coveragerc | 1 - tests/components/lcn/fixtures/config.json | 13 + .../lcn/fixtures/config_entry_pchk.json | 20 + tests/components/lcn/test_cover.py | 390 ++++++++++++++++++ 4 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 tests/components/lcn/test_cover.py diff --git a/.coveragerc b/.coveragerc index e378f0291cc..b87a91716cf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -616,7 +616,6 @@ omit = homeassistant/components/launch_library/sensor.py homeassistant/components/lcn/binary_sensor.py homeassistant/components/lcn/climate.py - homeassistant/components/lcn/cover.py homeassistant/components/lcn/helpers.py homeassistant/components/lcn/scene.py homeassistant/components/lcn/sensor.py diff --git a/tests/components/lcn/fixtures/config.json b/tests/components/lcn/fixtures/config.json index e9278d8e2cd..cc615b6083b 100644 --- a/tests/components/lcn/fixtures/config.json +++ b/tests/components/lcn/fixtures/config.json @@ -77,6 +77,19 @@ "address": "s0.g5", "output": "relay1" } + ], + "covers": [ + { + "name": "Cover_Ouputs", + "address": "s0.m7", + "motor": "outputs", + "reverse_time": "rt1200" + }, + { + "name": "Cover_Relays", + "address": "s0.m7", + "motor": "motor1" + } ] } } diff --git a/tests/components/lcn/fixtures/config_entry_pchk.json b/tests/components/lcn/fixtures/config_entry_pchk.json index 9d34add37ff..620bbb673f5 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk.json +++ b/tests/components/lcn/fixtures/config_entry_pchk.json @@ -100,6 +100,26 @@ "domain_data": { "output": "RELAY1" } + }, + { + "address": [0, 7, false], + "name": "Cover_Outputs", + "resource": "outputs", + "domain": "cover", + "domain_data": { + "motor": "OUTPUTS", + "reverse_time": "RT1200" + } + }, + { + "address": [0, 7, false], + "name": "Cover_Relays", + "resource": "motor1", + "domain": "cover", + "domain_data": { + "motor": "MOTOR1", + "reverse_time": "RT1200" + } } ] } diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py new file mode 100644 index 00000000000..f89cfa41071 --- /dev/null +++ b/tests/components/lcn/test_cover.py @@ -0,0 +1,390 @@ +"""Test for the LCN cover platform.""" +from unittest.mock import patch + +from pypck.inputs import ModStatusOutput, ModStatusRelays +from pypck.lcn_addr import LcnAddr +from pypck.lcn_defs import MotorReverseTime, MotorStateModifier + +from homeassistant.components.cover import DOMAIN as DOMAIN_COVER +from homeassistant.components.lcn.helpers import get_device_connection +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, +) + +from .conftest import MockModuleConnection + + +async def test_setup_lcn_cover(hass, entry, lcn_connection): + """Test the setup of cover.""" + for entity_id in ( + "cover.cover_outputs", + "cover.cover_relays", + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OPEN + + +async def test_entity_attributes(hass, entry, lcn_connection): + """Test the attributes of an entity.""" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entity_outputs = entity_registry.async_get("cover.cover_outputs") + + assert entity_outputs + assert entity_outputs.unique_id == f"{entry.entry_id}-m000007-outputs" + assert entity_outputs.original_name == "Cover_Outputs" + + entity_relays = entity_registry.async_get("cover.cover_relays") + + assert entity_relays + assert entity_relays.unique_id == f"{entry.entry_id}-m000007-motor1" + assert entity_relays.original_name == "Cover_Relays" + + +@patch.object(MockModuleConnection, "control_motors_outputs") +async def test_outputs_open(control_motors_outputs, hass, lcn_connection): + """Test the outputs cover opens.""" + state = hass.states.get("cover.cover_outputs") + state.state = STATE_CLOSED + + # command failed + control_motors_outputs.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.UP, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state != STATE_OPENING + + # command success + control_motors_outputs.reset_mock(return_value=True) + control_motors_outputs.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.UP, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_OPENING + + +@patch.object(MockModuleConnection, "control_motors_outputs") +async def test_outputs_close(control_motors_outputs, hass, lcn_connection): + """Test the outputs cover closes.""" + state = hass.states.get("cover.cover_outputs") + state.state = STATE_OPEN + + # command failed + control_motors_outputs.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.DOWN, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state != STATE_CLOSING + + # command success + control_motors_outputs.reset_mock(return_value=True) + control_motors_outputs.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with( + MotorStateModifier.DOWN, MotorReverseTime.RT1200 + ) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_CLOSING + + +@patch.object(MockModuleConnection, "control_motors_outputs") +async def test_outputs_stop(control_motors_outputs, hass, lcn_connection): + """Test the outputs cover stops.""" + state = hass.states.get("cover.cover_outputs") + state.state = STATE_CLOSING + + # command failed + control_motors_outputs.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_CLOSING + + # command success + control_motors_outputs.reset_mock(return_value=True) + control_motors_outputs.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_outputs"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state not in (STATE_CLOSING, STATE_OPENING) + + +@patch.object(MockModuleConnection, "control_motors_relays") +async def test_relays_open(control_motors_relays, hass, lcn_connection): + """Test the relays cover opens.""" + states = [MotorStateModifier.NOCHANGE] * 4 + states[0] = MotorStateModifier.UP + + state = hass.states.get("cover.cover_relays") + state.state = STATE_CLOSED + + # command failed + control_motors_relays.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state != STATE_OPENING + + # command success + control_motors_relays.reset_mock(return_value=True) + control_motors_relays.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_OPENING + + +@patch.object(MockModuleConnection, "control_motors_relays") +async def test_relays_close(control_motors_relays, hass, lcn_connection): + """Test the relays cover closes.""" + states = [MotorStateModifier.NOCHANGE] * 4 + states[0] = MotorStateModifier.DOWN + + state = hass.states.get("cover.cover_relays") + state.state = STATE_OPEN + + # command failed + control_motors_relays.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state != STATE_CLOSING + + # command success + control_motors_relays.reset_mock(return_value=True) + control_motors_relays.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_CLOSING + + +@patch.object(MockModuleConnection, "control_motors_relays") +async def test_relays_stop(control_motors_relays, hass, lcn_connection): + """Test the relays cover stops.""" + states = [MotorStateModifier.NOCHANGE] * 4 + states[0] = MotorStateModifier.STOP + + state = hass.states.get("cover.cover_relays") + state.state = STATE_CLOSING + + # command failed + control_motors_relays.return_value = False + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_CLOSING + + # command success + control_motors_relays.reset_mock(return_value=True) + control_motors_relays.return_value = True + + await hass.services.async_call( + DOMAIN_COVER, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.cover_relays"}, + blocking=True, + ) + await hass.async_block_till_done() + control_motors_relays.assert_awaited_with(states) + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state not in (STATE_CLOSING, STATE_OPENING) + + +async def test_pushed_outputs_status_change(hass, entry, lcn_connection): + """Test the outputs cover changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + + state = hass.states.get("cover.cover_outputs") + state.state = STATE_CLOSED + + # push status "open" + input = ModStatusOutput(address, 0, 100) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_OPENING + + # push status "stop" + input = ModStatusOutput(address, 0, 0) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state not in (STATE_OPENING, STATE_CLOSING) + + # push status "close" + input = ModStatusOutput(address, 1, 100) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_outputs") + assert state is not None + assert state.state == STATE_CLOSING + + +async def test_pushed_relays_status_change(hass, entry, lcn_connection): + """Test the relays cover changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + states = [False] * 8 + + state = hass.states.get("cover.cover_relays") + state.state = STATE_CLOSED + + # push status "open" + states[0:2] = [True, False] + input = ModStatusRelays(address, states) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_OPENING + + # push status "stop" + states[0] = False + input = ModStatusRelays(address, states) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state not in (STATE_OPENING, STATE_CLOSING) + + # push status "close" + states[0:2] = [True, True] + input = ModStatusRelays(address, states) + await device_connection.async_process_input(input) + await hass.async_block_till_done() + + state = hass.states.get("cover.cover_relays") + assert state is not None + assert state.state == STATE_CLOSING + + +async def test_unload_config_entry(hass, entry, lcn_connection): + """Test the cover is removed when the config entry is unloaded.""" + await hass.config_entries.async_unload(entry.entry_id) + assert hass.states.get("cover.cover_outputs").state == STATE_UNAVAILABLE + assert hass.states.get("cover.cover_relays").state == STATE_UNAVAILABLE From 0bd25a192d4eb352bd4e0b105a31ca423923b77d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 10:57:29 +0100 Subject: [PATCH 0043/1054] Move Phone Modem reject call deprecation warning (#67223) --- homeassistant/components/modem_callerid/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 3a1af4aa0a8..f4b2f3c3e44 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -44,13 +44,7 @@ async def async_setup_entry( ) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_REJECT_CALL, {}, "async_reject_call") - _LOGGER.warning( - "Calling reject_call service is deprecated and will be removed after 2022.4; " - "A new button entity is now available with the same function " - "and replaces the existing service" - ) class ModemCalleridSensor(SensorEntity): @@ -94,4 +88,9 @@ class ModemCalleridSensor(SensorEntity): async def async_reject_call(self) -> None: """Reject Incoming Call.""" + _LOGGER.warning( + "Calling reject_call service is deprecated and will be removed after 2022.4; " + "A new button entity is now available with the same function " + "and replaces the existing service" + ) await self.api.reject_call(self.device) From a0214d695aa79569e0bb0b4ead699e873ef1b94b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 25 Feb 2022 14:54:25 +0100 Subject: [PATCH 0044/1054] Clean up mysensors hass.data gateway access (#67233) --- homeassistant/components/mysensors/__init__.py | 9 +++------ homeassistant/components/mysensors/const.py | 2 -- homeassistant/components/mysensors/gateway.py | 11 ----------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 4d3c3046a89..dc4462b8d0e 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -43,7 +43,7 @@ from .const import ( SensorType, ) from .device import MySensorsDevice, get_mysensors_devices -from .gateway import finish_setup, get_mysensors_gateway, gw_stop, setup_gateway +from .gateway import finish_setup, gw_stop, setup_gateway from .helpers import on_unload _LOGGER = logging.getLogger(__name__) @@ -244,7 +244,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove an instance of the MySensors integration.""" - gateway = get_mysensors_gateway(hass, entry.entry_id) + gateway: BaseAsyncGateway = hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] unload_ok = await hass.config_entries.async_unload_platforms( entry, PLATFORMS_WITH_ENTRY_SUPPORT @@ -314,10 +314,7 @@ def setup_mysensors_platform( ) continue gateway_id, node_id, child_id, value_type = dev_id - gateway: BaseAsyncGateway | None = get_mysensors_gateway(hass, gateway_id) - if not gateway: - _LOGGER.warning("Skipping setup of %s, no gateway found", dev_id) - continue + gateway: BaseAsyncGateway = hass.data[DOMAIN][MYSENSORS_GATEWAYS][gateway_id] if isinstance(device_class, dict): child = gateway.sensors[node_id].children[child_id] diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index ad25e601bbd..5f3cb6aed96 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -62,8 +62,6 @@ ValueType = str GatewayId = str # a unique id generated by config_flow.py and stored in the ConfigEntry as the entry id. -# -# Gateway may be fetched by giving the gateway id to get_mysensors_gateway() DevId = tuple[GatewayId, int, int, int] # describes the backend of a hass entity. Contents are: GatewayId, node_id, child_id, v_type as int diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index be0381ab74e..7f13035f55c 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -37,7 +37,6 @@ from .const import ( CONF_VERSION, DOMAIN, MYSENSORS_GATEWAY_START_TASK, - MYSENSORS_GATEWAYS, ConfGatewayType, GatewayId, ) @@ -122,16 +121,6 @@ async def try_connect( return False -def get_mysensors_gateway( - hass: HomeAssistant, gateway_id: GatewayId -) -> BaseAsyncGateway | None: - """Return the Gateway for a given GatewayId.""" - if MYSENSORS_GATEWAYS not in hass.data[DOMAIN]: - hass.data[DOMAIN][MYSENSORS_GATEWAYS] = {} - gateways = hass.data[DOMAIN].get(MYSENSORS_GATEWAYS) - return gateways.get(gateway_id) - - async def setup_gateway( hass: HomeAssistant, entry: ConfigEntry ) -> BaseAsyncGateway | None: From 29350ee2e2a6be91238e41074715598377f2ba53 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 15:04:17 +0100 Subject: [PATCH 0045/1054] Remove deprecated Sensirion SHT31 integration (#67176) --- .coveragerc | 1 - homeassistant/components/sht31/__init__.py | 1 - homeassistant/components/sht31/manifest.json | 8 - homeassistant/components/sht31/sensor.py | 159 ------------------- requirements_all.txt | 6 - 5 files changed, 175 deletions(-) delete mode 100644 homeassistant/components/sht31/__init__.py delete mode 100644 homeassistant/components/sht31/manifest.json delete mode 100644 homeassistant/components/sht31/sensor.py diff --git a/.coveragerc b/.coveragerc index b87a91716cf..6e6eaf5637a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1047,7 +1047,6 @@ omit = homeassistant/components/shelly/number.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py - homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py diff --git a/homeassistant/components/sht31/__init__.py b/homeassistant/components/sht31/__init__.py deleted file mode 100644 index 16bfe384d94..00000000000 --- a/homeassistant/components/sht31/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sht31 component.""" diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json deleted file mode 100644 index c91d6a62768..00000000000 --- a/homeassistant/components/sht31/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "sht31", - "name": "Sensirion SHT31", - "documentation": "https://www.home-assistant.io/integrations/sht31", - "requirements": ["Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py deleted file mode 100644 index 4c9dc63606d..00000000000 --- a/homeassistant/components/sht31/sensor.py +++ /dev/null @@ -1,159 +0,0 @@ -"""Support for Sensirion SHT31 temperature and humidity sensor.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from datetime import timedelta -import logging -import math - -from Adafruit_SHT31 import SHT31 -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_ADDRESS = "i2c_address" - -DEFAULT_NAME = "SHT31" -DEFAULT_I2C_ADDRESS = 0x44 - - -@dataclass -class SHT31RequiredKeysMixin: - """Mixin for required keys.""" - - value_fn: Callable[[SHTClient], float | None] - - -@dataclass -class SHT31SensorEntityDescription(SensorEntityDescription, SHT31RequiredKeysMixin): - """Describes SHT31 sensor entity.""" - - -SENSOR_TYPES = ( - SHT31SensorEntityDescription( - key="temperature", - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, - value_fn=lambda sensor: sensor.temperature, - ), - SHT31SensorEntityDescription( - key="humidity", - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - native_unit_of_measurement=PERCENTAGE, - value_fn=lambda sensor: ( - round(val) # pylint: disable=undefined-variable - if (val := sensor.humidity) - else None - ), - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.All( - vol.Coerce(int), vol.Range(min=0x44, max=0x45) - ), - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sensor platform.""" - _LOGGER.warning( - "The Sensirion SHT31 integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config[CONF_NAME] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - i2c_address = config[CONF_I2C_ADDRESS] - sensor = SHT31(address=i2c_address) - - try: - if sensor.read_status() is None: - raise ValueError("CRC error while reading SHT31 status") - except (OSError, ValueError): - _LOGGER.error("SHT31 sensor not detected at address %s", hex(i2c_address)) - return - sensor_client = SHTClient(sensor) - - entities = [ - SHTSensor(sensor_client, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities) - - -class SHTClient: - """Get the latest data from the SHT sensor.""" - - def __init__(self, adafruit_sht): - """Initialize the sensor.""" - self.adafruit_sht = adafruit_sht - self.temperature: float | None = None - self.humidity: float | None = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the SHT sensor.""" - temperature, humidity = self.adafruit_sht.read_temperature_humidity() - if math.isnan(temperature) or math.isnan(humidity): - _LOGGER.warning("Bad sample from sensor SHT31") - return - self.temperature = temperature - self.humidity = humidity - - -class SHTSensor(SensorEntity): - """An abstract SHTSensor, can be either temperature or humidity.""" - - entity_description: SHT31SensorEntityDescription - - def __init__(self, sensor, name, description: SHT31SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self._sensor = sensor - - self._attr_name = f"{name} {description.name}" - - def update(self): - """Fetch temperature and humidity from the sensor.""" - self._sensor.update() - self._attr_native_value = self.entity_description.value_fn(self._sensor) diff --git a/requirements_all.txt b/requirements_all.txt index 916b320acca..866d4f8ce1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,12 +4,6 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 -# homeassistant.components.sht31 -Adafruit-GPIO==1.0.3 - -# homeassistant.components.sht31 -Adafruit-SHT31==1.0.2 - # homeassistant.components.adax Adax-local==0.1.3 From 3173f1672bf2f02816aed1cbf6b1f377c90f2829 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 25 Feb 2022 17:01:03 +0100 Subject: [PATCH 0046/1054] Remove deprecated reverse_order and data_count from Modbus (#67236) --- homeassistant/components/modbus/__init__.py | 5 ----- homeassistant/components/modbus/const.py | 2 -- 2 files changed, 7 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a5ad05a4711..d6fe41f66fc 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -60,7 +60,6 @@ from .const import ( CONF_BYTESIZE, CONF_CLIMATES, CONF_CLOSE_COMM_ON_ERROR, - CONF_DATA_COUNT, CONF_DATA_TYPE, CONF_FANS, CONF_INPUT_TYPE, @@ -72,7 +71,6 @@ from .const import ( CONF_PRECISION, CONF_RETRIES, CONF_RETRY_ON_EMPTY, - CONF_REVERSE_ORDER, CONF_SCALE, CONF_SLAVE_COUNT, CONF_STATE_CLOSED, @@ -210,7 +208,6 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend( CLIMATE_SCHEMA = vol.All( - cv.deprecated(CONF_DATA_COUNT, replacement_key=CONF_COUNT), BASE_STRUCT_SCHEMA.extend( { vol.Required(CONF_TARGET_TEMP): cv.positive_int, @@ -254,13 +251,11 @@ LIGHT_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) FAN_SCHEMA = BASE_SWITCH_SCHEMA.extend({}) SENSOR_SCHEMA = vol.All( - cv.deprecated(CONF_REVERSE_ORDER), BASE_STRUCT_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_REVERSE_ORDER): cv.boolean, } ), ) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 82c5245da80..62929505acc 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -18,7 +18,6 @@ CONF_CLOSE_COMM_ON_ERROR = "close_comm_on_error" CONF_COILS = "coils" CONF_CURRENT_TEMP = "current_temp_register" CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" -CONF_DATA_COUNT = "data_count" CONF_DATA_TYPE = "data_type" CONF_FANS = "fans" CONF_HUB = "hub" @@ -34,7 +33,6 @@ CONF_REGISTER_TYPE = "register_type" CONF_REGISTERS = "registers" CONF_RETRIES = "retries" CONF_RETRY_ON_EMPTY = "retry_on_empty" -CONF_REVERSE_ORDER = "reverse_order" CONF_PRECISION = "precision" CONF_SCALE = "scale" CONF_SLAVE_COUNT = "slave_count" From cd186413852393bfee020061049dd7c984dea6cd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 17:01:27 +0100 Subject: [PATCH 0047/1054] Remove deprecated Media Player Classic Home Cinema (MPC-HC) integration (#67189) --- .coveragerc | 1 - homeassistant/components/mpchc/__init__.py | 1 - homeassistant/components/mpchc/manifest.json | 7 - .../components/mpchc/media_player.py | 199 ------------------ 4 files changed, 208 deletions(-) delete mode 100644 homeassistant/components/mpchc/__init__.py delete mode 100644 homeassistant/components/mpchc/manifest.json delete mode 100644 homeassistant/components/mpchc/media_player.py diff --git a/.coveragerc b/.coveragerc index 6e6eaf5637a..68679fe5519 100644 --- a/.coveragerc +++ b/.coveragerc @@ -722,7 +722,6 @@ omit = homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/sensor.py - homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py homeassistant/components/msteams/notify.py diff --git a/homeassistant/components/mpchc/__init__.py b/homeassistant/components/mpchc/__init__.py deleted file mode 100644 index e8a0057a9b6..00000000000 --- a/homeassistant/components/mpchc/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The mpchc component.""" diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json deleted file mode 100644 index a1a9e769be6..00000000000 --- a/homeassistant/components/mpchc/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "mpchc", - "name": "Media Player Classic Home Cinema (MPC-HC)", - "documentation": "https://www.home-assistant.io/integrations/mpchc", - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py deleted file mode 100644 index c2d92da7eac..00000000000 --- a/homeassistant/components/mpchc/media_player.py +++ /dev/null @@ -1,199 +0,0 @@ -"""Support to interface with the MPC-HC Web API.""" -from __future__ import annotations - -import logging -import re - -import requests -import voluptuous as vol - -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity -from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "MPC-HC" -DEFAULT_PORT = 13579 - -SUPPORT_MPCHC = ( - SUPPORT_VOLUME_MUTE - | SUPPORT_PAUSE - | SUPPORT_STOP - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_VOLUME_STEP - | SUPPORT_PLAY -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MPC-HC platform.""" - _LOGGER.warning( - "The Media Player Classic Home Cinema integration is now deprecated " - "and will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0004, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" - ) - - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - - url = f"{host}:{port}" - - add_entities([MpcHcDevice(name, url)], True) - - -class MpcHcDevice(MediaPlayerEntity): - """Representation of a MPC-HC server.""" - - def __init__(self, name, url): - """Initialize the MPC-HC device.""" - self._name = name - self._url = url - self._player_variables = {} - self._available = False - - def update(self): - """Get the latest details.""" - try: - response = requests.get(f"{self._url}/variables.html", data=None, timeout=3) - - mpchc_variables = re.findall(r'

(.+?)

', response.text) - - for var in mpchc_variables: - self._player_variables[var[0]] = var[1].lower() - self._available = True - except requests.exceptions.RequestException: - if self.available: - _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) - - self._player_variables = {} - self._available = False - - def _send_command(self, command_id): - """Send a command to MPC-HC via its window message ID.""" - try: - params = {"wm_command": command_id} - requests.get(f"{self._url}/command.html", params=params, timeout=3) - except requests.exceptions.RequestException: - _LOGGER.error( - "Could not send command %d to MPC-HC at: %s", command_id, self._url - ) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - state = self._player_variables.get("statestring", None) - - if state is None: - return STATE_OFF - if state == "playing": - return STATE_PLAYING - if state == "paused": - return STATE_PAUSED - - return STATE_IDLE - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def media_title(self): - """Return the title of current playing media.""" - return self._player_variables.get("file", None) - - @property - def volume_level(self): - """Return the volume level of the media player (0..1).""" - return int(self._player_variables.get("volumelevel", 0)) / 100.0 - - @property - def is_volume_muted(self): - """Return boolean if volume is currently muted.""" - return self._player_variables.get("muted", "0") == "1" - - @property - def media_duration(self): - """Return the duration of the current playing media in seconds.""" - duration = self._player_variables.get("durationstring", "00:00:00").split(":") - return int(duration[0]) * 3600 + int(duration[1]) * 60 + int(duration[2]) - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_MPCHC - - def volume_up(self): - """Volume up the media player.""" - self._send_command(907) - - def volume_down(self): - """Volume down media player.""" - self._send_command(908) - - def mute_volume(self, mute): - """Mute the volume.""" - self._send_command(909) - - def media_play(self): - """Send play command.""" - self._send_command(887) - - def media_pause(self): - """Send pause command.""" - self._send_command(888) - - def media_stop(self): - """Send stop command.""" - self._send_command(890) - - def media_next_track(self): - """Send next track command.""" - self._send_command(920) - - def media_previous_track(self): - """Send previous track command.""" - self._send_command(919) From 5c4b149f50c8a9ff9b35605ed64a4878ed64f920 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 17:03:32 +0100 Subject: [PATCH 0048/1054] Remove deprecated Piglow integration (#67227) --- .coveragerc | 1 - homeassistant/components/piglow/__init__.py | 1 - homeassistant/components/piglow/light.py | 129 ------------------ homeassistant/components/piglow/manifest.json | 8 -- requirements_all.txt | 3 - 5 files changed, 142 deletions(-) delete mode 100644 homeassistant/components/piglow/__init__.py delete mode 100644 homeassistant/components/piglow/light.py delete mode 100644 homeassistant/components/piglow/manifest.json diff --git a/.coveragerc b/.coveragerc index 68679fe5519..38d2197701d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -898,7 +898,6 @@ omit = homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py homeassistant/components/pi4ioe5v9xxxx/switch.py homeassistant/components/picotts/tts.py - homeassistant/components/piglow/light.py homeassistant/components/pilight/* homeassistant/components/ping/__init__.py homeassistant/components/ping/const.py diff --git a/homeassistant/components/piglow/__init__.py b/homeassistant/components/piglow/__init__.py deleted file mode 100644 index e6d4bbd3ec2..00000000000 --- a/homeassistant/components/piglow/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The piglow component.""" diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py deleted file mode 100644 index f66121ec413..00000000000 --- a/homeassistant/components/piglow/light.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Support for Piglow LED's.""" -from __future__ import annotations - -import logging -import subprocess - -import piglow -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - LightEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_PIGLOW = SUPPORT_BRIGHTNESS | SUPPORT_COLOR - -DEFAULT_NAME = "Piglow" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Piglow Light platform.""" - _LOGGER.warning( - "The Piglow integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != "54": - _LOGGER.error("A Piglow device was not found") - return - - name = config.get(CONF_NAME) - - add_entities([PiglowLight(name)]) - - -class PiglowLight(LightEntity): - """Representation of an Piglow Light.""" - - def __init__(self, name): - """Initialize an PiglowLight.""" - self._name = name - self._is_on = False - self._brightness = 255 - self._hs_color = [0, 0] - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Read back the color of the light.""" - return self._hs_color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_PIGLOW - - @property - def should_poll(self): - """Return if we should poll this device.""" - return False - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return True - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - def turn_on(self, **kwargs): - """Instruct the light to turn on.""" - piglow.clear() - - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_HS_COLOR in kwargs: - self._hs_color = kwargs[ATTR_HS_COLOR] - - rgb = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 - ) - piglow.red(rgb[0]) - piglow.green(rgb[1]) - piglow.blue(rgb[2]) - piglow.show() - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - piglow.clear() - piglow.show() - self._is_on = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json deleted file mode 100644 index f4b869aacf8..00000000000 --- a/homeassistant/components/piglow/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "piglow", - "name": "Piglow", - "documentation": "https://www.home-assistant.io/integrations/piglow", - "requirements": ["piglow==1.2.4"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/requirements_all.txt b/requirements_all.txt index 866d4f8ce1e..a07faa1e3d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1217,9 +1217,6 @@ pifacecommon==4.2.2 # homeassistant.components.rpi_pfio pifacedigitalio==3.0.5 -# homeassistant.components.piglow -piglow==1.2.4 - # homeassistant.components.pilight pilight==0.1.1 From adc4c1e33f06ede18cc94077d20526683d71246e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 25 Feb 2022 17:05:19 +0100 Subject: [PATCH 0049/1054] Secure ATTR_ and CONF_ use identical texts in Modbus (#66901) --- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/const.py | 7 +++---- homeassistant/components/modbus/modbus.py | 2 +- tests/components/modbus/test_init.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 37ff9504b35..f2e5035e40c 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -11,6 +11,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_NAME, CONF_TEMPERATURE_UNIT, PRECISION_TENTHS, @@ -26,7 +27,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import get_hub from .base_platform import BaseStructPlatform from .const import ( - ATTR_TEMPERATURE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 62929505acc..7244e2a16e6 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -2,6 +2,7 @@ from enum import Enum from homeassistant.const import ( + CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COVERS, CONF_LIGHTS, @@ -64,13 +65,11 @@ UDP = "udp" # service call attributes -ATTR_ADDRESS = "address" -ATTR_HUB = "hub" +ATTR_ADDRESS = CONF_ADDRESS +ATTR_HUB = CONF_HUB ATTR_UNIT = "unit" ATTR_SLAVE = "slave" ATTR_VALUE = "value" -ATTR_STATE = "state" -ATTR_TEMPERATURE = "temperature" class DataType(str, Enum): diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 9d73f0afcc3..c980eab2b34 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -20,6 +20,7 @@ from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( + ATTR_STATE, CONF_DELAY, CONF_HOST, CONF_METHOD, @@ -40,7 +41,6 @@ from .const import ( ATTR_ADDRESS, ATTR_HUB, ATTR_SLAVE, - ATTR_STATE, ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index ba5a9291d81..06fbcafef07 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -26,7 +26,6 @@ from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, ATTR_SLAVE, - ATTR_STATE, ATTR_UNIT, ATTR_VALUE, CALL_TYPE_COIL, @@ -67,6 +66,7 @@ from homeassistant.components.modbus.validators import ( ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + ATTR_STATE, CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COUNT, From c856f673fb88916a6951caa0a32e9329581a43c2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 25 Feb 2022 17:05:56 +0100 Subject: [PATCH 0050/1054] Fix zwave_js migration luminance sensor (#67234) --- homeassistant/components/zwave_js/migrate.py | 7 ++-- tests/components/zwave_js/test_migrate.py | 36 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 73a094fd95a..204b5d0aebd 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -9,7 +9,7 @@ from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import LIGHT_LUX, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import ( DeviceEntry, @@ -91,6 +91,8 @@ CC_ID_LABEL_TO_PROPERTY = { 113: NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME, } +UNIT_LEGACY_MIGRATION_MAP = {LIGHT_LUX: "Lux"} + class ZWaveMigrationData(TypedDict): """Represent the Z-Wave migration data dict.""" @@ -209,7 +211,8 @@ class LegacyZWaveMigration: # Normalize unit of measurement. if unit := entity_entry.unit_of_measurement: - unit = unit.lower() + _unit = UNIT_LEGACY_MIGRATION_MAP.get(unit, unit) + unit = _unit.lower() if unit == "": unit = None diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index 3479638b387..95f969a9586 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -8,6 +8,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.zwave_js.api import ENTRY_ID, ID, TYPE from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.const import LIGHT_LUX from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR @@ -33,6 +34,10 @@ ZWAVE_MULTISENSOR_DEVICE_NAME = "Z-Wave Multisensor Device" ZWAVE_MULTISENSOR_DEVICE_AREA = "Z-Wave Multisensor Area" ZWAVE_SOURCE_NODE_ENTITY = "sensor.zwave_source_node" ZWAVE_SOURCE_NODE_UNIQUE_ID = "52-4321" +ZWAVE_LUMINANCE_ENTITY = "sensor.zwave_luminance" +ZWAVE_LUMINANCE_UNIQUE_ID = "52-6543" +ZWAVE_LUMINANCE_NAME = "Z-Wave Luminance" +ZWAVE_LUMINANCE_ICON = "mdi:zwave-test-luminance" ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level" ZWAVE_BATTERY_UNIQUE_ID = "52-1234" ZWAVE_BATTERY_NAME = "Z-Wave Battery Level" @@ -69,6 +74,14 @@ def zwave_migration_data_fixture(hass): platform="zwave", name="Z-Wave Source Node", ) + zwave_luminance_entry = er.RegistryEntry( + entity_id=ZWAVE_LUMINANCE_ENTITY, + unique_id=ZWAVE_LUMINANCE_UNIQUE_ID, + platform="zwave", + name=ZWAVE_LUMINANCE_NAME, + icon=ZWAVE_LUMINANCE_ICON, + unit_of_measurement="lux", + ) zwave_battery_entry = er.RegistryEntry( entity_id=ZWAVE_BATTERY_ENTITY, unique_id=ZWAVE_BATTERY_UNIQUE_ID, @@ -131,6 +144,18 @@ def zwave_migration_data_fixture(hass): "unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID, "unit_of_measurement": zwave_source_node_entry.unit_of_measurement, }, + ZWAVE_LUMINANCE_ENTITY: { + "node_id": 52, + "node_instance": 1, + "command_class": 49, + "command_class_label": "Luminance", + "value_index": 3, + "device_id": zwave_multisensor_device.id, + "domain": zwave_luminance_entry.domain, + "entity_id": zwave_luminance_entry.entity_id, + "unique_id": ZWAVE_LUMINANCE_UNIQUE_ID, + "unit_of_measurement": zwave_luminance_entry.unit_of_measurement, + }, ZWAVE_BATTERY_ENTITY: { "node_id": 52, "node_instance": 1, @@ -169,6 +194,7 @@ def zwave_migration_data_fixture(hass): { ZWAVE_SWITCH_ENTITY: zwave_switch_entry, ZWAVE_SOURCE_NODE_ENTITY: zwave_source_node_entry, + ZWAVE_LUMINANCE_ENTITY: zwave_luminance_entry, ZWAVE_BATTERY_ENTITY: zwave_battery_entry, ZWAVE_POWER_ENTITY: zwave_power_entry, ZWAVE_TAMPERING_ENTITY: zwave_tampering_entry, @@ -218,6 +244,7 @@ async def test_migrate_zwave( migration_entity_map = { ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6", + ZWAVE_LUMINANCE_ENTITY: "sensor.multisensor_6_illuminance", ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level", } @@ -225,6 +252,7 @@ async def test_migrate_zwave( ZWAVE_SWITCH_ENTITY, ZWAVE_POWER_ENTITY, ZWAVE_SOURCE_NODE_ENTITY, + ZWAVE_LUMINANCE_ENTITY, ZWAVE_BATTERY_ENTITY, ZWAVE_TAMPERING_ENTITY, ] @@ -279,6 +307,7 @@ async def test_migrate_zwave( # this should have been migrated and no longer present under that id assert not ent_reg.async_is_registered("sensor.multisensor_6_battery_level") + assert not ent_reg.async_is_registered("sensor.multisensor_6_illuminance") # these should not have been migrated and is still in the registry assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY) @@ -295,6 +324,7 @@ async def test_migrate_zwave( # this is the new entity_ids of the zwave_js entities assert ent_reg.async_is_registered(ZWAVE_SWITCH_ENTITY) assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + assert ent_reg.async_is_registered(ZWAVE_LUMINANCE_ENTITY) # check that the migrated entries have correct attributes switch_entry = ent_reg.async_get(ZWAVE_SWITCH_ENTITY) @@ -307,6 +337,12 @@ async def test_migrate_zwave( assert battery_entry.unique_id == "3245146787.52-128-0-level" assert battery_entry.name == ZWAVE_BATTERY_NAME assert battery_entry.icon == ZWAVE_BATTERY_ICON + luminance_entry = ent_reg.async_get(ZWAVE_LUMINANCE_ENTITY) + assert luminance_entry + assert luminance_entry.unique_id == "3245146787.52-49-0-Illuminance" + assert luminance_entry.name == ZWAVE_LUMINANCE_NAME + assert luminance_entry.icon == ZWAVE_LUMINANCE_ICON + assert luminance_entry.unit_of_measurement == LIGHT_LUX # check that the zwave config entry has been removed assert not hass.config_entries.async_entries("zwave") From c6f5633e2494a8a6fc81b0d08bbe5da4244921eb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 25 Feb 2022 17:06:25 +0100 Subject: [PATCH 0051/1054] Fix modbus test_delay (#66993) --- tests/components/modbus/test_init.py | 45 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 06fbcafef07..8b0f3952fdc 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -711,34 +711,33 @@ async def test_delay(hass, mock_pymodbus): ] } mock_pymodbus.read_coils.return_value = ReadResult([0x01]) - now = dt_util.utcnow() - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + start_time = dt_util.utcnow() + with mock.patch( + "homeassistant.helpers.event.dt_util.utcnow", return_value=start_time + ): assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNKNOWN - # pass first scan_interval - start_time = now - now = now + timedelta(seconds=(set_scan_interval + 1)) - with mock.patch( - "homeassistant.helpers.event.dt_util.utcnow", return_value=now, autospec=True - ): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - stop_time = start_time + timedelta(seconds=(set_delay + 1)) - step_timedelta = timedelta(seconds=1) - while now < stop_time: - now = now + step_timedelta - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + time_sensor_active = start_time + timedelta(seconds=2) + time_after_delay = start_time + timedelta(seconds=(set_delay)) + time_after_scan = start_time + timedelta(seconds=(set_delay + set_scan_interval)) + time_stop = time_after_scan + timedelta(seconds=10) + now = start_time + while now < time_stop: + now += timedelta(seconds=1) + with mock.patch( + "homeassistant.helpers.event.dt_util.utcnow", + return_value=now, + autospec=True, + ): async_fire_time_changed(hass, now) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - now = now + step_timedelta + timedelta(seconds=2) - with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - async_fire_time_changed(hass, now) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON + if now > time_sensor_active: + if now <= time_after_delay: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + elif now > time_after_scan: + assert hass.states.get(entity_id).state == STATE_ON @pytest.mark.parametrize( From e7e8ee5ff3aee0f2ddac0a45fcbb16f7fe6a4e92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 17:09:23 +0100 Subject: [PATCH 0052/1054] Remove deprecated pigpio Daemon PWM LED integration (#67187) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/rpi_gpio_pwm/__init__.py | 1 - .../components/rpi_gpio_pwm/light.py | 259 ------------------ .../components/rpi_gpio_pwm/manifest.json | 9 - requirements_all.txt | 3 - 6 files changed, 274 deletions(-) delete mode 100644 homeassistant/components/rpi_gpio_pwm/__init__.py delete mode 100644 homeassistant/components/rpi_gpio_pwm/light.py delete mode 100644 homeassistant/components/rpi_gpio_pwm/manifest.json diff --git a/.coveragerc b/.coveragerc index 38d2197701d..7116cab0ceb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -993,7 +993,6 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* - homeassistant/components/rpi_gpio_pwm/light.py homeassistant/components/rpi_pfio/* homeassistant/components/rpi_rf/switch.py homeassistant/components/rtorrent/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 643c287e04a..574d559cfa4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -797,7 +797,6 @@ homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn tests/components/roomba/* @pschmitt @cyr-ius @shenxn homeassistant/components/roon/* @pavoni tests/components/roon/* @pavoni -homeassistant/components/rpi_gpio_pwm/* @soldag homeassistant/components/rpi_power/* @shenxn @swetoast tests/components/rpi_power/* @shenxn @swetoast homeassistant/components/rtsp_to_webrtc/* @allenporter diff --git a/homeassistant/components/rpi_gpio_pwm/__init__.py b/homeassistant/components/rpi_gpio_pwm/__init__.py deleted file mode 100644 index 46aa24c12e6..00000000000 --- a/homeassistant/components/rpi_gpio_pwm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The rpi_gpio_pwm component.""" diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py deleted file mode 100644 index 02444f902b4..00000000000 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Support for LED lights that can be controlled using PWM.""" -from __future__ import annotations - -import logging - -from pwmled import Color -from pwmled.driver.gpio import GpioDriver -from pwmled.driver.pca9685 import Pca9685Driver -from pwmled.led import SimpleLed -from pwmled.led.rgb import RgbLed -from pwmled.led.rgbw import RgbwLed -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_TRANSITION, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_TYPE, STATE_ON -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -_LOGGER = logging.getLogger(__name__) - -CONF_LEDS = "leds" -CONF_DRIVER = "driver" -CONF_PINS = "pins" -CONF_FREQUENCY = "frequency" - -CONF_DRIVER_GPIO = "gpio" -CONF_DRIVER_PCA9685 = "pca9685" -CONF_DRIVER_TYPES = [CONF_DRIVER_GPIO, CONF_DRIVER_PCA9685] - -CONF_LED_TYPE_SIMPLE = "simple" -CONF_LED_TYPE_RGB = "rgb" -CONF_LED_TYPE_RGBW = "rgbw" -CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] - -DEFAULT_BRIGHTNESS = 255 -DEFAULT_COLOR = [0, 0] - -SUPPORT_SIMPLE_LED = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION -SUPPORT_RGB_LED = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_LEDS): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), - vol.Required(CONF_PINS): vol.All(cv.ensure_list, [cv.positive_int]), - vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES), - vol.Optional(CONF_FREQUENCY): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.byte, - vol.Optional(CONF_HOST): cv.string, - } - ], - ) - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PWM LED lights.""" - _LOGGER.warning( - "The pigpio Daemon PWM LED integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - leds = [] - for led_conf in config[CONF_LEDS]: - driver_type = led_conf[CONF_DRIVER] - pins = led_conf[CONF_PINS] - opt_args = {} - if CONF_FREQUENCY in led_conf: - opt_args["freq"] = led_conf[CONF_FREQUENCY] - if driver_type == CONF_DRIVER_GPIO: - if CONF_HOST in led_conf: - opt_args["host"] = led_conf[CONF_HOST] - driver = GpioDriver(pins, **opt_args) - elif driver_type == CONF_DRIVER_PCA9685: - if CONF_ADDRESS in led_conf: - opt_args["address"] = led_conf[CONF_ADDRESS] - driver = Pca9685Driver(pins, **opt_args) - else: - _LOGGER.error("Invalid driver type") - return - - name = led_conf[CONF_NAME] - led_type = led_conf[CONF_TYPE] - if led_type == CONF_LED_TYPE_SIMPLE: - led = PwmSimpleLed(SimpleLed(driver), name) - elif led_type == CONF_LED_TYPE_RGB: - led = PwmRgbLed(RgbLed(driver), name) - elif led_type == CONF_LED_TYPE_RGBW: - led = PwmRgbLed(RgbwLed(driver), name) - else: - _LOGGER.error("Invalid led type") - return - leds.append(led) - - add_entities(leds) - - -class PwmSimpleLed(LightEntity, RestoreEntity): - """Representation of a simple one-color PWM LED.""" - - def __init__(self, led, name): - """Initialize one-color PWM LED.""" - self._led = led - self._name = name - self._is_on = False - self._brightness = DEFAULT_BRIGHTNESS - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - if last_state := await self.async_get_last_state(): - self._is_on = last_state.state == STATE_ON - self._brightness = last_state.attributes.get( - "brightness", DEFAULT_BRIGHTNESS - ) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the group.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._is_on - - @property - def brightness(self): - """Return the brightness property.""" - return self._brightness - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_SIMPLE_LED - - def turn_on(self, **kwargs): - """Turn on a led.""" - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] - self._led.transition( - transition_time, - is_on=True, - brightness=_from_hass_brightness(self._brightness), - ) - else: - self._led.set( - is_on=True, brightness=_from_hass_brightness(self._brightness) - ) - - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn off a LED.""" - if self.is_on: - if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] - self._led.transition(transition_time, is_on=False) - else: - self._led.off() - - self._is_on = False - self.schedule_update_ha_state() - - -class PwmRgbLed(PwmSimpleLed): - """Representation of a RGB(W) PWM LED.""" - - def __init__(self, led, name): - """Initialize a RGB(W) PWM LED.""" - super().__init__(led, name) - self._color = DEFAULT_COLOR - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - if last_state := await self.async_get_last_state(): - self._color = last_state.attributes.get("hs_color", DEFAULT_COLOR) - - @property - def hs_color(self): - """Return the color property.""" - return self._color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_RGB_LED - - def turn_on(self, **kwargs): - """Turn on a LED.""" - if ATTR_HS_COLOR in kwargs: - self._color = kwargs[ATTR_HS_COLOR] - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] - self._led.transition( - transition_time, - is_on=True, - brightness=_from_hass_brightness(self._brightness), - color=_from_hass_color(self._color), - ) - else: - self._led.set( - is_on=True, - brightness=_from_hass_brightness(self._brightness), - color=_from_hass_color(self._color), - ) - - self._is_on = True - self.schedule_update_ha_state() - - -def _from_hass_brightness(brightness): - """Convert Home Assistant brightness units to percentage.""" - return brightness / 255 - - -def _from_hass_color(color): - """Convert Home Assistant RGB list to Color tuple.""" - - rgb = color_util.color_hs_to_RGB(*color) - return Color(*tuple(rgb)) diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json deleted file mode 100644 index 403f607e379..00000000000 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "rpi_gpio_pwm", - "name": "pigpio Daemon PWM LED", - "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", - "requirements": ["pwmled==1.6.10"], - "codeowners": ["@soldag"], - "iot_class": "local_push", - "loggers": ["adafruit_blinka", "adafruit_circuitpython_pca9685", "pwmled"] -} diff --git a/requirements_all.txt b/requirements_all.txt index a07faa1e3d6..3b3ebfccd5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1289,9 +1289,6 @@ pushover_complete==1.1.1 # homeassistant.components.pvoutput pvo==0.2.2 -# homeassistant.components.rpi_gpio_pwm -pwmled==1.6.10 - # homeassistant.components.canary py-canary==0.5.1 From 5edb4cbdc6bdebc15efb1e5547fbc9c69c85fb25 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 17:10:12 +0100 Subject: [PATCH 0053/1054] Remove deprecated pi4ioe5v9xxxx IO Expander integration (#67188) --- .coveragerc | 2 - CODEOWNERS | 1 - .../components/pi4ioe5v9xxxx/__init__.py | 1 - .../components/pi4ioe5v9xxxx/binary_sensor.py | 97 ---------------- .../components/pi4ioe5v9xxxx/manifest.json | 9 -- .../components/pi4ioe5v9xxxx/switch.py | 108 ------------------ requirements_all.txt | 3 - 7 files changed, 221 deletions(-) delete mode 100644 homeassistant/components/pi4ioe5v9xxxx/__init__.py delete mode 100644 homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py delete mode 100644 homeassistant/components/pi4ioe5v9xxxx/manifest.json delete mode 100644 homeassistant/components/pi4ioe5v9xxxx/switch.py diff --git a/.coveragerc b/.coveragerc index 7116cab0ceb..d3c08769095 100644 --- a/.coveragerc +++ b/.coveragerc @@ -895,8 +895,6 @@ omit = homeassistant/components/philips_js/remote.py homeassistant/components/philips_js/switch.py homeassistant/components/pi_hole/sensor.py - homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py - homeassistant/components/pi4ioe5v9xxxx/switch.py homeassistant/components/picotts/tts.py homeassistant/components/pilight/* homeassistant/components/ping/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 574d559cfa4..ef6ef0271d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -707,7 +707,6 @@ homeassistant/components/persistent_notification/* @home-assistant/core tests/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus tests/components/philips_js/* @elupus -homeassistant/components/pi4ioe5v9xxxx/* @antonverburg homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn tests/components/pi_hole/* @fabaff @johnluetke @shenxn homeassistant/components/picnic/* @corneyl diff --git a/homeassistant/components/pi4ioe5v9xxxx/__init__.py b/homeassistant/components/pi4ioe5v9xxxx/__init__.py deleted file mode 100644 index 516cfc32575..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Support for controlling IO expanders from Digital.com (PI4IOE5V9570, PI4IOE5V9674, PI4IOE5V9673, PI4IOE5V96224, PI4IOE5V96248).""" diff --git a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py b/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py deleted file mode 100644 index c1a82c3eb5a..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Support for binary sensor using RPi GPIO.""" -from __future__ import annotations - -import logging - -from pi4ioe5v9xxxx import pi4ioe5v9xxxx -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_PINS = "pins" -CONF_I2CBUS = "i2c_bus" -CONF_I2CADDR = "i2c_address" -CONF_BITS = "bits" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_BITS = 24 -DEFAULT_BUS = 1 -DEFAULT_ADDR = 0x20 - - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SENSORS_SCHEMA, - vol.Optional(CONF_I2CBUS, default=DEFAULT_BUS): cv.positive_int, - vol.Optional(CONF_I2CADDR, default=DEFAULT_ADDR): cv.positive_int, - vol.Optional(CONF_BITS, default=DEFAULT_BITS): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the IO expander devices.""" - _LOGGER.warning( - "The pi4ioe5v9xxxx IO Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pins = config[CONF_PINS] - binary_sensors = [] - - pi4ioe5v9xxxx.setup( - i2c_bus=config[CONF_I2CBUS], - i2c_addr=config[CONF_I2CADDR], - bits=config[CONF_BITS], - read_mode=True, - invert=False, - ) - for pin_num, pin_name in pins.items(): - binary_sensors.append( - Pi4ioe5v9BinarySensor(pin_name, pin_num, config[CONF_INVERT_LOGIC]) - ) - add_entities(binary_sensors, True) - - -class Pi4ioe5v9BinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses pi4ioe5v9xxxx IO expander in read mode.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pi4ioe5v9xxxx sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._invert_logic = invert_logic - self._state = pi4ioe5v9xxxx.pin_from_memory(self._pin) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update the IO state.""" - pi4ioe5v9xxxx.hw_to_memory() - self._state = pi4ioe5v9xxxx.pin_from_memory(self._pin) diff --git a/homeassistant/components/pi4ioe5v9xxxx/manifest.json b/homeassistant/components/pi4ioe5v9xxxx/manifest.json deleted file mode 100644 index 3ea322a6c63..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "pi4ioe5v9xxxx", - "name": "pi4ioe5v9xxxx IO Expander", - "documentation": "https://www.home-assistant.io/integrations/pi4ioe5v9xxxx", - "requirements": ["pi4ioe5v9xxxx==0.0.2"], - "codeowners": ["@antonverburg"], - "iot_class": "local_polling", - "loggers": ["pi4ioe5v9xxxx", "smbus2"] -} diff --git a/homeassistant/components/pi4ioe5v9xxxx/switch.py b/homeassistant/components/pi4ioe5v9xxxx/switch.py deleted file mode 100644 index 28a963629e9..00000000000 --- a/homeassistant/components/pi4ioe5v9xxxx/switch.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Allows to configure a switch using RPi GPIO.""" -from __future__ import annotations - -import logging - -from pi4ioe5v9xxxx import pi4ioe5v9xxxx -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_PINS = "pins" -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2CBUS = "i2c_bus" -CONF_I2CADDR = "i2c_address" -CONF_BITS = "bits" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_BITS = 24 -DEFAULT_BUS = 1 -DEFAULT_ADDR = 0x20 - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SWITCHES_SCHEMA, - vol.Optional(CONF_I2CBUS, default=DEFAULT_BUS): cv.positive_int, - vol.Optional(CONF_I2CADDR, default=DEFAULT_ADDR): cv.positive_int, - vol.Optional(CONF_BITS, default=DEFAULT_BITS): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the swiches devices.""" - _LOGGER.warning( - "The pi4ioe5v9xxxx IO Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pins = config[CONF_PINS] - switches = [] - - pi4ioe5v9xxxx.setup( - i2c_bus=config[CONF_I2CBUS], - i2c_addr=config[CONF_I2CADDR], - bits=config[CONF_BITS], - read_mode=False, - invert=False, - ) - for pin, name in pins.items(): - switches.append(Pi4ioe5v9Switch(name, pin, config[CONF_INVERT_LOGIC])) - add_entities(switches) - - -class Pi4ioe5v9Switch(SwitchEntity): - """Representation of a pi4ioe5v9 IO expansion IO.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._invert_logic = invert_logic - self._state = not invert_logic - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn the device on.""" - pi4ioe5v9xxxx.pin_to_memory(self._pin, not self._invert_logic) - pi4ioe5v9xxxx.memory_to_hw() - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - pi4ioe5v9xxxx.pin_to_memory(self._pin, self._invert_logic) - pi4ioe5v9xxxx.memory_to_hw() - self._state = False - self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 3b3ebfccd5a..7f0a7653844 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1208,9 +1208,6 @@ phone_modem==0.1.1 # homeassistant.components.onewire pi1wire==0.1.0 -# homeassistant.components.pi4ioe5v9xxxx -pi4ioe5v9xxxx==0.0.2 - # homeassistant.components.rpi_pfio pifacecommon==4.2.2 From c62a3c4f0d1f98a8f748bdda19d62dda4db398f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Feb 2022 17:11:22 +0100 Subject: [PATCH 0054/1054] Add support for 8-gang switches to Tuya (#67218) --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/switch.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index ee653534fc8..e7340040658 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -329,6 +329,8 @@ class DPCode(StrEnum): SWITCH_4 = "switch_4" # Switch 4 SWITCH_5 = "switch_5" # Switch 5 SWITCH_6 = "switch_6" # Switch 6 + SWITCH_7 = "switch_7" # Switch 7 + SWITCH_8 = "switch_8" # Switch 8 SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch SWITCH_CHARGE = "switch_charge" SWITCH_CONTROLLER = "switch_controller" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 6be35e0102b..d978b377cc5 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -137,6 +137,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { name="Switch 6", device_class=SwitchDeviceClass.OUTLET, ), + SwitchEntityDescription( + key=DPCode.SWITCH_7, + name="Switch 7", + device_class=SwitchDeviceClass.OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_8, + name="Switch 8", + device_class=SwitchDeviceClass.OUTLET, + ), SwitchEntityDescription( key=DPCode.SWITCH_USB1, name="USB 1", From 715d7f70f0aa5c08e587e9745b95d881af30719d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 25 Feb 2022 17:15:45 +0100 Subject: [PATCH 0055/1054] Adjust SamsungTV abstraction layer (#67216) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 24 +-- homeassistant/components/samsungtv/bridge.py | 184 ++++++++++-------- .../components/samsungtv/test_media_player.py | 56 +++++- 3 files changed, 171 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 508ed2f876f..c9ca282fdd1 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -39,7 +39,6 @@ from .const import ( LEGACY_PORT, LOGGER, METHOD_LEGACY, - METHOD_WEBSOCKET, ) @@ -134,7 +133,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" - await bridge.async_stop() + LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) + await bridge.async_close_remote() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) @@ -149,11 +149,11 @@ async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry ) -> SamsungTVLegacyBridge | SamsungTVWSBridge: """Create a bridge object and update any missing data in the config entry.""" - updated_data = {} - host = entry.data[CONF_HOST] - port = entry.data.get(CONF_PORT) - method = entry.data.get(CONF_METHOD) - info = None + updated_data: dict[str, str | int] = {} + host: str = entry.data[CONF_HOST] + port: int | None = entry.data.get(CONF_PORT) + method: str | None = entry.data.get(CONF_METHOD) + info: dict[str, Any] | None = None if not port or not method: if method == METHOD_LEGACY: @@ -162,7 +162,7 @@ async def _async_create_bridge_with_updated_data( # When we imported from yaml we didn't setup the method # because we didn't know it port, method, info = await async_get_device_info(hass, None, host) - if not port: + if not port or not method: raise ConfigEntryNotReady( "Failed to determine connection method, make sure the device is on." ) @@ -172,8 +172,8 @@ async def _async_create_bridge_with_updated_data( bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data}) - mac = entry.data.get(CONF_MAC) - if not mac and bridge.method == METHOD_WEBSOCKET: + mac: str | None = entry.data.get(CONF_MAC) + if not mac: if info: mac = mac_from_device_info(info) else: @@ -197,7 +197,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - await hass.data[DOMAIN][entry.entry_id].async_stop() + bridge: SamsungTVBridge = hass.data[DOMAIN][entry.entry_id] + LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) + await bridge.async_close_remote() return unload_ok diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 74daf1d34e0..ddb3c0a4e9b 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -19,7 +19,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_TIMEOUT, - CONF_TOKEN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.device_registry import format_mac @@ -67,7 +66,7 @@ async def async_get_device_info( bridge = SamsungTVBridge.get_bridge(hass, METHOD_LEGACY, host, LEGACY_PORT) result = await bridge.async_try_connect() if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING): - return LEGACY_PORT, METHOD_LEGACY, None + return LEGACY_PORT, METHOD_LEGACY, await bridge.async_device_info() return None, None, None @@ -97,7 +96,6 @@ class SamsungTVBridge(ABC): self.method = method self.host = host self.token: str | None = None - self._remote: Remote | None = None self._reauth_callback: CALLBACK_TYPE | None = None self._new_token_callback: CALLBACK_TYPE | None = None @@ -125,66 +123,17 @@ class SamsungTVBridge(ABC): async def async_get_app_list(self) -> dict[str, str] | None: """Get installed app list.""" + @abstractmethod async def async_is_on(self) -> bool: """Tells if the TV is on.""" - if self._remote is not None: - await self.async_close_remote() - - try: - remote = await self.hass.async_add_executor_job(self._get_remote) - return remote is not None - except ( - UnhandledResponse, - AccessDenied, - ConnectionFailure, - ): - # We got a response so it's working. - return True - except OSError: - # Different reasons, e.g. hostname not resolveable - return False + @abstractmethod async def async_send_key(self, key: str, key_type: str | None = None) -> None: """Send a key to the tv and handles exceptions.""" - try: - # recreate connection if connection was dead - retry_count = 1 - for _ in range(retry_count + 1): - try: - await self._async_send_key(key, key_type) - break - except ( - ConnectionClosed, - BrokenPipeError, - WebSocketException, - ): - # BrokenPipe can occur when the commands is sent to fast - # WebSocketException can occur when timed out - self._remote = None - except (UnhandledResponse, AccessDenied): - # We got a response so it's on. - LOGGER.debug("Failed sending command %s", key, exc_info=True) - except OSError: - # Different reasons, e.g. hostname not resolveable - pass @abstractmethod - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send the key.""" - - @abstractmethod - def _get_remote(self, avoid_open: bool = False) -> Remote | SamsungTVWS: - """Get Remote object.""" - async def async_close_remote(self) -> None: """Close remote object.""" - try: - if self._remote is not None: - # Close the current remote connection - await self.hass.async_add_executor_job(self._remote.close) - self._remote = None - except OSError: - LOGGER.debug("Could not establish connection") def _notify_reauth_callback(self) -> None: """Notify access denied callback.""" @@ -214,6 +163,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): CONF_PORT: None, CONF_TIMEOUT: 1, } + self._remote: Remote | None = None async def async_mac_from_device(self) -> None: """Try to fetch the mac address of the TV.""" @@ -223,6 +173,21 @@ class SamsungTVLegacyBridge(SamsungTVBridge): """Get installed app list.""" return {} + async def async_is_on(self) -> bool: + """Tells if the TV is on.""" + return await self.hass.async_add_executor_job(self._is_on) + + def _is_on(self) -> bool: + """Tells if the TV is on.""" + if self._remote is not None: + self._close_remote() + + try: + return self._get_remote() is not None + except (UnhandledResponse, AccessDenied): + # We got a response so it's working. + return True + async def async_try_connect(self) -> str: """Try to connect to the Legacy TV.""" return await self.hass.async_add_executor_job(self._try_connect) @@ -258,7 +223,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): """Try to gather infos of this device.""" return None - def _get_remote(self, avoid_open: bool = False) -> Remote: + def _get_remote(self) -> Remote: """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -276,19 +241,43 @@ class SamsungTVLegacyBridge(SamsungTVBridge): pass return self._remote - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: + async def async_send_key(self, key: str, key_type: str | None = None) -> None: """Send the key using legacy protocol.""" - return await self.hass.async_add_executor_job(self._send_key, key) + await self.hass.async_add_executor_job(self._send_key, key) def _send_key(self, key: str) -> None: """Send the key using legacy protocol.""" - if remote := self._get_remote(): - remote.control(key) + try: + # recreate connection if connection was dead + retry_count = 1 + for _ in range(retry_count + 1): + try: + if remote := self._get_remote(): + remote.control(key) + break + except (ConnectionClosed, BrokenPipeError): + # BrokenPipe can occur when the commands is sent to fast + self._remote = None + except (UnhandledResponse, AccessDenied): + # We got a response so it's on. + LOGGER.debug("Failed sending command %s", key, exc_info=True) + except OSError: + # Different reasons, e.g. hostname not resolveable + pass - async def async_stop(self) -> None: - """Stop Bridge.""" - LOGGER.debug("Stopping SamsungTVLegacyBridge") - await self.async_close_remote() + async def async_close_remote(self) -> None: + """Close remote object.""" + await self.hass.async_add_executor_job(self._close_remote) + + def _close_remote(self) -> None: + """Close remote object.""" + try: + if self._remote is not None: + # Close the current remote connection + self._remote.close() + self._remote = None + except OSError: + LOGGER.debug("Could not establish connection") class SamsungTVWSBridge(SamsungTVBridge): @@ -306,6 +295,7 @@ class SamsungTVWSBridge(SamsungTVBridge): super().__init__(hass, method, host, port) self.token = token self._app_list: dict[str, str] | None = None + self._remote: SamsungTVWS | None = None async def async_mac_from_device(self) -> str | None: """Try to fetch the mac address of the TV.""" @@ -328,6 +318,17 @@ class SamsungTVWSBridge(SamsungTVBridge): return self._app_list + async def async_is_on(self) -> bool: + """Tells if the TV is on.""" + return await self.hass.async_add_executor_job(self._is_on) + + def _is_on(self) -> bool: + """Tells if the TV is on.""" + if self._remote is not None: + self._close_remote() + + return self._get_remote() is not None + async def async_try_connect(self) -> str: """Try to connect to the Websocket TV.""" return await self.hass.async_add_executor_job(self._try_connect) @@ -356,8 +357,6 @@ class SamsungTVWSBridge(SamsungTVBridge): ) as remote: remote.open("samsung.remote.control") self.token = remote.token - if self.token is None: - config[CONF_TOKEN] = "*****" LOGGER.debug("Working config: %s", config) return RESULT_SUCCESS except WebSocketException as err: @@ -375,29 +374,47 @@ class SamsungTVWSBridge(SamsungTVBridge): return RESULT_CANNOT_CONNECT async def async_device_info(self) -> dict[str, Any] | None: + """Try to gather infos of this TV.""" + return await self.hass.async_add_executor_job(self._device_info) + + def _device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" if remote := self._get_remote(avoid_open=True): with contextlib.suppress(HttpApiError, RequestsTimeout): - device_info: dict[str, Any] = await self.hass.async_add_executor_job( - remote.rest_device_info - ) + device_info: dict[str, Any] = remote.rest_device_info() return device_info return None - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: + async def async_send_key(self, key: str, key_type: str | None = None) -> None: """Send the key using websocket protocol.""" - return await self.hass.async_add_executor_job(self._send_key, key, key_type) + await self.hass.async_add_executor_job(self._send_key, key, key_type) def _send_key(self, key: str, key_type: str | None = None) -> None: """Send the key using websocket protocol.""" if key == "KEY_POWEROFF": key = "KEY_POWER" - if remote := self._get_remote(): - if key_type == "run_app": - remote.run_app(key) - else: - remote.send_key(key) + try: + # recreate connection if connection was dead + retry_count = 1 + for _ in range(retry_count + 1): + try: + if remote := self._get_remote(): + if key_type == "run_app": + remote.run_app(key) + else: + remote.send_key(key) + break + except ( + BrokenPipeError, + WebSocketException, + ): + # BrokenPipe can occur when the commands is sent to fast + # WebSocketException can occur when timed out + self._remote = None + except OSError: + # Different reasons, e.g. hostname not resolveable + pass def _get_remote(self, avoid_open: bool = False) -> SamsungTVWS: """Create or return a remote control instance.""" @@ -437,7 +454,16 @@ class SamsungTVWSBridge(SamsungTVBridge): self._notify_new_token_callback() return self._remote - async def async_stop(self) -> None: - """Stop Bridge.""" - LOGGER.debug("Stopping SamsungTVWSBridge") - await self.async_close_remote() + async def async_close_remote(self) -> None: + """Close remote object.""" + await self.hass.async_add_executor_job(self._close_remote) + + def _close_remote(self) -> None: + """Close remote object.""" + try: + if self._remote is not None: + # Close the current remote connection + self._remote.close() + self._remote = None + except OSError: + LOGGER.debug("Could not establish connection") diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 55d68453a38..eb7fb650a9a 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -274,6 +274,26 @@ async def test_update_off(hass: HomeAssistant, mock_now: datetime) -> None: assert state.state == STATE_OFF +async def test_update_off_ws( + hass: HomeAssistant, remotews: Mock, mock_now: datetime +) -> None: + """Testing update tv off.""" + await setup_samsungtv(hass, MOCK_CONFIGWS) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + remotews.open = Mock(side_effect=WebSocketException("Boom")) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + @pytest.mark.usefixtures("remote") async def test_update_access_denied(hass: HomeAssistant, mock_now: datetime) -> None: """Testing update tv access denied exception.""" @@ -440,10 +460,21 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> assert state.state == STATE_ON -async def test_send_key_websocketexception(hass: HomeAssistant, remote: Mock) -> None: +async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" - await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = Mock(side_effect=WebSocketException("Boom")) + await setup_samsungtv(hass, MOCK_CONFIGWS) + remotews.send_key = Mock(side_effect=WebSocketException("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None: + """Testing unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIGWS) + remotews.send_key = Mock(side_effect=OSError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -557,6 +588,12 @@ async def test_turn_off_websocket(hass: HomeAssistant, remotews: Mock) -> None: assert remotews.send_key.call_count == 1 assert remotews.send_key.call_args_list == [call("KEY_POWER")] + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key not called + assert remotews.send_key.call_count == 1 + async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: """Test for turn_off.""" @@ -582,6 +619,19 @@ async def test_turn_off_os_error( assert "Could not establish connection" in caplog.text +async def test_turn_off_ws_os_error( + hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test for turn_off with OSError.""" + caplog.set_level(logging.DEBUG) + await setup_samsungtv(hass, MOCK_CONFIGWS) + remotews.close = Mock(side_effect=OSError("BOOM")) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert "Could not establish connection" in caplog.text + + async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_up.""" await setup_samsungtv(hass, MOCK_CONFIG) From 199d359814656f63be86ed39d0f9f8bec18dfb28 Mon Sep 17 00:00:00 2001 From: martijnvanduijneveldt Date: Fri, 25 Feb 2022 17:25:13 +0100 Subject: [PATCH 0056/1054] Fix nanoleaf white flashing when using scenes (#67168) --- homeassistant/components/nanoleaf/light.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 29c7cb786e6..ed3476c4576 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -153,11 +153,17 @@ class NanoleafLight(NanoleafEntity, LightEntity): effect = kwargs.get(ATTR_EFFECT) transition = kwargs.get(ATTR_TRANSITION) - if hs_color: + if effect: + if effect not in self.effect_list: + raise ValueError( + f"Attempting to apply effect not in the effect list: '{effect}'" + ) + await self._nanoleaf.set_effect(effect) + elif hs_color: hue, saturation = hs_color await self._nanoleaf.set_hue(int(hue)) await self._nanoleaf.set_saturation(int(saturation)) - if color_temp_mired: + elif color_temp_mired: await self._nanoleaf.set_color_temperature( mired_to_kelvin(color_temp_mired) ) @@ -172,12 +178,6 @@ class NanoleafLight(NanoleafEntity, LightEntity): await self._nanoleaf.turn_on() if brightness: await self._nanoleaf.set_brightness(int(brightness / 2.55)) - if effect: - if effect not in self.effect_list: - raise ValueError( - f"Attempting to apply effect not in the effect list: '{effect}'" - ) - await self._nanoleaf.set_effect(effect) async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" From a4ba71408b08c93ec6ab83d8ce5d61e84f1696a4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 25 Feb 2022 10:27:06 -0600 Subject: [PATCH 0057/1054] Adjust Sonos visibility checks (#67196) --- homeassistant/components/sonos/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 71068479fe4..27d51d8f3e6 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -182,6 +182,9 @@ class SonosDiscoveryManager: soco = SoCo(ip_address) # Ensure that the player is available and UID is cached uid = soco.uid + # Abort early if the device is not visible + if not soco.is_visible: + return None _ = soco.volume return soco except NotSupportedException as exc: @@ -240,8 +243,7 @@ class SonosDiscoveryManager: None, ) if not known_uid: - soco = self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED) - if soco and soco.is_visible: + if soco := self._create_soco(ip_addr, SoCoCreationSource.CONFIGURED): self._discovered_player(soco) self.data.hosts_heartbeat = call_later( @@ -249,8 +251,7 @@ class SonosDiscoveryManager: ) def _discovered_ip(self, ip_address): - soco = self._create_soco(ip_address, SoCoCreationSource.DISCOVERED) - if soco and soco.is_visible: + if soco := self._create_soco(ip_address, SoCoCreationSource.DISCOVERED): self._discovered_player(soco) async def _async_create_discovered_player(self, uid, discovered_ip, boot_seqnum): From 07a792019ec97f441f47850e5be90560c37008e4 Mon Sep 17 00:00:00 2001 From: Mark Dietzer Date: Fri, 25 Feb 2022 09:15:49 -0800 Subject: [PATCH 0058/1054] Fix Twitch component to use new API (#67153) Co-authored-by: Paulus Schoutsen --- homeassistant/components/twitch/__init__.py | 2 +- homeassistant/components/twitch/manifest.json | 2 +- homeassistant/components/twitch/sensor.py | 112 +++++++++----- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/twitch/test_twitch.py | 138 +++++++++++------- 6 files changed, 164 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/twitch/__init__.py b/homeassistant/components/twitch/__init__.py index 0cdeb813945..64feb17d6b5 100644 --- a/homeassistant/components/twitch/__init__.py +++ b/homeassistant/components/twitch/__init__.py @@ -1 +1 @@ -"""The twitch component.""" +"""The Twitch component.""" diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 17f1c8586c0..ef68ba94518 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -2,7 +2,7 @@ "domain": "twitch", "name": "Twitch", "documentation": "https://www.home-assistant.io/integrations/twitch", - "requirements": ["python-twitch-client==0.6.0"], + "requirements": ["twitchAPI==2.5.2"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["twitch"] diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index b3357d331bd..771f88f0ef1 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -3,12 +3,18 @@ from __future__ import annotations import logging -from requests.exceptions import HTTPError -from twitch import TwitchClient +from twitchAPI.twitch import ( + AuthScope, + AuthType, + InvalidTokenException, + MissingScopeException, + Twitch, + TwitchAuthorizationException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,9 +39,12 @@ ICON = "mdi:twitch" STATE_OFFLINE = "offline" STATE_STREAMING = "streaming" +OAUTH_SCOPES = [AuthScope.USER_READ_SUBSCRIPTIONS] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_TOKEN): cv.string, } @@ -51,28 +60,45 @@ def setup_platform( """Set up the Twitch platform.""" channels = config[CONF_CHANNELS] client_id = config[CONF_CLIENT_ID] + client_secret = config[CONF_CLIENT_SECRET] oauth_token = config.get(CONF_TOKEN) - client = TwitchClient(client_id, oauth_token) + client = Twitch(app_id=client_id, app_secret=client_secret) + client.auto_refresh_auth = False try: - client.ingests.get_server_list() - except HTTPError: - _LOGGER.error("Client ID or OAuth token is not valid") + client.authenticate_app(scope=OAUTH_SCOPES) + except TwitchAuthorizationException: + _LOGGER.error("INvalid client ID or client secret") return - channel_ids = client.users.translate_usernames_to_ids(channels) + if oauth_token: + try: + client.set_user_authentication( + token=oauth_token, scope=OAUTH_SCOPES, validate=True + ) + except MissingScopeException: + _LOGGER.error("OAuth token is missing required scope") + return + except InvalidTokenException: + _LOGGER.error("OAuth token is invalid") + return - add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True) + channels = client.get_users(logins=channels) + + add_entities( + [TwitchSensor(channel=channel, client=client) for channel in channels["data"]], + True, + ) class TwitchSensor(SensorEntity): """Representation of an Twitch channel.""" - def __init__(self, channel, client): + def __init__(self, channel, client: Twitch): """Initialize the sensor.""" self._client = client self._channel = channel - self._oauth_enabled = client._oauth_token is not None + self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES) self._state = None self._preview = None self._game = None @@ -84,7 +110,7 @@ class TwitchSensor(SensorEntity): @property def name(self): """Return the name of the sensor.""" - return self._channel.display_name + return self._channel["display_name"] @property def native_value(self): @@ -101,7 +127,7 @@ class TwitchSensor(SensorEntity): """Return the state attributes.""" attr = dict(self._statistics) - if self._oauth_enabled: + if self._enable_user_auth: attr.update(self._subscription) attr.update(self._follow) @@ -112,7 +138,7 @@ class TwitchSensor(SensorEntity): @property def unique_id(self): """Return unique ID for this sensor.""" - return self._channel.id + return self._channel["id"] @property def icon(self): @@ -122,41 +148,51 @@ class TwitchSensor(SensorEntity): def update(self): """Update device state.""" - channel = self._client.channels.get_by_id(self._channel.id) + followers = self._client.get_users_follows(to_id=self._channel["id"])["total"] + channel = self._client.get_users(user_ids=[self._channel["id"]])["data"][0] self._statistics = { - ATTR_FOLLOWING: channel.followers, - ATTR_VIEWS: channel.views, + ATTR_FOLLOWING: followers, + ATTR_VIEWS: channel["view_count"], } - if self._oauth_enabled: - user = self._client.users.get() + if self._enable_user_auth: + user = self._client.get_users()["data"][0] - try: - sub = self._client.users.check_subscribed_to_channel( - user.id, self._channel.id - ) + subs = self._client.check_user_subscription( + user_id=user["id"], broadcaster_id=self._channel["id"] + ) + if "data" in subs: self._subscription = { ATTR_SUBSCRIPTION: True, - ATTR_SUBSCRIPTION_SINCE: sub.created_at, - ATTR_SUBSCRIPTION_GIFTED: sub.is_gift, + ATTR_SUBSCRIPTION_GIFTED: subs["data"][0]["is_gift"], } - except HTTPError: + elif "status" in subs and subs["status"] == 404: self._subscription = {ATTR_SUBSCRIPTION: False} - - try: - follow = self._client.users.check_follows_channel( - user.id, self._channel.id + elif "error" in subs: + raise Exception( + f"Error response on check_user_subscription: {subs['error']}" ) - self._follow = {ATTR_FOLLOW: True, ATTR_FOLLOW_SINCE: follow.created_at} - except HTTPError: + else: + raise Exception("Unknown error response on check_user_subscription") + + follows = self._client.get_users_follows( + from_id=user["id"], to_id=self._channel["id"] + )["data"] + if len(follows) > 0: + self._follow = { + ATTR_FOLLOW: True, + ATTR_FOLLOW_SINCE: follows[0]["followed_at"], + } + else: self._follow = {ATTR_FOLLOW: False} - stream = self._client.streams.get_stream_by_user(self._channel.id) - if stream: - self._game = stream.channel.get("game") - self._title = stream.channel.get("status") - self._preview = stream.preview.get("medium") + streams = self._client.get_streams(user_id=[self._channel["id"]])["data"] + if len(streams) > 0: + stream = streams[0] + self._game = stream["game_name"] + self._title = stream["title"] + self._preview = stream["thumbnail_url"] self._state = STATE_STREAMING else: - self._preview = self._channel.logo + self._preview = channel["offline_image_url"] self._state = STATE_OFFLINE diff --git a/requirements_all.txt b/requirements_all.txt index 7f0a7653844..e494ca410d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1954,9 +1954,6 @@ python-tado==0.12.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 -# homeassistant.components.twitch -python-twitch-client==0.6.0 - # homeassistant.components.vlc python-vlc==1.1.2 @@ -2360,6 +2357,9 @@ twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twitch +twitchAPI==2.5.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 286473ee5aa..93ce156a638 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1235,9 +1235,6 @@ python-songpal==0.14 # homeassistant.components.tado python-tado==0.12.0 -# homeassistant.components.twitch -python-twitch-client==0.6.0 - # homeassistant.components.awair python_awair==0.2.1 @@ -1467,6 +1464,9 @@ twentemilieu==0.5.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.twitch +twitchAPI==2.5.2 + # homeassistant.components.rainforest_eagle uEagle==0.0.2 diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 310be91c796..bfffeb4ae7f 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -1,11 +1,8 @@ """The tests for an update of the Twitch component.""" from unittest.mock import MagicMock, patch -from requests import HTTPError -from twitch.resources import Channel, Follow, Stream, Subscription, User - from homeassistant.components import sensor -from homeassistant.const import CONF_CLIENT_ID +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component ENTITY_ID = "sensor.channel123" @@ -13,6 +10,7 @@ CONFIG = { sensor.DOMAIN: { "platform": "twitch", CONF_CLIENT_ID: "1234", + CONF_CLIENT_SECRET: " abcd", "channels": ["channel123"], } } @@ -20,39 +18,46 @@ CONFIG_WITH_OAUTH = { sensor.DOMAIN: { "platform": "twitch", CONF_CLIENT_ID: "1234", + CONF_CLIENT_SECRET: "abcd", "channels": ["channel123"], "token": "9876", } } -USER_ID = User({"id": 123, "display_name": "channel123", "logo": "logo.png"}) -STREAM_OBJECT_ONLINE = Stream( - { - "channel": {"game": "Good Game", "status": "Title"}, - "preview": {"medium": "stream-medium.png"}, - } -) -CHANNEL_OBJECT = Channel({"followers": 42, "views": 24}) -OAUTH_USER_ID = User({"id": 987}) -SUB_ACTIVE = Subscription({"created_at": "2020-01-20T21:22:42", "is_gift": False}) -FOLLOW_ACTIVE = Follow({"created_at": "2020-01-20T21:22:42"}) +USER_OBJECT = { + "id": 123, + "display_name": "channel123", + "offline_image_url": "logo.png", + "view_count": 42, +} +STREAM_OBJECT_ONLINE = { + "game_name": "Good Game", + "title": "Title", + "thumbnail_url": "stream-medium.png", +} + +FOLLOWERS_OBJECT = [{"followed_at": "2020-01-20T21:22:42"}] * 24 +OAUTH_USER_ID = {"id": 987} +SUB_ACTIVE = {"is_gift": False} +FOLLOW_ACTIVE = {"followed_at": "2020-01-20T21:22:42"} + + +def make_data(data): + """Create a data object.""" + return {"data": data, "total": len(data)} async def test_init(hass): """Test initial config.""" - channels = MagicMock() - channels.get_by_id.return_value = CHANNEL_OBJECT - streams = MagicMock() - streams.get_stream_by_user.return_value = None - twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels = channels - twitch_mock.streams = streams + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True await hass.async_block_till_done() @@ -62,20 +67,21 @@ async def test_init(hass): assert sensor_state.name == "channel123" assert sensor_state.attributes["icon"] == "mdi:twitch" assert sensor_state.attributes["friendly_name"] == "channel123" - assert sensor_state.attributes["views"] == 24 - assert sensor_state.attributes["followers"] == 42 + assert sensor_state.attributes["views"] == 42 + assert sensor_state.attributes["followers"] == 24 async def test_offline(hass): """Test offline state.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock.streams.get_stream_by_user.return_value = None + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True @@ -90,12 +96,13 @@ async def test_streaming(hass): """Test streaming state.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock.streams.get_stream_by_user.return_value = STREAM_OBJECT_ONLINE + twitch_mock.get_users.return_value = make_data([USER_OBJECT]) + twitch_mock.get_users_follows.return_value = make_data(FOLLOWERS_OBJECT) + twitch_mock.get_streams.return_value = make_data([STREAM_OBJECT_ONLINE]) + twitch_mock.has_required_auth.return_value = False with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True @@ -112,15 +119,21 @@ async def test_oauth_without_sub_and_follow(hass): """Test state with oauth.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError() - twitch_mock.users.check_follows_channel.side_effect = HTTPError() + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([]), + ] + twitch_mock.has_required_auth.return_value = True + twitch_mock.check_user_subscription.return_value = {"status": 404} with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) @@ -135,15 +148,23 @@ async def test_oauth_with_sub(hass): """Test state with oauth and sub.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.return_value = SUB_ACTIVE - twitch_mock.users.check_follows_channel.side_effect = HTTPError() + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([]), + ] + twitch_mock.has_required_auth.return_value = True + + # This function does not return an array so use make_data + twitch_mock.check_user_subscription.return_value = make_data([SUB_ACTIVE]) with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) @@ -151,7 +172,6 @@ async def test_oauth_with_sub(hass): sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.attributes["subscribed"] is True - assert sensor_state.attributes["subscribed_since"] == "2020-01-20T21:22:42" assert sensor_state.attributes["subscription_is_gifted"] is False assert sensor_state.attributes["following"] is False @@ -160,15 +180,21 @@ async def test_oauth_with_follow(hass): """Test state with oauth and follow.""" twitch_mock = MagicMock() - twitch_mock.users.translate_usernames_to_ids.return_value = [USER_ID] - twitch_mock.channels.get_by_id.return_value = CHANNEL_OBJECT - twitch_mock._oauth_token = True # A replacement for the token - twitch_mock.users.get.return_value = OAUTH_USER_ID - twitch_mock.users.check_subscribed_to_channel.side_effect = HTTPError() - twitch_mock.users.check_follows_channel.return_value = FOLLOW_ACTIVE + twitch_mock.get_streams.return_value = make_data([]) + twitch_mock.get_users.side_effect = [ + make_data([USER_OBJECT]), + make_data([USER_OBJECT]), + make_data([OAUTH_USER_ID]), + ] + twitch_mock.get_users_follows.side_effect = [ + make_data(FOLLOWERS_OBJECT), + make_data([FOLLOW_ACTIVE]), + ] + twitch_mock.has_required_auth.return_value = True + twitch_mock.check_user_subscription.return_value = {"status": 404} with patch( - "homeassistant.components.twitch.sensor.TwitchClient", + "homeassistant.components.twitch.sensor.Twitch", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) From 6fd9e71b8ff2554bc7b07de22ca007fdb2afd20d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 10:00:03 -0800 Subject: [PATCH 0059/1054] Adjust serializing resolved media (#67240) --- .../components/media_player/browse_media.py | 8 +++--- .../components/media_source/__init__.py | 26 +++++++++---------- tests/components/media_source/test_init.py | 3 ++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 6fe4683c1fc..26494e4c8a7 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -15,7 +15,9 @@ from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY @callback -def async_process_play_media_url(hass: HomeAssistant, media_content_id: str) -> str: +def async_process_play_media_url( + hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False +) -> str: """Update a media URL with authentication if it points at Home Assistant.""" if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): return media_content_id @@ -34,8 +36,8 @@ def async_process_play_media_url(hass: HomeAssistant, media_content_id: str) -> ) media_content_id = str(parsed.join(yarl.URL(signed_path))) - # prepend external URL - if media_content_id[0] == "/": + # convert relative URL to absolute URL + if media_content_id[0] == "/" and not allow_relative_url: media_content_id = f"{get_url(hass)}{media_content_id}" return media_content_id diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 77b254dcf9d..2bcd80a39ab 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -2,21 +2,20 @@ from __future__ import annotations from collections.abc import Callable -import dataclasses -from datetime import timedelta from typing import Any -from urllib.parse import quote import voluptuous as vol from homeassistant.components import frontend, websocket_api -from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, CONTENT_AUTH_EXPIRY_TIME, BrowseError, BrowseMedia, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( @@ -177,13 +176,12 @@ async def websocket_resolve_media( connection.send_error(msg["id"], "resolve_media_failed", str(err)) return - data = dataclasses.asdict(media) - - if data["url"][0] == "/": - data["url"] = async_sign_path( - hass, - quote(data["url"]), - timedelta(seconds=msg["expires"]), - ) - - connection.send_result(msg["id"], data) + connection.send_result( + msg["id"], + { + "url": async_process_play_media_url( + hass, media.url, allow_relative_url=True + ), + "mime_type": media.mime_type, + }, + ) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 319ef295be3..2655000efc9 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -187,7 +187,8 @@ async def test_websocket_resolve_media(hass, hass_ws_client, filename): assert msg["id"] == 1 assert msg["result"]["mime_type"] == media.mime_type - # Validate url is signed. + # Validate url is relative and signed. + assert msg["result"]["url"][0] == "/" parsed = yarl.URL(msg["result"]["url"]) assert parsed.path == getattr(media, "url") assert "authSig" in parsed.query From 3f16c6d6efad20b60a4a8d2114a0905ecd252820 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Feb 2022 09:20:56 -1000 Subject: [PATCH 0060/1054] Fix powerwall data incompatibility with energy integration (#67245) --- homeassistant/components/powerwall/sensor.py | 87 ++++++++++++++------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index a48726211b2..bc8ab1c0215 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from tesla_powerwall import MeterType +from tesla_powerwall import Meter, MeterType from homeassistant.components.sensor import ( SensorDeviceClass, @@ -28,7 +28,6 @@ from .models import PowerwallData, PowerwallRuntimeData _METER_DIRECTION_EXPORT = "export" _METER_DIRECTION_IMPORT = "import" -_METER_DIRECTIONS = [_METER_DIRECTION_EXPORT, _METER_DIRECTION_IMPORT] async def async_setup_entry( @@ -42,20 +41,20 @@ async def async_setup_entry( assert coordinator is not None data: PowerwallData = coordinator.data entities: list[ - PowerWallEnergySensor | PowerWallEnergyDirectionSensor | PowerWallChargeSensor - ] = [] - for meter in data.meters.meters: - entities.append(PowerWallEnergySensor(powerwall_data, meter)) - for meter_direction in _METER_DIRECTIONS: - entities.append( - PowerWallEnergyDirectionSensor( - powerwall_data, - meter, - meter_direction, - ) - ) + PowerWallEnergySensor + | PowerWallImportSensor + | PowerWallExportSensor + | PowerWallChargeSensor + ] = [PowerWallChargeSensor(powerwall_data)] - entities.append(PowerWallChargeSensor(powerwall_data)) + for meter in data.meters.meters: + entities.extend( + [ + PowerWallEnergySensor(powerwall_data, meter), + PowerWallExportSensor(powerwall_data, meter), + PowerWallImportSensor(powerwall_data, meter), + ] + ) async_add_entities(entities) @@ -128,18 +127,54 @@ class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Initialize the sensor.""" super().__init__(powerwall_data) self._meter = meter - self._meter_direction = meter_direction - self._attr_name = ( - f"Powerwall {self._meter.value.title()} {self._meter_direction.title()}" - ) - self._attr_unique_id = ( - f"{self.base_unique_id}_{self._meter.value}_{self._meter_direction}" - ) + self._attr_name = f"Powerwall {meter.value.title()} {meter_direction.title()}" + self._attr_unique_id = f"{self.base_unique_id}_{meter.value}_{meter_direction}" + + @property + def available(self) -> bool: + """Check if the reading is actually available. + + The device reports 0 when something goes wrong which + we do not want to include in statistics and its a + transient data error. + """ + return super().available and self.native_value != 0 + + @property + def meter(self) -> Meter: + """Get the meter for the sensor.""" + return self.data.meters.get_meter(self._meter) + + +class PowerWallExportSensor(PowerWallEnergyDirectionSensor): + """Representation of an Powerwall Export sensor.""" + + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + ) -> None: + """Initialize the sensor.""" + super().__init__(powerwall_data, meter, _METER_DIRECTION_EXPORT) @property def native_value(self) -> float: """Get the current value in kWh.""" - meter = self.data.meters.get_meter(self._meter) - if self._meter_direction == _METER_DIRECTION_EXPORT: - return meter.get_energy_exported() - return meter.get_energy_imported() + return abs(self.meter.get_energy_exported()) + + +class PowerWallImportSensor(PowerWallEnergyDirectionSensor): + """Representation of an Powerwall Import sensor.""" + + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + ) -> None: + """Initialize the sensor.""" + super().__init__(powerwall_data, meter, _METER_DIRECTION_IMPORT) + + @property + def native_value(self) -> float: + """Get the current value in kWh.""" + return abs(self.meter.get_energy_imported()) From 99bc2a7c9e849a1f508865fc1e4601fb0b430fc0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 11:35:39 -0800 Subject: [PATCH 0061/1054] =?UTF-8?q?Give=20Sonos=20media=20browse=20folde?= =?UTF-8?q?rs=20Sonos=20logos=20to=20distinguish=20from=20media=E2=80=A6?= =?UTF-8?q?=20(#67248)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/sonos/media_browser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 2272ceb183f..b2d881e8bf2 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -267,6 +267,7 @@ async def root_payload( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="favorites", + thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", can_play=False, can_expand=True, ) @@ -281,6 +282,7 @@ async def root_payload( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="library", + thumbnail="https://brands.home-assistant.io/_/sonos/logo.png", can_play=False, can_expand=True, ) From e81ca64aa4923c102f181b489ccc002eefcc8863 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Feb 2022 09:37:19 -1000 Subject: [PATCH 0062/1054] Prevent the wrong WiZ device from being used when the IP is a different device (#67250) --- homeassistant/components/wiz/__init__.py | 9 +++++++++ tests/components/wiz/test_init.py | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 7bea86d323c..d739c571c8b 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -60,6 +60,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await bulb.async_close() raise ConfigEntryNotReady(f"{ip_address}: {err}") from err + if bulb.mac != entry.unique_id: + # The ip address of the bulb has changed and its likely offline + # and another WiZ device has taken the IP. Avoid setting up + # since its the wrong device. As soon as the device comes back + # online the ip will get updated and setup will proceed. + raise ConfigEntryNotReady( + "Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" + ) + async def _async_update() -> None: """Update the WiZ device.""" try: diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index fb21e930efd..58afb5c944a 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -50,3 +50,11 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: _, entry = await async_setup_integration(hass, wizlight=bulb) assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY bulb.async_close.assert_called_once() + + +async def test_wrong_device_now_has_our_ip(hass: HomeAssistant) -> None: + """Test setup is retried when the wrong device is found.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.mac = "dddddddddddd" + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY From 8233278cccb044665a67a237094b2b9f156c98dc Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 25 Feb 2022 20:37:52 +0100 Subject: [PATCH 0063/1054] Correct tests to use data_type in Modbus (#67246) --- tests/components/modbus/test_climate.py | 6 +- tests/components/modbus/test_init.py | 6 +- tests/components/modbus/test_sensor.py | 87 +++++++++---------------- 3 files changed, 34 insertions(+), 65 deletions(-) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 80c8590f7cc..e453e3d4d44 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -13,7 +13,6 @@ from homeassistant.components.modbus.const import ( from homeassistant.const import ( ATTR_TEMPERATURE, CONF_ADDRESS, - CONF_COUNT, CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, @@ -46,7 +45,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 20, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, CONF_LAZY_ERROR: 10, } ], @@ -68,7 +67,7 @@ async def test_config_climate(hass, mock_modbus): CONF_SLAVE: 1, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, }, ], }, @@ -94,7 +93,6 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): { CONF_CLIMATES: [ { - CONF_COUNT: 2, CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 8b0f3952fdc..b416ebad537 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -146,13 +146,11 @@ async def test_number_validator(): }, { CONF_NAME: TEST_ENTITY_NAME, - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, }, { CONF_NAME: TEST_ENTITY_NAME, - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_BYTE, }, { diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 053dc46c6ba..e3085ecf91e 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -48,6 +48,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT16, } ] }, @@ -57,8 +58,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", + CONF_DATA_TYPE: DataType.INT16, CONF_PRECISION: 0, CONF_SCALE: 1, CONF_OFFSET: 0, @@ -75,8 +75,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, CONF_SLAVE: 10, - CONF_COUNT: 1, - CONF_DATA_TYPE: "int", + CONF_DATA_TYPE: DataType.INT16, CONF_PRECISION: 0, CONF_SCALE: 1, CONF_OFFSET: 0, @@ -90,7 +89,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 1, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_NONE, } ] @@ -100,7 +99,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 1, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_BYTE, } ] @@ -110,7 +109,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD, } ] @@ -120,7 +119,7 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD_BYTE, } ] @@ -253,8 +252,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl [ ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -271,8 +269,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1, CONF_OFFSET: 13, CONF_PRECISION: 0, @@ -283,8 +280,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 3, CONF_OFFSET: 13, CONF_PRECISION: 0, @@ -295,8 +291,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT16, CONF_SCALE: 3, CONF_OFFSET: 13, CONF_PRECISION: 4, @@ -307,8 +302,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1.5, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -319,8 +313,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: "1.5", CONF_OFFSET: "5", CONF_PRECISION: "1", @@ -331,8 +324,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 2.4, CONF_OFFSET: 0, CONF_PRECISION: 2, @@ -343,8 +335,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SCALE: 1, CONF_OFFSET: -10.3, CONF_PRECISION: 1, @@ -355,8 +346,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -367,8 +357,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -379,8 +368,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 4, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT64, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -391,8 +379,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 4, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT64, CONF_SCALE: 2, CONF_OFFSET: 3, CONF_PRECISION: 0, @@ -403,8 +390,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 4, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT64, CONF_SCALE: 2.0, CONF_OFFSET: 3.0, CONF_PRECISION: 0, @@ -415,9 +401,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -428,9 +413,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -441,9 +425,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DataType.FLOAT, + CONF_DATA_TYPE: DataType.FLOAT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 5, @@ -480,9 +463,8 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DataType.UINT, + CONF_DATA_TYPE: DataType.UINT32, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -493,8 +475,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_NONE, }, [0x0102], @@ -503,8 +484,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 1, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, CONF_SWAP: CONF_SWAP_BYTE, }, [0x0201], @@ -513,8 +493,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_BYTE, }, [0x0102, 0x0304], @@ -523,8 +502,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD, }, [0x0102, 0x0304], @@ -533,8 +511,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT32, CONF_SWAP: CONF_SWAP_WORD_BYTE, }, [0x0102, 0x0304], @@ -543,7 +520,6 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ), ( { - CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, CONF_DATA_TYPE: DataType.FLOAT32, CONF_PRECISION: 2, @@ -578,14 +554,12 @@ async def test_all_sensor(hass, mock_do_cycle, expected): [ ( { - CONF_COUNT: 1, CONF_DATA_TYPE: DataType.INT16, }, [7, 9], ), ( { - CONF_COUNT: 2, CONF_DATA_TYPE: DataType.INT32, }, [7], @@ -675,9 +649,8 @@ async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): ), ( { - CONF_COUNT: 1, CONF_PRECISION: 0, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: DataType.INT16, }, [0x0101], "257", From 069e70ff03bdbb6fd0f7a0e21608df21f8de556d Mon Sep 17 00:00:00 2001 From: James Hewitt Date: Fri, 25 Feb 2022 19:43:29 +0000 Subject: [PATCH 0064/1054] Add mode setting to RFXtrx device configuration (#67173) This allows users to configure the modes on their RFXtrx device without downloading additional software that only runs on Windows. It also enables the use of modes that cannot be permanently enabled on the device, such as for undecoded and raw messages. --- homeassistant/components/rfxtrx/__init__.py | 16 ++++++- .../components/rfxtrx/config_flow.py | 10 +++++ homeassistant/components/rfxtrx/const.py | 1 + homeassistant/components/rfxtrx/strings.json | 1 + .../components/rfxtrx/translations/en.json | 1 + tests/components/rfxtrx/conftest.py | 5 ++- tests/components/rfxtrx/test_config_flow.py | 44 ++++++++++++++++++- tests/components/rfxtrx/test_init.py | 38 +++++++++++++++- 8 files changed, 111 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index edf79ce15a5..ad3e2503527 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -40,6 +40,7 @@ from .const import ( COMMAND_GROUP_LIST, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, + CONF_PROTOCOLS, DATA_RFXOBJECT, DEVICE_PACKET_TYPE_LIGHTING4, EVENT_RFXTRX_EVENT, @@ -123,15 +124,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _create_rfx(config): """Construct a rfx object based on config.""" + + modes = config.get(CONF_PROTOCOLS) + + if modes: + _LOGGER.debug("Using modes: %s", ",".join(modes)) + else: + _LOGGER.debug("No modes defined, using device configuration") + if config[CONF_PORT] is not None: # If port is set then we create a TCP connection rfx = rfxtrxmod.Connect( (config[CONF_HOST], config[CONF_PORT]), None, transport_protocol=rfxtrxmod.PyNetworkTransport, + modes=modes, ) else: - rfx = rfxtrxmod.Connect(config[CONF_DEVICE], None) + rfx = rfxtrxmod.Connect( + config[CONF_DEVICE], + None, + modes=modes, + ) return rfx diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 7a842ad470c..754eaeca9ff 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy +import itertools import os from typing import TypedDict, cast @@ -23,6 +24,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceRegistry, @@ -40,6 +42,7 @@ from .const import ( CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_OFF_DELAY, + CONF_PROTOCOLS, CONF_REPLACE_DEVICE, CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, @@ -55,6 +58,8 @@ from .switch import supported as switch_supported CONF_EVENT_CODE = "event_code" CONF_MANUAL_PATH = "Enter Manually" +RECV_MODES = sorted(itertools.chain(*rfxtrxmod.lowlevel.Status.RECMODES)) + class DeviceData(TypedDict): """Dict data representing a device entry.""" @@ -96,6 +101,7 @@ class OptionsFlow(config_entries.OptionsFlow): if user_input is not None: self._global_options = { CONF_AUTOMATIC_ADD: user_input[CONF_AUTOMATIC_ADD], + CONF_PROTOCOLS: user_input[CONF_PROTOCOLS] or None, } if CONF_DEVICE in user_input: entry_id = user_input[CONF_DEVICE] @@ -145,6 +151,10 @@ class OptionsFlow(config_entries.OptionsFlow): CONF_AUTOMATIC_ADD, default=self._config_entry.data[CONF_AUTOMATIC_ADD], ): bool, + vol.Optional( + CONF_PROTOCOLS, + default=self._config_entry.data.get(CONF_PROTOCOLS) or [], + ): cv.multi_select(RECV_MODES), vol.Optional(CONF_EVENT_CODE): str, vol.Optional(CONF_DEVICE): vol.In(configure_devices), } diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 50cd355c457..74fe1ae2790 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -5,6 +5,7 @@ CONF_AUTOMATIC_ADD = "automatic_add" CONF_SIGNAL_REPETITIONS = "signal_repetitions" CONF_OFF_DELAY = "off_delay" CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode" +CONF_PROTOCOLS = "protocols" CONF_REPLACE_DEVICE = "replace_device" diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 542ff9a45cd..f21905c1d21 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -41,6 +41,7 @@ "data": { "debug": "Enable debugging", "automatic_add": "Enable automatic add", + "protocols": "Protocols", "event_code": "Enter event code to add", "device": "Select device to configure" }, diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 2728c189010..af042719f77 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -61,6 +61,7 @@ "debug": "Enable debugging", "device": "Select device to configure", "event_code": "Enter event code to add", + "protocols": "Protocols", "remove_device": "Select device to delete" }, "title": "Rfxtrx Options" diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 70de0be5937..86fec2fdc7f 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -14,13 +14,16 @@ from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.light.conftest import mock_light_profiles # noqa: F401 -def create_rfx_test_cfg(device="abcd", automatic_add=False, devices=None): +def create_rfx_test_cfg( + device="abcd", automatic_add=False, protocols=None, devices=None +): """Create rfxtrx config entry data.""" return { "device": device, "host": None, "port": None, "automatic_add": automatic_add, + "protocols": protocols, "debug": False, "devices": devices, } diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index bee9ea4880a..82406b8e587 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -11,6 +11,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry +SOME_PROTOCOLS = ["ac", "arc"] + def serial_connect(self): """Mock a serial connection.""" @@ -286,6 +288,7 @@ async def test_options_global(hass): "port": None, "device": "/dev/tty123", "automatic_add": False, + "protocols": None, "devices": {}, }, unique_id=DOMAIN, @@ -298,7 +301,8 @@ async def test_options_global(hass): assert result["step_id"] == "prompt_options" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"automatic_add": True} + result["flow_id"], + user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -307,6 +311,44 @@ async def test_options_global(hass): assert entry.data["automatic_add"] + assert not set(entry.data["protocols"]) ^ set(SOME_PROTOCOLS) + + +async def test_no_protocols(hass): + """Test we set protocols to None if none are selected.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": True, + "protocols": SOME_PROTOCOLS, + "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": False, "protocols": []}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert not entry.data["automatic_add"] + + assert entry.data["protocols"] is None + async def test_options_add_device(hass): """Test we can add a device.""" diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index d5562fa5149..2103db35f13 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,14 +1,20 @@ """The tests for the Rfxtrx component.""" from __future__ import annotations -from unittest.mock import call +from unittest.mock import ANY, call, patch +import RFXtrx as rfxtrxmod + +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from tests.components.rfxtrx.conftest import setup_rfx_test_cfg +from tests.common import MockConfigEntry +from tests.components.rfxtrx.conftest import create_rfx_test_cfg, setup_rfx_test_cfg + +SOME_PROTOCOLS = ["ac", "arc"] async def test_fire_event(hass, rfxtrx): @@ -118,3 +124,31 @@ async def test_ws_device_remove(hass, hass_ws_client): # Verify that the config entry has removed the device assert mock_entry.data["devices"] == {} + + +async def test_connect(hass): + """Test that we attempt to connect to the device.""" + entry_data = create_rfx_test_cfg(device="/dev/ttyUSBfake") + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + with patch.object(rfxtrxmod, "Connect") as connect: + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + connect.assert_called_once_with("/dev/ttyUSBfake", ANY, modes=ANY) + + +async def test_connect_with_protocols(hass): + """Test that we attempt to set protocols.""" + entry_data = create_rfx_test_cfg(device="/dev/ttyUSBfake", protocols=SOME_PROTOCOLS) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + with patch.object(rfxtrxmod, "Connect") as connect: + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + connect.assert_called_once_with("/dev/ttyUSBfake", ANY, modes=SOME_PROTOCOLS) From 0a6c8f8e6c4dc25a78fae4d0f56e2ad9d8f0daa6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 11:52:14 -0800 Subject: [PATCH 0065/1054] Improve not shown handling (#67247) --- .../components/camera/media_source.py | 3 +++ .../components/media_source/__init__.py | 2 +- tests/components/camera/test_media_source.py | 1 + tests/components/media_source/test_init.py | 26 ++++++++++++++++++- 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index c61cbef146a..ab7661fefe2 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -81,11 +81,13 @@ class CameraMediaSource(MediaSource): # Root. List cameras. component: EntityComponent = self.hass.data[DOMAIN] children = [] + not_shown = 0 for camera in component.entities: camera = cast(Camera, camera) stream_type = camera.frontend_stream_type if stream_type not in supported_stream_types: + not_shown += 1 continue children.append( @@ -111,4 +113,5 @@ class CameraMediaSource(MediaSource): can_expand=True, children_media_class=MEDIA_CLASS_VIDEO, children=children, + not_shown=not_shown, ) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 2bcd80a39ab..3c42016f8f7 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -119,7 +119,7 @@ async def async_browse_media( item.children = [ child for child in item.children if child.can_expand or content_filter(child) ] - item.not_shown = old_count - len(item.children) + item.not_shown += old_count - len(item.children) return item diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 3a3558419e5..54d6ef6279e 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -35,6 +35,7 @@ async def test_browsing_filter_non_hls(hass, mock_camera_web_rtc): assert item is not None assert item.title == "Camera" assert len(item.children) == 0 + assert item.not_shown == 2 async def test_resolving(hass, mock_camera_hls): diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 2655000efc9..491b1972cb6 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -6,7 +6,7 @@ import yarl from homeassistant.components import media_source from homeassistant.components.media_player import MEDIA_CLASS_DIRECTORY, BrowseError -from homeassistant.components.media_source import const +from homeassistant.components.media_source import const, models from homeassistant.setup import async_setup_component @@ -60,6 +60,30 @@ async def test_async_browse_media(hass): media.children[0].title = "Epic Sax Guy 10 Hours" assert media.not_shown == 1 + # Test content filter adds to original not_shown + orig_browse = models.MediaSourceItem.async_browse + + async def not_shown_browse(self): + """Patch browsed item to set not_shown base value.""" + item = await orig_browse(self) + item.not_shown = 10 + return item + + with patch( + "homeassistant.components.media_source.models.MediaSourceItem.async_browse", + not_shown_browse, + ): + media = await media_source.async_browse_media( + hass, + "", + content_filter=lambda item: item.media_content_type.startswith("video/"), + ) + assert isinstance(media, media_source.models.BrowseMediaSource) + assert media.title == "media" + assert len(media.children) == 1, media.children + media.children[0].title = "Epic Sax Guy 10 Hours" + assert media.not_shown == 11 + # Test invalid media content with pytest.raises(BrowseError): await media_source.async_browse_media(hass, "invalid") From 22aed088f33a87b75504c88c53226c03fd0d142f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 12:19:56 -0800 Subject: [PATCH 0066/1054] Kodi/Roku: Add brand logos to brand folders at root level (#67251) --- homeassistant/components/kodi/browse_media.py | 3 +++ homeassistant/components/roku/browse_media.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index e0fdb0f73fd..519c4dc7c1b 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -224,6 +224,9 @@ async def library_payload(hass): ) ) + for child in library_info.children: + child.thumbnail = "https://brands.home-assistant.io/_/kodi/logo.png" + with contextlib.suppress(media_source.BrowseError): item = await media_source.async_browse_media(hass, None) # If domain is None, it's overview of available sources diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index d8cd540e613..72b572e8d3e 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -135,6 +135,9 @@ async def root_payload( ) ) + for child in children: + child.thumbnail = "https://brands.home-assistant.io/_/roku/logo.png" + try: browse_item = await media_source.async_browse_media(hass, None) From e6cd155c01e25bf137c5595340093b378f97dd97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Feb 2022 14:01:20 -0800 Subject: [PATCH 0067/1054] Bump hass-nabucasa to 0.54.0 (#67252) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e9548c03ba6..d5d0c2c0370 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.53.1"], + "requirements": ["hass-nabucasa==0.54.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1bd905e6abd..fd8fe9c0681 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 -hass-nabucasa==0.53.1 +hass-nabucasa==0.54.0 home-assistant-frontend==20220224.0 httpx==0.21.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index e494ca410d5..3b20cc58879 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ habitipy==0.2.0 hangups==0.4.17 # homeassistant.components.cloud -hass-nabucasa==0.53.1 +hass-nabucasa==0.54.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93ce156a638..a2728d40ff2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -532,7 +532,7 @@ habitipy==0.2.0 hangups==0.4.17 # homeassistant.components.cloud -hass-nabucasa==0.53.1 +hass-nabucasa==0.54.0 # homeassistant.components.tasmota hatasmota==0.3.1 From 0f418341f3a5eeb4b80429816724085adcb289cc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 26 Feb 2022 00:17:09 +0000 Subject: [PATCH 0068/1054] [ci skip] Translation update --- .../binary_sensor/translations/es.json | 3 +++ .../binary_sensor/translations/id.json | 4 ++++ .../components/dlna_dms/translations/id.json | 24 +++++++++++++++++++ .../components/fritz/translations/id.json | 3 ++- .../components/iss/translations/es.json | 12 ++++++++++ .../components/nanoleaf/translations/id.json | 8 +++++++ .../components/nanoleaf/translations/ja.json | 8 +++++++ .../components/nanoleaf/translations/no.json | 8 +++++++ .../radio_browser/translations/id.json | 12 ++++++++++ .../components/rfxtrx/translations/ca.json | 1 + .../components/rfxtrx/translations/el.json | 1 + .../components/rfxtrx/translations/hu.json | 1 + .../components/sense/translations/id.json | 16 ++++++++++++- .../components/sonarr/translations/id.json | 3 ++- 14 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/dlna_dms/translations/id.json create mode 100644 homeassistant/components/radio_browser/translations/id.json diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 3bf3c1a74a2..1e328150c39 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -134,6 +134,9 @@ "off": "No est\u00e1 cargando", "on": "Cargando" }, + "carbon_monoxide": { + "on": "Detectado" + }, "co": { "off": "No detectado", "on": "Detectado" diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index 57dc0ab6930..5215f57814a 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -134,6 +134,10 @@ "off": "Tidak mengisi daya", "on": "Mengisi daya" }, + "carbon_monoxide": { + "off": "Tidak ada", + "on": "Terdeteksi" + }, "co": { "off": "Tidak ada", "on": "Terdeteksi" diff --git a/homeassistant/components/dlna_dms/translations/id.json b/homeassistant/components/dlna_dms/translations/id.json new file mode 100644 index 00000000000..020045c0bcd --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "bad_ssdp": "Data SSDP tidak memiliki nilai yang diperlukan", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_dms": "Perangkat bukan Server Media yang didukung" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Pilih perangkat untuk dikonfigurasi", + "title": "Perangkat DLNA DMA yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json index 5aae1443d02..a5ca1887256 100644 --- a/homeassistant/components/fritz/translations/id.json +++ b/homeassistant/components/fritz/translations/id.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Wakti dalam detik untuk mempertimbangkan perangkat sebagai 'di rumah'" + "consider_home": "Wakti dalam detik untuk mempertimbangkan perangkat sebagai 'di rumah'", + "old_discovery": "Aktifkan metode penemuan lawas" } } } diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index 91bbd571ab9..2cc46fefdb1 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -1,4 +1,16 @@ { + "config": { + "abort": { + "latitude_longitude_not_defined": "La latitud y la longitud no est\u00e1n definidas en Home Assistant." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u00bfMostrar en el mapa?" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/nanoleaf/translations/id.json b/homeassistant/components/nanoleaf/translations/id.json index f17c0de7209..a638cc19fbc 100644 --- a/homeassistant/components/nanoleaf/translations/id.json +++ b/homeassistant/components/nanoleaf/translations/id.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Usap ke bawah", + "swipe_left": "Usap ke Kiri", + "swipe_right": "Usap ke Kanan", + "swipe_up": "Usap ke Atas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/ja.json b/homeassistant/components/nanoleaf/translations/ja.json index 824c4193f87..a72b64a991c 100644 --- a/homeassistant/components/nanoleaf/translations/ja.json +++ b/homeassistant/components/nanoleaf/translations/ja.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "\u4e0b\u306b\u30b9\u30ef\u30a4\u30d7", + "swipe_left": "\u5de6\u306b\u30b9\u30ef\u30a4\u30d7", + "swipe_right": "\u53f3\u306b\u30b9\u30ef\u30a4\u30d7", + "swipe_up": "\u4e0a\u306b\u30b9\u30ef\u30a4\u30d7" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/no.json b/homeassistant/components/nanoleaf/translations/no.json index 5c06bc4b811..37240468676 100644 --- a/homeassistant/components/nanoleaf/translations/no.json +++ b/homeassistant/components/nanoleaf/translations/no.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Sveip ned", + "swipe_left": "Sveip til venstre", + "swipe_right": "Sveip til h\u00f8yre", + "swipe_up": "Sveip opp" + } } } \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/id.json b/homeassistant/components/radio_browser/translations/id.json new file mode 100644 index 00000000000..a06ce8b840d --- /dev/null +++ b/homeassistant/components/radio_browser/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin menambahkan Radio Browser ke Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index dd1bb23c56a..a8a4f958cb6 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -61,6 +61,7 @@ "debug": "Activa la depuraci\u00f3", "device": "Selecciona el dispositiu a configurar", "event_code": "Introdueix el codi de l'esdeveniment a afegir", + "protocols": "Protocols", "remove_device": "Selecciona el dispositiu a eliminar" }, "title": "Opcions de Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/el.json b/homeassistant/components/rfxtrx/translations/el.json index b04b6172969..8d55c7252a0 100644 --- a/homeassistant/components/rfxtrx/translations/el.json +++ b/homeassistant/components/rfxtrx/translations/el.json @@ -61,6 +61,7 @@ "debug": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd", "device": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd", "event_code": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7", + "protocols": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1", "remove_device": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 86242a4e973..a6f1c925fbf 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -66,6 +66,7 @@ "debug": "Enged\u00e9lyezze a hibakeres\u00e9st", "device": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt", "event_code": "\u00cdrja be a hozz\u00e1adni k\u00edv\u00e1nt esem\u00e9ny k\u00f3dj\u00e1t", + "protocols": "Protokollok", "remove_device": "V\u00e1lassza ki a t\u00f6r\u00f6lni k\u00edv\u00e1nt eszk\u00f6zt" }, "title": "Rfxtrx opci\u00f3k" diff --git a/homeassistant/components/sense/translations/id.json b/homeassistant/components/sense/translations/id.json index 6767f1a54ca..8f9fc17e2c0 100644 --- a/homeassistant/components/sense/translations/id.json +++ b/homeassistant/components/sense/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_validate": { + "data": { + "password": "Kata Sandi" + }, + "description": "Integrasi Sense perlu mengautentikasi ulang akun Anda {email}.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "email": "Email", @@ -16,6 +24,12 @@ "timeout": "Tenggang waktu" }, "title": "Hubungkan ke Sense Energy Monitor Anda" + }, + "validation": { + "data": { + "code": "Kode verifikasi" + }, + "title": "Autentikasi Multifaktor Sense" } } } diff --git a/homeassistant/components/sonarr/translations/id.json b/homeassistant/components/sonarr/translations/id.json index 9d906a07f91..ec76bf44491 100644 --- a/homeassistant/components/sonarr/translations/id.json +++ b/homeassistant/components/sonarr/translations/id.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {host}", + "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {url}", "title": "Autentikasi Ulang Integrasi" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Port", "ssl": "Menggunakan sertifikat SSL", + "url": "URL", "verify_ssl": "Verifikasi sertifikat SSL" } } From a3d30f6ecca51133b4f9c7f85a3f6f253d1d26d7 Mon Sep 17 00:00:00 2001 From: pailloM <56462552+pailloM@users.noreply.github.com> Date: Sat, 26 Feb 2022 03:46:16 -0500 Subject: [PATCH 0069/1054] Re-enable apcupsd (#67264) --- homeassistant/components/apcupsd/manifest.json | 1 - requirements_all.txt | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 18d5549ef9a..13a08685c68 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,5 +1,4 @@ { - "disabled": "Integration library not compatible with Python 3.10", "domain": "apcupsd", "name": "apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", diff --git a/requirements_all.txt b/requirements_all.txt index 3b20cc58879..167e6c6ce72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -307,6 +307,9 @@ anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav anthemav==1.2.0 +# homeassistant.components.apcupsd +apcaccess==0.0.13 + # homeassistant.components.apprise apprise==0.9.7 From 34d38c7ada63f5e92aea10f0c4edcadf48704a65 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sat, 26 Feb 2022 00:56:07 -0800 Subject: [PATCH 0070/1054] Fix Doorbird warning if registering favorites fail (#67262) --- homeassistant/components/doorbird/__init__.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 06264153af2..502ff453a27 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus import logging +from typing import Any from aiohttp import web from doorbirdpy import DoorBird @@ -166,7 +167,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _async_register_events(hass, doorstation): +async def _async_register_events( + hass: HomeAssistant, doorstation: ConfiguredDoorBird +) -> bool: try: await hass.async_add_executor_job(doorstation.register_events, hass) except requests.exceptions.HTTPError: @@ -243,7 +246,7 @@ class ConfiguredDoorBird: """Get token for device.""" return self._token - def register_events(self, hass): + def register_events(self, hass: HomeAssistant) -> None: """Register events on device.""" # Get the URL of this server hass_url = get_url(hass) @@ -258,9 +261,10 @@ class ConfiguredDoorBird: favorites = self.device.favorites() for event in self.doorstation_events: - self._register_event(hass_url, event, favs=favorites) - - _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) + if self._register_event(hass_url, event, favs=favorites): + _LOGGER.info( + "Successfully registered URL for %s on %s", event, self.name + ) @property def slug(self): @@ -270,21 +274,25 @@ class ConfiguredDoorBird: def _get_event_name(self, event): return f"{self.slug}_{event}" - def _register_event(self, hass_url, event, favs=None): + def _register_event( + self, hass_url: str, event: str, favs: dict[str, Any] | None = None + ) -> bool: """Add a schedule entry in the device for a sensor.""" url = f"{hass_url}{API_URL}/{event}?token={self._token}" # Register HA URL as webhook if not already, then get the ID if self.webhook_is_registered(url, favs=favs): - return + return True self.device.change_favorite("http", f"Home Assistant ({event})", url) if not self.webhook_is_registered(url): _LOGGER.warning( - 'Could not find favorite for URL "%s". ' 'Skipping sensor "%s"', + 'Unable to set favorite URL "%s". ' 'Event "%s" will not fire', url, event, ) + return False + return True def webhook_is_registered(self, url, favs=None) -> bool: """Return whether the given URL is registered as a device favorite.""" From 442e2eecd58707ac40879328ae88279e37e5c80b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 00:58:45 -0800 Subject: [PATCH 0071/1054] Kodi: Mark MJPEG cameras using PNGs as incompatible (#67257) --- homeassistant/components/kodi/browse_media.py | 13 ++++++++++++- homeassistant/components/kodi/media_player.py | 11 +++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 519c4dc7c1b..73247d23a9d 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -186,6 +186,15 @@ async def item_payload(item, get_thumbnail_url=None): ) +def media_source_content_filter(item: BrowseMedia) -> bool: + """Content filter for media sources.""" + # Filter out cameras using PNG over MJPEG. They don't work in Kodi. + return not ( + item.media_content_id.startswith("media-source://camera/") + and item.media_content_type == "image/png" + ) + + async def library_payload(hass): """ Create response payload to describe contents of a specific library. @@ -228,7 +237,9 @@ async def library_payload(hass): child.thumbnail = "https://brands.home-assistant.io/_/kodi/logo.png" with contextlib.suppress(media_source.BrowseError): - item = await media_source.async_browse_media(hass, None) + item = await media_source.async_browse_media( + hass, None, content_filter=media_source_content_filter + ) # If domain is None, it's overview of available sources if item.domain is None: library_info.children.extend(item.children) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 56b0abb6a15..53798a7ccd9 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -77,7 +77,12 @@ from homeassistant.helpers.network import is_internal_request from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .browse_media import build_item_response, get_media_info, library_payload +from .browse_media import ( + build_item_response, + get_media_info, + library_payload, + media_source_content_filter, +) from .const import ( CONF_WS_PORT, DATA_CONNECTION, @@ -916,7 +921,9 @@ class KodiEntity(MediaPlayerEntity): return await library_payload(self.hass) if media_source.is_media_source_id(media_content_id): - return await media_source.async_browse_media(self.hass, media_content_id) + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=media_source_content_filter + ) payload = { "search_type": media_content_type, From 7e4b63690d4a42a4b07fb8b2e3bc18fcf50ef88c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 01:02:13 -0800 Subject: [PATCH 0072/1054] Fix camera content type while browsing (#67256) --- .../components/camera/media_source.py | 15 +++++++++------ tests/components/camera/test_media_source.py | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index ab7661fefe2..e65aabe459d 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -73,10 +73,7 @@ class CameraMediaSource(MediaSource): if item.identifier: raise BrowseError("Unknown item") - supported_stream_types: list[str | None] = [None] - - if "stream" in self.hass.config.components: - supported_stream_types.append(STREAM_TYPE_HLS) + can_stream_hls = "stream" in self.hass.config.components # Root. List cameras. component: EntityComponent = self.hass.data[DOMAIN] @@ -86,7 +83,13 @@ class CameraMediaSource(MediaSource): camera = cast(Camera, camera) stream_type = camera.frontend_stream_type - if stream_type not in supported_stream_types: + if stream_type is None: + content_type = camera.content_type + + elif can_stream_hls and stream_type == STREAM_TYPE_HLS: + content_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] + + else: not_shown += 1 continue @@ -95,7 +98,7 @@ class CameraMediaSource(MediaSource): domain=DOMAIN, identifier=camera.entity_id, media_class=MEDIA_CLASS_VIDEO, - media_content_type=FORMAT_CONTENT_TYPE[HLS_PROVIDER], + media_content_type=content_type, title=camera.name, thumbnail=f"/api/camera_proxy/{camera.entity_id}", can_play=True, diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index 54d6ef6279e..b9fb22c9ed8 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -15,21 +15,35 @@ async def setup_media_source(hass): assert await async_setup_component(hass, "media_source", {}) -async def test_browsing(hass, mock_camera_hls): +async def test_browsing_hls(hass, mock_camera_hls): """Test browsing camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None assert item.title == "Camera" assert len(item.children) == 0 + assert item.not_shown == 2 # Adding stream enables HLS camera hass.config.components.add("stream") item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 0 assert len(item.children) == 2 + assert item.children[0].media_content_type == FORMAT_CONTENT_TYPE["hls"] -async def test_browsing_filter_non_hls(hass, mock_camera_web_rtc): +async def test_browsing_mjpeg(hass, mock_camera): + """Test browsing camera media source.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item is not None + assert item.title == "Camera" + assert len(item.children) == 2 + assert item.not_shown == 0 + assert item.children[0].media_content_type == "image/jpg" + assert item.children[1].media_content_type == "image/png" + + +async def test_browsing_filter_web_rtc(hass, mock_camera_web_rtc): """Test browsing camera media source hides non-HLS cameras.""" item = await media_source.async_browse_media(hass, "media-source://camera") assert item is not None From 452b072bd25c1296c504c24e12bd5a08869f7b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Feb 2022 10:38:19 +0100 Subject: [PATCH 0073/1054] Bump aiogithubapi from 22.2.3 to 22.2.4 (#67269) --- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 196095a5b6e..f252cf99f61 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,7 +3,7 @@ "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "aiogithubapi==22.2.3" + "aiogithubapi==22.2.4" ], "codeowners": [ "@timmo001", diff --git a/requirements_all.txt b/requirements_all.txt index 167e6c6ce72..374c2811181 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aioflo==2021.11.0 aioftp==0.12.0 # homeassistant.components.github -aiogithubapi==22.2.3 +aiogithubapi==22.2.4 # homeassistant.components.guardian aioguardian==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2728d40ff2..1887aae0db8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -125,7 +125,7 @@ aioesphomeapi==10.8.2 aioflo==2021.11.0 # homeassistant.components.github -aiogithubapi==22.2.3 +aiogithubapi==22.2.4 # homeassistant.components.guardian aioguardian==2021.11.0 From 73fdd47d54248030aaae97c1d35438429ce2911f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Feb 2022 11:50:05 +0100 Subject: [PATCH 0074/1054] Change GitHub coordinator name (#67285) --- homeassistant/components/github/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 10f30bb1006..9723d3600f6 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -14,7 +14,7 @@ from aiogithubapi import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, LOGGER +from .const import DEFAULT_UPDATE_INTERVAL, LOGGER GRAPHQL_REPOSITORY_QUERY = """ query ($owner: String!, $repository: String!) { @@ -114,7 +114,7 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): super().__init__( hass, LOGGER, - name=DOMAIN, + name=repository, update_interval=DEFAULT_UPDATE_INTERVAL, ) From e65670fef459016b465ab1c8d2ae10732d14803d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Feb 2022 15:56:36 +0100 Subject: [PATCH 0075/1054] Repository event subscription (#67284) --- homeassistant/components/github/__init__.py | 6 +++- homeassistant/components/github/const.py | 13 +++++++- .../components/github/coordinator.py | 30 +++++++++++++++++-- tests/components/github/common.py | 5 ++++ tests/components/github/test_sensor.py | 14 +++++++-- 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 4ecff6e9648..404aeae11b5 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -38,6 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() + await coordinator.subscribe() hass.data[DOMAIN][repository] = coordinator @@ -45,7 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) - return True @@ -77,6 +77,10 @@ def async_cleanup_device_registry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + repositories: dict[str, GitHubDataUpdateCoordinator] = hass.data[DOMAIN] + for coordinator in repositories.values(): + coordinator.unsubscribe() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/github/const.py b/homeassistant/components/github/const.py index efe9d7baa5e..a186f4684b3 100644 --- a/homeassistant/components/github/const.py +++ b/homeassistant/components/github/const.py @@ -11,7 +11,18 @@ DOMAIN = "github" CLIENT_ID = "1440cafcc86e3ea5d6a2" DEFAULT_REPOSITORIES = ["home-assistant/core", "esphome/esphome"] -DEFAULT_UPDATE_INTERVAL = timedelta(seconds=300) +FALLBACK_UPDATE_INTERVAL = timedelta(hours=1, minutes=30) CONF_ACCESS_TOKEN = "access_token" CONF_REPOSITORIES = "repositories" + + +REFRESH_EVENT_TYPES = ( + "CreateEvent", + "ForkEvent", + "IssuesEvent", + "PullRequestEvent", + "PushEvent", + "ReleaseEvent", + "WatchEvent", +) diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 9723d3600f6..679c3d89aeb 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -6,15 +6,17 @@ from typing import Any from aiogithubapi import ( GitHubAPI, GitHubConnectionException, + GitHubEventModel, GitHubException, GitHubRatelimitException, GitHubResponseModel, ) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_UPDATE_INTERVAL, LOGGER +from .const import FALLBACK_UPDATE_INTERVAL, LOGGER, REFRESH_EVENT_TYPES GRAPHQL_REPOSITORY_QUERY = """ query ($owner: String!, $repository: String!) { @@ -109,13 +111,14 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self.repository = repository self._client = client self._last_response: GitHubResponseModel[dict[str, Any]] | None = None + self._subscription_id: str | None = None self.data = {} super().__init__( hass, LOGGER, name=repository, - update_interval=DEFAULT_UPDATE_INTERVAL, + update_interval=FALLBACK_UPDATE_INTERVAL, ) async def _async_update_data(self) -> GitHubResponseModel[dict[str, Any]]: @@ -136,3 +139,26 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): else: self._last_response = response return response.data["data"]["repository"] + + async def _handle_event(self, event: GitHubEventModel) -> None: + """Handle an event.""" + if event.type in REFRESH_EVENT_TYPES: + await self.async_request_refresh() + + @staticmethod + async def _handle_error(error: GitHubException) -> None: + """Handle an error.""" + LOGGER.error("An error occurred while processing new events - %s", error) + + async def subscribe(self) -> None: + """Subscribe to repository events.""" + self._subscription_id = await self._client.repos.events.subscribe( + self.repository, + event_callback=self._handle_event, + error_callback=self._handle_error, + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.unsubscribe) + + def unsubscribe(self, *args) -> None: + """Unsubscribe to repository events.""" + self._client.repos.events.unsubscribe(subscription_id=self._subscription_id) diff --git a/tests/components/github/common.py b/tests/components/github/common.py index a75a8cfaa78..4bd26183299 100644 --- a/tests/components/github/common.py +++ b/tests/components/github/common.py @@ -31,6 +31,11 @@ async def setup_github_integration( }, headers=headers, ) + aioclient_mock.get( + f"https://api.github.com/repos/{repository}/events", + json=[], + headers=headers, + ) aioclient_mock.post( "https://api.github.com/graphql", json=json.loads(load_fixture("graphql.json", DOMAIN)), diff --git a/tests/components/github/test_sensor.py b/tests/components/github/test_sensor.py index cba787cbc28..892fb8956e6 100644 --- a/tests/components/github/test_sensor.py +++ b/tests/components/github/test_sensor.py @@ -1,10 +1,12 @@ """Test GitHub sensor.""" import json -from homeassistant.components.github.const import DEFAULT_UPDATE_INTERVAL, DOMAIN +from homeassistant.components.github.const import DOMAIN, FALLBACK_UPDATE_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.util import dt +from .common import TEST_REPOSITORY + from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -22,15 +24,21 @@ async def test_sensor_updates_with_empty_release_array( response_json = json.loads(load_fixture("graphql.json", DOMAIN)) response_json["data"]["repository"]["release"] = None + headers = json.loads(load_fixture("base_headers.json", DOMAIN)) aioclient_mock.clear_requests() + aioclient_mock.get( + f"https://api.github.com/repos/{TEST_REPOSITORY}/events", + json=[], + headers=headers, + ) aioclient_mock.post( "https://api.github.com/graphql", json=response_json, - headers=json.loads(load_fixture("base_headers.json", DOMAIN)), + headers=headers, ) - async_fire_time_changed(hass, dt.utcnow() + DEFAULT_UPDATE_INTERVAL) + async_fire_time_changed(hass, dt.utcnow() + FALLBACK_UPDATE_INTERVAL) await hass.async_block_till_done() new_state = hass.states.get(TEST_SENSOR_ENTITY) From 858c09060df6c52a6e18ee830192fb6d8406faf4 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Sat, 26 Feb 2022 11:55:11 -0500 Subject: [PATCH 0076/1054] Validate SleepIQ connection/credentials for the import step (#67292) --- .../components/sleepiq/config_flow.py | 33 ++++++++----- tests/components/sleepiq/test_config_flow.py | 46 +++++++++++-------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index dffb30f39d7..47e08fdfd5b 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure SleepIQ component.""" from __future__ import annotations +import logging from typing import Any from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException @@ -14,6 +15,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SleepIQ config flow.""" @@ -28,6 +31,10 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(import_config[CONF_USERNAME].lower()) self._abort_if_unique_id_configured() + if error := await try_connection(self.hass, import_config): + _LOGGER.error("Could not authenticate with SleepIQ server: %s", error) + return self.async_abort(reason=error) + return self.async_create_entry( title=import_config[CONF_USERNAME], data=import_config ) @@ -43,26 +50,23 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) self._abort_if_unique_id_configured() - try: - await try_connection(self.hass, user_input) - except SleepIQLoginException: - errors["base"] = "invalid_auth" - except SleepIQTimeoutException: - errors["base"] = "cannot_connect" + if error := await try_connection(self.hass, user_input): + errors["base"] = error else: return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) + else: + user_input = {} + return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( CONF_USERNAME, - default=user_input.get(CONF_USERNAME) - if user_input is not None - else "", + default=user_input.get(CONF_USERNAME), ): str, vol.Required(CONF_PASSWORD): str, } @@ -72,10 +76,17 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> None: +async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str | None: """Test if the given credentials can successfully login to SleepIQ.""" client_session = async_get_clientsession(hass) gateway = AsyncSleepIQ(client_session=client_session) - await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + try: + await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + except SleepIQLoginException: + return "invalid_auth" + except SleepIQTimeoutException: + return "cannot_connect" + + return None diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index b2554ea968e..516a783f302 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from asyncsleepiq import SleepIQLoginException, SleepIQTimeoutException +import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.sleepiq.const import DOMAIN @@ -26,6 +27,21 @@ async def test_import(hass: HomeAssistant) -> None: assert entry.data[CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] +@pytest.mark.parametrize( + "side_effect", [SleepIQLoginException, SleepIQTimeoutException] +) +async def test_import_failure(hass: HomeAssistant, side_effect) -> None: + """Test that we won't import a config entry on login failure.""" + with patch( + "asyncsleepiq.AsyncSleepIQ.login", + side_effect=side_effect, + ): + assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: SLEEPIQ_CONFIG}) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" with patch("asyncsleepiq.AsyncSleepIQ.login"): @@ -37,11 +53,18 @@ async def test_show_set_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_login_invalid_auth(hass: HomeAssistant) -> None: - """Test we show user form with appropriate error on login failure.""" +@pytest.mark.parametrize( + "side_effect,error", + [ + (SleepIQLoginException, "invalid_auth"), + (SleepIQTimeoutException, "cannot_connect"), + ], +) +async def test_login_failure(hass: HomeAssistant, side_effect, error) -> None: + """Test that we show user form with appropriate error on login failure.""" with patch( "asyncsleepiq.AsyncSleepIQ.login", - side_effect=SleepIQLoginException, + side_effect=side_effect, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG @@ -49,22 +72,7 @@ async def test_login_invalid_auth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "invalid_auth"} - - -async def test_login_cannot_connect(hass: HomeAssistant) -> None: - """Test we show user form with appropriate error on login failure.""" - with patch( - "asyncsleepiq.AsyncSleepIQ.login", - side_effect=SleepIQTimeoutException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result["errors"] == {"base": error} async def test_success(hass: HomeAssistant) -> None: From f901c61d5473c94ca5fc2369b21c609fdf8aac9e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 08:55:46 -0800 Subject: [PATCH 0077/1054] Add myself as code owner for google calendar integration (#67299) --- CODEOWNERS | 2 ++ homeassistant/components/google/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index ef6ef0271d0..562a0836255 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -353,6 +353,8 @@ homeassistant/components/gogogate2/* @vangorra @bdraco tests/components/gogogate2/* @vangorra @bdraco homeassistant/components/goodwe/* @mletenay @starkillerOG tests/components/goodwe/* @mletenay @starkillerOG +homeassistant/components/google/* @allenporter +tests/components/google/* @allenporter homeassistant/components/google_assistant/* @home-assistant/cloud tests/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 1695c7d0d84..32d4b8f3166 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,7 +7,7 @@ "httplib2==0.19.0", "oauth2client==4.1.3" ], - "codeowners": [], + "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] } From d9abd5efea9a847dd54e5cda80aec29a0ffcabaa Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 26 Feb 2022 19:37:24 +0100 Subject: [PATCH 0078/1054] Fix dhcp None hostname (#67289) * Fix dhcp None hostname * Test handle None hostname --- homeassistant/components/dhcp/__init__.py | 16 +++++++++------- tests/components/dhcp/test_init.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index a3de0e51708..0b5f8a49a34 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -159,7 +159,7 @@ class WatcherBase: async def async_start(self): """Start the watcher.""" - def process_client(self, ip_address, hostname, mac_address): + def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None: """Process a client.""" return run_callback_threadsafe( self.hass.loop, @@ -170,7 +170,9 @@ class WatcherBase: ).result() @callback - def async_process_client(self, ip_address, hostname, mac_address): + def async_process_client( + self, ip_address: str, hostname: str, mac_address: str + ) -> None: """Process a client.""" made_ip_address = make_ip_address(ip_address) @@ -355,15 +357,15 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): async def async_start(self): """Stop watching for device tracker registrations.""" self._unsub = async_dispatcher_connect( - self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_state + self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data ) @callback - def _async_process_device_state(self, data: dict[str, Any]) -> None: + def _async_process_device_data(self, data: dict[str, str | None]) -> None: """Process a device tracker state.""" - ip_address = data.get(ATTR_IP) - hostname = data.get(ATTR_HOST_NAME, "") - mac_address = data.get(ATTR_MAC) + ip_address = data[ATTR_IP] + hostname = data[ATTR_HOST_NAME] or "" + mac_address = data[ATTR_MAC] if ip_address is None or mac_address is None: return diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index d1b8d72be67..a809d6eb5ab 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -663,6 +663,28 @@ async def test_device_tracker_registered(hass): await hass.async_block_till_done() +async def test_device_tracker_registered_hostname_none(hass): + """Test handle None hostname.""" + with patch.object(hass.config_entries.flow, "async_init") as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerRegisteredWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + async_dispatcher_send( + hass, + CONNECTED_DEVICE_REGISTERED, + {"ip": "192.168.210.56", "mac": "b8b7f16db533", "host_name": None}, + ) + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + async def test_device_tracker_hostname_and_macaddress_after_start(hass): """Test matching based on hostname and macaddress after start.""" From 1cc4ae2bdd73672e601655a9d3fca62ba48ab72a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 10:47:42 -0800 Subject: [PATCH 0079/1054] Bump googel-api-python-client to 2.3.8 (last updated 2017) (#67298) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 32d4b8f3166..bd4355ac4e3 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -3,7 +3,7 @@ "name": "Google Calendars", "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": [ - "google-api-python-client==1.6.4", + "google-api-python-client==2.38.0", "httplib2==0.19.0", "oauth2client==4.1.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 374c2811181..81b428e10b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ goalzero==0.2.1 goodwe==0.2.15 # homeassistant.components.google -google-api-python-client==1.6.4 +google-api-python-client==2.38.0 # homeassistant.components.google_pubsub google-cloud-pubsub==2.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1887aae0db8..cd08cbc1273 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -493,7 +493,7 @@ goalzero==0.2.1 goodwe==0.2.15 # homeassistant.components.google -google-api-python-client==1.6.4 +google-api-python-client==2.38.0 # homeassistant.components.google_pubsub google-cloud-pubsub==2.9.0 From 1ee4f47f8be3cd56f00e8fec35ce445436c80b82 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 12:30:04 -0800 Subject: [PATCH 0080/1054] Cleanup google calendar use of hass.data (#67305) Update google calendar's use of hass.data to follow the best practices of storing data under hass.data[DOMAIN]. This is in preparation for storing additional data later, pulled out of a larger cleanup. --- homeassistant/components/google/__init__.py | 24 +++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 9f85d41774b..3ab50cc0a56 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -75,7 +75,7 @@ SERVICE_SCAN_CALENDARS = "scan_for_calendars" SERVICE_FOUND_CALENDARS = "found_calendar" SERVICE_ADD_EVENT = "add_event" -DATA_INDEX = "google_calendars" +DATA_CALENDARS = "calendars" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" @@ -244,8 +244,7 @@ def do_authentication( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Google platform.""" - if DATA_INDEX not in hass.data: - hass.data[DATA_INDEX] = {} + hass.data[DOMAIN] = {DATA_CALENDARS: {}} if not (conf := config.get(DOMAIN, {})): # component is set up by tts platform @@ -305,20 +304,16 @@ def setup_services( def _found_calendar(call: ServiceCall) -> None: """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" calendar = get_calendar_info(hass, call.data) - if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID]) is not None: + calendar_id = calendar[CONF_CAL_ID] + if calendar_id in hass.data[DOMAIN][DATA_CALENDARS]: return - - hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar}) - - update_config( - hass.config.path(YAML_DEVICES), hass.data[DATA_INDEX][calendar[CONF_CAL_ID]] - ) - + hass.data[DOMAIN][DATA_CALENDARS][calendar_id] = calendar + update_config(hass.config.path(YAML_DEVICES), calendar) discovery.load_platform( hass, Platform.CALENDAR, DOMAIN, - hass.data[DATA_INDEX][calendar[CONF_CAL_ID]], + calendar, hass_config, ) @@ -392,7 +387,8 @@ def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) - """Run the setup after we have everything configured.""" _LOGGER.debug("Setting up integration") # Load calendars the user has configured - hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES)) + calendars = load_config(hass.config.path(YAML_DEVICES)) + hass.data[DOMAIN][DATA_CALENDARS] = calendars calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) track_new_found_calendars = convert( @@ -403,7 +399,7 @@ def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) - hass, hass_config, config, track_new_found_calendars, calendar_service ) - for calendar in hass.data[DATA_INDEX].values(): + for calendar in calendars.values(): discovery.load_platform(hass, Platform.CALENDAR, DOMAIN, calendar, hass_config) # Look for any new calendars From 648aa0cae64fde4148467276ba79f5b65fff136e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 13:23:11 -0800 Subject: [PATCH 0081/1054] Reduce google calendar test flake (#67310) --- homeassistant/components/google/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 3ab50cc0a56..f35f79a5f92 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,4 +1,6 @@ """Support for Google - Calendar Event Devices.""" +from __future__ import annotations + from collections.abc import Mapping from datetime import datetime, timedelta, timezone from enum import Enum @@ -28,7 +30,7 @@ from homeassistant.const import ( CONF_OFFSET, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id @@ -196,6 +198,8 @@ def do_authentication( notification_id=NOTIFICATION_ID, ) + listener: CALLBACK_TYPE | None = None + def step2_exchange(now: datetime) -> None: """Keep trying to validate the user_code until it expires.""" _LOGGER.debug("Attempting to validate user code") @@ -212,6 +216,7 @@ def do_authentication( title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, ) + assert listener listener() return @@ -224,6 +229,7 @@ def do_authentication( storage = Storage(hass.config.path(TOKEN_FILE)) storage.put(credentials) do_setup(hass, hass_config, config) + assert listener listener() persistent_notification.create( hass, @@ -236,7 +242,7 @@ def do_authentication( ) listener = track_utc_time_change( - hass, step2_exchange, second=range(0, 60, dev_flow.interval) + hass, step2_exchange, second=range(1, 60, dev_flow.interval) ) return True From fbfdabe4fc7df878517e004f4dea763fcbab6625 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Feb 2022 22:26:41 +0100 Subject: [PATCH 0082/1054] Remove deprecated Raspberry Pi RF integration (#67283) --- .coveragerc | 1 - .github/workflows/wheels.yml | 1 - homeassistant/components/rpi_rf/__init__.py | 1 - homeassistant/components/rpi_rf/manifest.json | 8 - homeassistant/components/rpi_rf/switch.py | 153 ------------------ requirements_all.txt | 4 - script/gen_requirements_all.py | 1 - 7 files changed, 169 deletions(-) delete mode 100644 homeassistant/components/rpi_rf/__init__.py delete mode 100644 homeassistant/components/rpi_rf/manifest.json delete mode 100644 homeassistant/components/rpi_rf/switch.py diff --git a/.coveragerc b/.coveragerc index d3c08769095..47f8b12287c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -992,7 +992,6 @@ omit = homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* homeassistant/components/rpi_pfio/* - homeassistant/components/rpi_rf/switch.py homeassistant/components/rtorrent/sensor.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 30f8e0bdac9..0dc006885f8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -136,7 +136,6 @@ jobs: sed -i "s|# beacontools|beacontools|g" ${requirement_file} sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} sed -i "s|# raspihats|raspihats|g" ${requirement_file} - sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} diff --git a/homeassistant/components/rpi_rf/__init__.py b/homeassistant/components/rpi_rf/__init__.py deleted file mode 100644 index 6e4d58099d9..00000000000 --- a/homeassistant/components/rpi_rf/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The rpi_rf component.""" diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json deleted file mode 100644 index 022c84eb13f..00000000000 --- a/homeassistant/components/rpi_rf/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "rpi_rf", - "name": "Raspberry Pi RF", - "documentation": "https://www.home-assistant.io/integrations/rpi_rf", - "requirements": ["rpi-rf==0.9.7", "RPi.GPIO==0.7.1a4"], - "codeowners": [], - "iot_class": "assumed_state" -} diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py deleted file mode 100644 index e82b2cb12b1..00000000000 --- a/homeassistant/components/rpi_rf/switch.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Support for a switch using a 433MHz module via GPIO on a Raspberry Pi.""" -from __future__ import annotations - -import importlib -import logging -from threading import RLock - -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import ( - CONF_NAME, - CONF_PROTOCOL, - CONF_SWITCHES, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -CONF_CODE_OFF = "code_off" -CONF_CODE_ON = "code_on" -CONF_GPIO = "gpio" -CONF_PULSELENGTH = "pulselength" -CONF_SIGNAL_REPETITIONS = "signal_repetitions" - -DEFAULT_PROTOCOL = 1 -DEFAULT_SIGNAL_REPETITIONS = 10 - -SWITCH_SCHEMA = vol.Schema( - { - vol.Required(CONF_CODE_OFF): vol.All(cv.ensure_list_csv, [cv.positive_int]), - vol.Required(CONF_CODE_ON): vol.All(cv.ensure_list_csv, [cv.positive_int]), - vol.Optional(CONF_PULSELENGTH): cv.positive_int, - vol.Optional( - CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS - ): cv.positive_int, - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): cv.positive_int, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_GPIO): cv.positive_int, - vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCH_SCHEMA}), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Find and return switches controlled by a generic RF device via GPIO.""" - _LOGGER.warning( - "The Raspberry Pi RF integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - rpi_rf = importlib.import_module("rpi_rf") - - gpio = config[CONF_GPIO] - rfdevice = rpi_rf.RFDevice(gpio) - rfdevice_lock = RLock() - switches = config[CONF_SWITCHES] - - devices = [] - for dev_name, properties in switches.items(): - devices.append( - RPiRFSwitch( - properties.get(CONF_NAME, dev_name), - rfdevice, - rfdevice_lock, - properties.get(CONF_PROTOCOL), - properties.get(CONF_PULSELENGTH), - properties.get(CONF_SIGNAL_REPETITIONS), - properties.get(CONF_CODE_ON), - properties.get(CONF_CODE_OFF), - ) - ) - if devices: - rfdevice.enable_tx() - - add_entities(devices) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: rfdevice.cleanup()) - - -class RPiRFSwitch(SwitchEntity): - """Representation of a GPIO RF switch.""" - - def __init__( - self, - name, - rfdevice, - lock, - protocol, - pulselength, - signal_repetitions, - code_on, - code_off, - ): - """Initialize the switch.""" - self._name = name - self._state = False - self._rfdevice = rfdevice - self._lock = lock - self._protocol = protocol - self._pulselength = pulselength - self._code_on = code_on - self._code_off = code_off - self._rfdevice.tx_repeat = signal_repetitions - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def _send_code(self, code_list, protocol, pulselength): - """Send the code(s) with a specified pulselength.""" - with self._lock: - _LOGGER.info("Sending code(s): %s", code_list) - for code in code_list: - self._rfdevice.tx_code(code, protocol, pulselength) - return True - - def turn_on(self, **kwargs): - """Turn the switch on.""" - if self._send_code(self._code_on, self._protocol, self._pulselength): - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the switch off.""" - if self._send_code(self._code_off, self._protocol, self._pulselength): - self._state = False - self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 81b428e10b8..3cfbc63a9fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -53,7 +53,6 @@ PyXiaomiGateway==0.13.4 # homeassistant.components.dht # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio -# homeassistant.components.rpi_rf # RPi.GPIO==0.7.1a4 # homeassistant.components.remember_the_milk @@ -2102,9 +2101,6 @@ rova==0.2.1 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 -# homeassistant.components.rpi_rf -# rpi-rf==0.9.7 - # homeassistant.components.rtsp_to_webrtc rtsp-to-webrtc==0.5.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c853fb8f678..f02b3596630 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -37,7 +37,6 @@ COMMENT_REQUIREMENTS = ( "python-lirc", "pyuserinput", "raspihats", - "rpi-rf", "RPi.GPIO", "smbus-cffi", "tensorflow", From 6ec9c402b100d0c5030445cc83855ed6dc9ed1b8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 26 Feb 2022 22:27:48 +0100 Subject: [PATCH 0083/1054] Remove generic data types INT, UINT, FLOAT in modbus (#67268) --- homeassistant/components/modbus/__init__.py | 5 +--- homeassistant/components/modbus/const.py | 3 -- homeassistant/components/modbus/validators.py | 30 +++---------------- tests/components/modbus/test_init.py | 2 +- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index d6fe41f66fc..fd8e15b9b7e 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -136,7 +136,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ] ), vol.Optional(CONF_COUNT): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DataType.INT): vol.In( + vol.Optional(CONF_DATA_TYPE, default=DataType.INT16): vol.In( [ DataType.INT8, DataType.INT16, @@ -150,9 +150,6 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( DataType.FLOAT32, DataType.FLOAT64, DataType.STRING, - DataType.INT, - DataType.UINT, - DataType.FLOAT, DataType.STRING, DataType.CUSTOM, ] diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 7244e2a16e6..b09d75f27e0 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -76,9 +76,6 @@ class DataType(str, Enum): """Data types used by sensor etc.""" CUSTOM = "custom" - FLOAT = "float" # deprecated - INT = "int" # deprecated - UINT = "uint" # deprecated STRING = "string" INT8 = "int8" INT16 = "int16" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 4f910043d12..646e1129c4e 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -39,23 +39,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -OLD_DATA_TYPES = { - DataType.INT: { - 1: DataType.INT16, - 2: DataType.INT32, - 4: DataType.INT64, - }, - DataType.UINT: { - 1: DataType.UINT16, - 2: DataType.UINT32, - 4: DataType.UINT64, - }, - DataType.FLOAT: { - 1: DataType.FLOAT16, - 2: DataType.FLOAT32, - 4: DataType.FLOAT64, - }, -} ENTRY = namedtuple("ENTRY", ["struct_id", "register_count"]) DEFAULT_STRUCT_FORMAT = { DataType.INT8: ENTRY("b", 1), @@ -81,19 +64,14 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) swap_type = config.get(CONF_SWAP) - if data_type in (DataType.INT, DataType.UINT, DataType.FLOAT): - error = f"{name} with {data_type} is not valid, trying to convert" - _LOGGER.warning(error) - try: - data_type = OLD_DATA_TYPES[data_type][config.get(CONF_COUNT, 1)] - config[CONF_DATA_TYPE] = data_type - except KeyError as exp: - error = f"{name} cannot convert automatically {data_type}" - raise vol.Invalid(error) from exp if config[CONF_DATA_TYPE] != DataType.CUSTOM: if structure: error = f"{name} structure: cannot be mixed with {data_type}" raise vol.Invalid(error) + if data_type not in DEFAULT_STRUCT_FORMAT: + error = f"Error in sensor {name}. data_type `{data_type}` not supported" + raise vol.Invalid(error) + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" if CONF_COUNT not in config: config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index b416ebad537..127389ee50f 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -176,7 +176,7 @@ async def test_ok_struct_validator(do_config): { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, - CONF_DATA_TYPE: DataType.INT, + CONF_DATA_TYPE: "int", }, { CONF_NAME: TEST_ENTITY_NAME, From 3b2b2b9c69ef355e0e2fde6963a3c162fecf8187 Mon Sep 17 00:00:00 2001 From: stegm Date: Sat, 26 Feb 2022 22:32:38 +0100 Subject: [PATCH 0084/1054] Add diagnostics to Kostal Plenticore (#66435) --- .../kostal_plenticore/diagnostics.py | 42 ++++++++ .../components/kostal_plenticore/conftest.py | 96 +++++++++++++++++++ .../kostal_plenticore/test_diagnostics.py | 49 ++++++++++ 3 files changed, 187 insertions(+) create mode 100644 homeassistant/components/kostal_plenticore/diagnostics.py create mode 100644 tests/components/kostal_plenticore/conftest.py create mode 100644 tests/components/kostal_plenticore/test_diagnostics.py diff --git a/homeassistant/components/kostal_plenticore/diagnostics.py b/homeassistant/components/kostal_plenticore/diagnostics.py new file mode 100644 index 00000000000..2e061d35528 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/diagnostics.py @@ -0,0 +1,42 @@ +"""Diagnostics support for Kostal Plenticore.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import REDACTED, async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .helper import Plenticore + +TO_REDACT = {CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, dict[str, Any]]: + """Return diagnostics for a config entry.""" + data = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)} + + plenticore: Plenticore = hass.data[DOMAIN][config_entry.entry_id] + + # Get information from Kostal Plenticore library + available_process_data = await plenticore.client.get_process_data() + available_settings_data = await plenticore.client.get_settings() + data["client"] = { + "version": str(await plenticore.client.get_version()), + "me": str(await plenticore.client.get_me()), + "available_process_data": available_process_data, + "available_settings_data": { + module_id: [str(setting) for setting in settings] + for module_id, settings in available_settings_data.items() + }, + } + + device_info = {**plenticore.device_info} + device_info["identifiers"] = REDACTED # contains serial number + data["device"] = device_info + + return data diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py new file mode 100644 index 00000000000..c3ed1b45592 --- /dev/null +++ b/tests/components/kostal_plenticore/conftest.py @@ -0,0 +1,96 @@ +"""Fixtures for Kostal Plenticore tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from kostal.plenticore import MeData, SettingsData, VersionData +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, +) -> Generator[None, MockConfigEntry, None]: + """Set up Kostal Plenticore integration for testing.""" + with patch( + "homeassistant.components.kostal_plenticore.Plenticore", autospec=True + ) as mock_api_class: + # setup + plenticore = mock_api_class.return_value + plenticore.async_setup = AsyncMock() + plenticore.async_setup.return_value = True + + plenticore.device_info = DeviceInfo( + configuration_url="http://192.168.1.2", + identifiers={("kostal_plenticore", "12345")}, + manufacturer="Kostal", + model="PLENTICORE plus 10", + name="scb", + sw_version="IOC: 01.45 MC: 01.46", + ) + + plenticore.client = MagicMock() + + plenticore.client.get_version = AsyncMock() + plenticore.client.get_version.return_value = VersionData( + { + "api_version": "0.2.0", + "hostname": "scb", + "name": "PUCK RESTful API", + "sw_version": "01.16.05025", + } + ) + + plenticore.client.get_me = AsyncMock() + plenticore.client.get_me.return_value = MeData( + { + "locked": False, + "active": True, + "authenticated": True, + "permissions": [], + "anonymous": False, + "role": "USER", + } + ) + + plenticore.client.get_process_data = AsyncMock() + plenticore.client.get_process_data.return_value = { + "devices:local": ["HomeGrid_P", "HomePv_P"] + } + + plenticore.client.get_settings = AsyncMock() + plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData( + { + "id": "Battery:MinSoc", + "unit": "%", + "default": "None", + "min": 5, + "max": 100, + "type": "byte", + "access": "readwrite", + } + ) + ] + } + + mock_config_entry = MockConfigEntry( + entry_id="2ab8dd92a62787ddfe213a67e09406bd", + title="scb", + domain="kostal_plenticore", + data={"host": "192.168.1.2", "password": "SecretPassword"}, + ) + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + yield mock_config_entry diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py new file mode 100644 index 00000000000..56af8bafe06 --- /dev/null +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -0,0 +1,49 @@ +"""Test Kostal Plenticore diagnostics.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry +): + """Test config entry diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "config_entry": { + "entry_id": "2ab8dd92a62787ddfe213a67e09406bd", + "version": 1, + "domain": "kostal_plenticore", + "title": "scb", + "data": {"host": "192.168.1.2", "password": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": None, + "disabled_by": None, + }, + "client": { + "version": "Version(api_version=0.2.0, hostname=scb, name=PUCK RESTful API, sw_version=01.16.05025)", + "me": "Me(locked=False, active=True, authenticated=True, permissions=[] anonymous=False role=USER)", + "available_process_data": {"devices:local": ["HomeGrid_P", "HomePv_P"]}, + "available_settings_data": { + "devices:local": [ + "SettingsData(id=Battery:MinSoc, unit=%, default=None, min=5, max=100,type=byte, access=readwrite)" + ] + }, + }, + "device": { + "configuration_url": "http://192.168.1.2", + "identifiers": "**REDACTED**", + "manufacturer": "Kostal", + "model": "PLENTICORE plus 10", + "name": "scb", + "sw_version": "IOC: 01.45 MC: 01.46", + }, + } From 0b20b107e198de87026fe973847f06c15847491a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Feb 2022 22:33:12 +0100 Subject: [PATCH 0085/1054] Remove deprecated Sense HAT integration (#67272) --- .coveragerc | 2 - homeassistant/components/sensehat/__init__.py | 1 - homeassistant/components/sensehat/light.py | 125 -------------- .../components/sensehat/manifest.json | 9 - homeassistant/components/sensehat/sensor.py | 163 ------------------ requirements_all.txt | 3 - 6 files changed, 303 deletions(-) delete mode 100644 homeassistant/components/sensehat/__init__.py delete mode 100644 homeassistant/components/sensehat/light.py delete mode 100644 homeassistant/components/sensehat/manifest.json delete mode 100644 homeassistant/components/sensehat/sensor.py diff --git a/.coveragerc b/.coveragerc index 47f8b12287c..24bddce6859 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1012,8 +1012,6 @@ omit = homeassistant/components/sense/__init__.py homeassistant/components/sense/binary_sensor.py homeassistant/components/sense/sensor.py - homeassistant/components/sensehat/light.py - homeassistant/components/sensehat/sensor.py homeassistant/components/senseme/__init__.py homeassistant/components/senseme/binary_sensor.py homeassistant/components/senseme/discovery.py diff --git a/homeassistant/components/sensehat/__init__.py b/homeassistant/components/sensehat/__init__.py deleted file mode 100644 index baef85d7f53..00000000000 --- a/homeassistant/components/sensehat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sensehat component.""" diff --git a/homeassistant/components/sensehat/light.py b/homeassistant/components/sensehat/light.py deleted file mode 100644 index c1184576362..00000000000 --- a/homeassistant/components/sensehat/light.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Support for Sense Hat LEDs.""" -from __future__ import annotations - -import logging - -from sense_hat import SenseHat -import voluptuous as vol - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - LightEntity, -) -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util - -SUPPORT_SENSEHAT = SUPPORT_BRIGHTNESS | SUPPORT_COLOR - -DEFAULT_NAME = "sensehat" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Sense Hat Light platform.""" - _LOGGER.warning( - "The Sense HAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - sensehat = SenseHat() - - name = config.get(CONF_NAME) - - add_entities([SenseHatLight(sensehat, name)]) - - -class SenseHatLight(LightEntity): - """Representation of an Sense Hat Light.""" - - def __init__(self, sensehat, name): - """Initialize an Sense Hat Light. - - Full brightness and white color. - """ - self._sensehat = sensehat - self._name = name - self._is_on = False - self._brightness = 255 - self._hs_color = [0, 0] - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def brightness(self): - """Read back the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Read back the color of the light.""" - return self._hs_color - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_SENSEHAT - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def should_poll(self): - """Return if we should poll this device.""" - return False - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return True - - def turn_on(self, **kwargs): - """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_HS_COLOR in kwargs: - self._hs_color = kwargs[ATTR_HS_COLOR] - - rgb = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 - ) - self._sensehat.clear(*rgb) - - self._is_on = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - self._sensehat.clear() - self._is_on = False - self.schedule_update_ha_state() diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json deleted file mode 100644 index 78f6e0609bc..00000000000 --- a/homeassistant/components/sensehat/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "sensehat", - "name": "Sense HAT", - "documentation": "https://www.home-assistant.io/integrations/sensehat", - "requirements": ["sense-hat==2.2.0"], - "codeowners": [], - "iot_class": "assumed_state", - "loggers": ["sense_hat"] -} diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py deleted file mode 100644 index bf9d77104da..00000000000 --- a/homeassistant/components/sensehat/sensor.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Support for Sense HAT sensors.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from pathlib import Path - -from sense_hat import SenseHat -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_DISPLAY_OPTIONS, - CONF_NAME, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "sensehat" -CONF_IS_HAT_ATTACHED = "is_hat_attached" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="humidity", - name="humidity", - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="pressure", - name="pressure", - native_unit_of_measurement="mb", - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_KEYS): [vol.In(SENSOR_KEYS)], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_IS_HAT_ATTACHED, default=True): cv.boolean, - } -) - - -def get_cpu_temp(): - """Get CPU temperature.""" - t_cpu = ( - Path("/sys/class/thermal/thermal_zone0/temp") - .read_text(encoding="utf-8") - .strip() - ) - return float(t_cpu) * 0.001 - - -def get_average(temp_base): - """Use moving average to get better readings.""" - if not hasattr(get_average, "temp"): - get_average.temp = [temp_base, temp_base, temp_base] - get_average.temp[2] = get_average.temp[1] - get_average.temp[1] = get_average.temp[0] - get_average.temp[0] = temp_base - temp_avg = (get_average.temp[0] + get_average.temp[1] + get_average.temp[2]) / 3 - return temp_avg - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Sense HAT sensor platform.""" - _LOGGER.warning( - "The Sense HAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - data = SenseHatData(config.get(CONF_IS_HAT_ATTACHED)) - display_options = config[CONF_DISPLAY_OPTIONS] - entities = [ - SenseHatSensor(data, description) - for description in SENSOR_TYPES - if description.key in display_options - ] - - add_entities(entities, True) - - -class SenseHatSensor(SensorEntity): - """Representation of a Sense HAT sensor.""" - - def __init__(self, data, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self.data = data - - def update(self): - """Get the latest data and updates the states.""" - self.data.update() - if not self.data.humidity: - _LOGGER.error("Don't receive data") - return - - sensor_type = self.entity_description.key - if sensor_type == "temperature": - self._attr_native_value = self.data.temperature - elif sensor_type == "humidity": - self._attr_native_value = self.data.humidity - elif sensor_type == "pressure": - self._attr_native_value = self.data.pressure - - -class SenseHatData: - """Get the latest data and update.""" - - def __init__(self, is_hat_attached): - """Initialize the data object.""" - self.temperature = None - self.humidity = None - self.pressure = None - self.is_hat_attached = is_hat_attached - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from Sense HAT.""" - - sense = SenseHat() - temp_from_h = sense.get_temperature_from_humidity() - temp_from_p = sense.get_temperature_from_pressure() - t_total = (temp_from_h + temp_from_p) / 2 - - if self.is_hat_attached: - t_cpu = get_cpu_temp() - t_correct = t_total - ((t_cpu - t_total) / 1.5) - t_correct = get_average(t_correct) - else: - t_correct = get_average(t_total) - - self.temperature = t_correct - self.humidity = sense.get_humidity() - self.pressure = sense.get_pressure() diff --git a/requirements_all.txt b/requirements_all.txt index 3cfbc63a9fc..a233cd2652d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2137,9 +2137,6 @@ scsgate==0.1.0 # homeassistant.components.sendgrid sendgrid==6.8.2 -# homeassistant.components.sensehat -sense-hat==2.2.0 - # homeassistant.components.emulated_kasa # homeassistant.components.sense sense_energy==0.10.2 From 0abecc85132062fa588a2673f8df91bc53ebef08 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Feb 2022 22:34:34 +0100 Subject: [PATCH 0086/1054] Remove deprecated DHT Sensor integration (#67276) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/dht/__init__.py | 1 - homeassistant/components/dht/manifest.json | 13 -- homeassistant/components/dht/sensor.py | 202 --------------------- requirements_all.txt | 4 - 6 files changed, 222 deletions(-) delete mode 100644 homeassistant/components/dht/__init__.py delete mode 100644 homeassistant/components/dht/manifest.json delete mode 100644 homeassistant/components/dht/sensor.py diff --git a/.coveragerc b/.coveragerc index 24bddce6859..efb01568b80 100644 --- a/.coveragerc +++ b/.coveragerc @@ -212,7 +212,6 @@ omit = homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/subscriber.py homeassistant/components/devolo_home_control/switch.py - homeassistant/components/dht/sensor.py homeassistant/components/digital_ocean/* homeassistant/components/digitalloggers/switch.py homeassistant/components/discogs/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 562a0836255..fb191264d89 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -207,7 +207,6 @@ homeassistant/components/dexcom/* @gagebenne tests/components/dexcom/* @gagebenne homeassistant/components/dhcp/* @bdraco tests/components/dhcp/* @bdraco -homeassistant/components/dht/* @thegardenmonkey homeassistant/components/diagnostics/* @home-assistant/core tests/components/diagnostics/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff diff --git a/homeassistant/components/dht/__init__.py b/homeassistant/components/dht/__init__.py deleted file mode 100644 index 23aa2b9d9df..00000000000 --- a/homeassistant/components/dht/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The dht component.""" diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json deleted file mode 100644 index 3eb3cfd202c..00000000000 --- a/homeassistant/components/dht/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "dht", - "name": "DHT Sensor", - "documentation": "https://www.home-assistant.io/integrations/dht", - "requirements": [ - "adafruit-circuitpython-dht==3.7.0", - "RPi.GPIO==0.7.1a4" - ], - "codeowners": [ - "@thegardenmonkey" - ], - "iot_class": "local_polling" -} \ No newline at end of file diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py deleted file mode 100644 index ef5cc0f97f3..00000000000 --- a/homeassistant/components/dht/sensor.py +++ /dev/null @@ -1,202 +0,0 @@ -"""Support for Adafruit DHT temperature and humidity sensor.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -import adafruit_dht -import board -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_PIN, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_SENSOR = "sensor" -CONF_HUMIDITY_OFFSET = "humidity_offset" -CONF_TEMPERATURE_OFFSET = "temperature_offset" - -DEFAULT_NAME = "DHT Sensor" - -# DHT11 is able to deliver data once per second, DHT22 once every two -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) - -SENSOR_TEMPERATURE = "temperature" -SENSOR_HUMIDITY = "humidity" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMPERATURE, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_HUMIDITY, - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - - -def validate_pin_input(value): - """Validate that the GPIO PIN is prefixed with a D.""" - try: - int(value) - return f"D{value}" - except ValueError: - return value.upper() - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_SENSOR): cv.string, - vol.Required(CONF_PIN): vol.All(cv.string, validate_pin_input), - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TEMPERATURE_OFFSET, default=0): vol.All( - vol.Coerce(float), vol.Range(min=-100, max=100) - ), - vol.Optional(CONF_HUMIDITY_OFFSET, default=0): vol.All( - vol.Coerce(float), vol.Range(min=-100, max=100) - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the DHT sensor.""" - _LOGGER.warning( - "The DHT Sensor integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - available_sensors = { - "AM2302": adafruit_dht.DHT22, - "DHT11": adafruit_dht.DHT11, - "DHT22": adafruit_dht.DHT22, - } - sensor = available_sensors.get(config[CONF_SENSOR]) - pin = config[CONF_PIN] - temperature_offset = config[CONF_TEMPERATURE_OFFSET] - humidity_offset = config[CONF_HUMIDITY_OFFSET] - name = config[CONF_NAME] - - if not sensor: - _LOGGER.error("DHT sensor type is not supported") - return - - data = DHTClient(sensor, pin, name) - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - DHTSensor(data, name, temperature_offset, humidity_offset, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities, True) - - -class DHTSensor(SensorEntity): - """Implementation of the DHT sensor.""" - - def __init__( - self, - dht_client, - name, - temperature_offset, - humidity_offset, - description: SensorEntityDescription, - ): - """Initialize the sensor.""" - self.entity_description = description - self.dht_client = dht_client - self.temperature_offset = temperature_offset - self.humidity_offset = humidity_offset - - self._attr_name = f"{name} {description.name}" - - def update(self): - """Get the latest data from the DHT and updates the states.""" - self.dht_client.update() - temperature_offset = self.temperature_offset - humidity_offset = self.humidity_offset - data = self.dht_client.data - - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMPERATURE and sensor_type in data: - temperature = data[SENSOR_TEMPERATURE] - _LOGGER.debug( - "Temperature %.1f \u00b0C + offset %.1f", - temperature, - temperature_offset, - ) - if -20 <= temperature < 80: - self._attr_native_value = round(temperature + temperature_offset, 1) - elif sensor_type == SENSOR_HUMIDITY and sensor_type in data: - humidity = data[SENSOR_HUMIDITY] - _LOGGER.debug("Humidity %.1f%% + offset %.1f", humidity, humidity_offset) - if 0 <= humidity <= 100: - self._attr_native_value = round(humidity + humidity_offset, 1) - - -class DHTClient: - """Get the latest data from the DHT sensor.""" - - def __init__(self, sensor, pin, name): - """Initialize the sensor.""" - self.sensor = sensor - self.pin = getattr(board, pin) - self.data = {} - self.name = name - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data the DHT sensor.""" - dht = self.sensor(self.pin) - try: - temperature = dht.temperature - humidity = dht.humidity - except RuntimeError: - _LOGGER.debug("Unexpected value from DHT sensor: %s", self.name) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error updating DHT sensor: %s", self.name) - else: - if temperature: - self.data[SENSOR_TEMPERATURE] = temperature - if humidity: - self.data[SENSOR_HUMIDITY] = humidity - finally: - dht.exit() diff --git a/requirements_all.txt b/requirements_all.txt index a233cd2652d..3666621e4a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,6 @@ PyViCare==2.16.1 PyXiaomiGateway==0.13.4 # homeassistant.components.bmp280 -# homeassistant.components.dht # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio # RPi.GPIO==0.7.1a4 @@ -79,9 +78,6 @@ accuweather==0.3.0 # homeassistant.components.bmp280 adafruit-circuitpython-bmp280==3.1.1 -# homeassistant.components.dht -adafruit-circuitpython-dht==3.7.0 - # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==2.2.2 From 9920b3eef5b08c728f35b86f4e3317a8ff85a636 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 26 Feb 2022 22:35:13 +0100 Subject: [PATCH 0087/1054] Remove deprecated Enviro pHAT integration (#67277) --- .coveragerc | 1 - .../components/envirophat/__init__.py | 1 - .../components/envirophat/manifest.json | 8 - homeassistant/components/envirophat/sensor.py | 281 ------------------ requirements_all.txt | 4 - script/gen_requirements_all.py | 1 - 6 files changed, 296 deletions(-) delete mode 100644 homeassistant/components/envirophat/__init__.py delete mode 100644 homeassistant/components/envirophat/manifest.json delete mode 100644 homeassistant/components/envirophat/sensor.py diff --git a/.coveragerc b/.coveragerc index efb01568b80..6508a46c47a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -296,7 +296,6 @@ omit = homeassistant/components/environment_canada/camera.py homeassistant/components/environment_canada/sensor.py homeassistant/components/environment_canada/weather.py - homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py homeassistant/components/epson/__init__.py diff --git a/homeassistant/components/envirophat/__init__.py b/homeassistant/components/envirophat/__init__.py deleted file mode 100644 index 68d3a99441c..00000000000 --- a/homeassistant/components/envirophat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The envirophat component.""" diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json deleted file mode 100644 index 9bb90facbf3..00000000000 --- a/homeassistant/components/envirophat/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "envirophat", - "name": "Enviro pHAT", - "documentation": "https://www.home-assistant.io/integrations/envirophat", - "requirements": ["envirophat==0.0.6", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py deleted file mode 100644 index 0b8634d019d..00000000000 --- a/homeassistant/components/envirophat/sensor.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Support for Enviro pHAT sensors.""" -from __future__ import annotations - -from datetime import timedelta -import importlib -import logging - -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_DISPLAY_OPTIONS, - CONF_NAME, - ELECTRIC_POTENTIAL_VOLT, - PRESSURE_HPA, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "envirophat" -CONF_USE_LEDS = "use_leds" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="light", - name="light", - icon="mdi:weather-sunny", - ), - SensorEntityDescription( - key="light_red", - name="light_red", - icon="mdi:invert-colors", - ), - SensorEntityDescription( - key="light_green", - name="light_green", - icon="mdi:invert-colors", - ), - SensorEntityDescription( - key="light_blue", - name="light_blue", - icon="mdi:invert-colors", - ), - SensorEntityDescription( - key="accelerometer_x", - name="accelerometer_x", - native_unit_of_measurement="G", - icon="mdi:earth", - ), - SensorEntityDescription( - key="accelerometer_y", - name="accelerometer_y", - native_unit_of_measurement="G", - icon="mdi:earth", - ), - SensorEntityDescription( - key="accelerometer_z", - name="accelerometer_z", - native_unit_of_measurement="G", - icon="mdi:earth", - ), - SensorEntityDescription( - key="magnetometer_x", - name="magnetometer_x", - icon="mdi:magnet", - ), - SensorEntityDescription( - key="magnetometer_y", - name="magnetometer_y", - icon="mdi:magnet", - ), - SensorEntityDescription( - key="magnetometer_z", - name="magnetometer_z", - icon="mdi:magnet", - ), - SensorEntityDescription( - key="temperature", - name="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="pressure", - name="pressure", - native_unit_of_measurement=PRESSURE_HPA, - icon="mdi:gauge", - ), - SensorEntityDescription( - key="voltage_0", - name="voltage_0", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), - SensorEntityDescription( - key="voltage_1", - name="voltage_1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), - SensorEntityDescription( - key="voltage_2", - name="voltage_2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), - SensorEntityDescription( - key="voltage_3", - name="voltage_3", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - icon="mdi:flash", - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_KEYS): [vol.In(SENSOR_KEYS)], - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USE_LEDS, default=False): cv.boolean, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Sense HAT sensor platform.""" - _LOGGER.warning( - "The Enviro pHAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - try: - envirophat = importlib.import_module("envirophat") - except OSError: - _LOGGER.error("No Enviro pHAT was found") - return - - data = EnvirophatData(envirophat, config.get(CONF_USE_LEDS)) - - display_options = config[CONF_DISPLAY_OPTIONS] - entities = [ - EnvirophatSensor(data, description) - for description in SENSOR_TYPES - if description.key in display_options - ] - add_entities(entities, True) - - -class EnvirophatSensor(SensorEntity): - """Representation of an Enviro pHAT sensor.""" - - def __init__(self, data, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self.data = data - - def update(self): - """Get the latest data and updates the states.""" - self.data.update() - - sensor_type = self.entity_description.key - if sensor_type == "light": - self._attr_native_value = self.data.light - elif sensor_type == "light_red": - self._attr_native_value = self.data.light_red - elif sensor_type == "light_green": - self._attr_native_value = self.data.light_green - elif sensor_type == "light_blue": - self._attr_native_value = self.data.light_blue - elif sensor_type == "accelerometer_x": - self._attr_native_value = self.data.accelerometer_x - elif sensor_type == "accelerometer_y": - self._attr_native_value = self.data.accelerometer_y - elif sensor_type == "accelerometer_z": - self._attr_native_value = self.data.accelerometer_z - elif sensor_type == "magnetometer_x": - self._attr_native_value = self.data.magnetometer_x - elif sensor_type == "magnetometer_y": - self._attr_native_value = self.data.magnetometer_y - elif sensor_type == "magnetometer_z": - self._attr_native_value = self.data.magnetometer_z - elif sensor_type == "temperature": - self._attr_native_value = self.data.temperature - elif sensor_type == "pressure": - self._attr_native_value = self.data.pressure - elif sensor_type == "voltage_0": - self._attr_native_value = self.data.voltage_0 - elif sensor_type == "voltage_1": - self._attr_native_value = self.data.voltage_1 - elif sensor_type == "voltage_2": - self._attr_native_value = self.data.voltage_2 - elif sensor_type == "voltage_3": - self._attr_native_value = self.data.voltage_3 - - -class EnvirophatData: - """Get the latest data and update.""" - - def __init__(self, envirophat, use_leds): - """Initialize the data object.""" - self.envirophat = envirophat - self.use_leds = use_leds - # sensors readings - self.light = None - self.light_red = None - self.light_green = None - self.light_blue = None - self.accelerometer_x = None - self.accelerometer_y = None - self.accelerometer_z = None - self.magnetometer_x = None - self.magnetometer_y = None - self.magnetometer_z = None - self.temperature = None - self.pressure = None - self.voltage_0 = None - self.voltage_1 = None - self.voltage_2 = None - self.voltage_3 = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from Enviro pHAT.""" - # Light sensor reading: 16-bit integer - self.light = self.envirophat.light.light() - if self.use_leds: - self.envirophat.leds.on() - # the three color values scaled against the overall light, 0-255 - self.light_red, self.light_green, self.light_blue = self.envirophat.light.rgb() - if self.use_leds: - self.envirophat.leds.off() - - # accelerometer readings in G - ( - self.accelerometer_x, - self.accelerometer_y, - self.accelerometer_z, - ) = self.envirophat.motion.accelerometer() - - # raw magnetometer reading - ( - self.magnetometer_x, - self.magnetometer_y, - self.magnetometer_z, - ) = self.envirophat.motion.magnetometer() - - # temperature resolution of BMP280 sensor: 0.01°C - self.temperature = round(self.envirophat.weather.temperature(), 2) - - # pressure resolution of BMP280 sensor: 0.16 Pa, rounding to 0.1 Pa - # with conversion to 100 Pa = 1 hPa - self.pressure = round(self.envirophat.weather.pressure() / 100.0, 3) - - # Voltage sensor, reading between 0-3.3V - ( - self.voltage_0, - self.voltage_1, - self.voltage_2, - self.voltage_3, - ) = self.envirophat.analog.read_all() diff --git a/requirements_all.txt b/requirements_all.txt index 3666621e4a3..bd5cfbc2d06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -597,9 +597,6 @@ enturclient==0.2.3 # homeassistant.components.environment_canada env_canada==0.5.20 -# homeassistant.components.envirophat -# envirophat==0.0.6 - # homeassistant.components.enphase_envoy envoy_reader==0.20.1 @@ -2177,7 +2174,6 @@ smart-meter-texas==0.4.7 smarthab==0.21 # homeassistant.components.bme680 -# homeassistant.components.envirophat # homeassistant.components.htu21d # homeassistant.components.raspihats # smbus-cffi==0.5.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f02b3596630..e1cb1ae1fef 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -22,7 +22,6 @@ COMMENT_REQUIREMENTS = ( "bme680", "decora", "decora_wifi", - "envirophat", "evdev", "face_recognition", "homeassistant-pyozw", From 7f4faafe389f96e2b13398cc32034aaeceff69d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 13:56:47 -0800 Subject: [PATCH 0088/1054] Add type code that is being ignored (#67311) --- homeassistant/components/websocket_api/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index abc37dd2a0a..6044e55978d 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -600,7 +600,7 @@ async def handle_validate_config( continue try: - await validator(hass, schema(msg[key])) # type: ignore + await validator(hass, schema(msg[key])) # type: ignore[operator] except vol.Invalid as err: result[key] = {"valid": False, "error": str(err)} else: From d299915c1a7e61c06d9d750400ec5d4cdb9be60e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 26 Feb 2022 23:00:33 +0100 Subject: [PATCH 0089/1054] Fix netgear typing (#67287) --- homeassistant/components/netgear/__init__.py | 6 +++++- .../components/netgear/config_flow.py | 10 +++++++--- .../components/netgear/device_tracker.py | 6 +++--- homeassistant/components/netgear/router.py | 20 ++++++++++--------- mypy.ini | 12 ----------- script/hassfest/mypy_config.py | 4 ---- 6 files changed, 26 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 2842157f578..72a56427e17 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -1,6 +1,9 @@ """Support for Netgear routers.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL @@ -51,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) + assert entry.unique_id device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -67,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from the router.""" return await router.async_update_device_trackers() - async def async_update_traffic_meter() -> dict: + async def async_update_traffic_meter() -> dict[str, Any] | None: """Fetch data from the router.""" return await router.async_get_traffic_meter() diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 791fbd26bd3..85c206ce463 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -1,5 +1,8 @@ """Config flow to configure the Netgear integration.""" +from __future__ import annotations + import logging +from typing import cast from urllib.parse import urlparse from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER @@ -119,11 +122,12 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Initialize flow from ssdp.""" - updated_data = {} + updated_data: dict[str, str | int | bool] = {} device_url = urlparse(discovery_info.ssdp_location) - if device_url.hostname: - updated_data[CONF_HOST] = device_url.hostname + if hostname := device_url.hostname: + hostname = cast(str, hostname) + updated_data[CONF_HOST] = hostname _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 72699768f84..0f7f5cffb10 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -59,7 +59,7 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): self._hostname = self.get_hostname() self._icon = DEVICE_ICONS.get(device["device_type"], "mdi:help-network") - def get_hostname(self): + def get_hostname(self) -> str | None: """Return the hostname of the given device or None if we don't know.""" if (hostname := self._device["name"]) == "--": return None @@ -74,7 +74,7 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): self._icon = DEVICE_ICONS.get(self._device["device_type"], "mdi:help-network") @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the router.""" return self._active @@ -94,7 +94,7 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): return self._mac @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Return the hostname.""" return self._hostname diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 9e44495aa62..f543ba8a5f3 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -5,6 +5,7 @@ from abc import abstractmethod import asyncio from datetime import timedelta import logging +from typing import Any from pynetgear import Netgear @@ -59,13 +60,14 @@ class NetgearRouter: def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize a Netgear router.""" + assert entry.unique_id self.hass = hass self.entry = entry self.entry_id = entry.entry_id self.unique_id = entry.unique_id - self._host = entry.data.get(CONF_HOST) - self._port = entry.data.get(CONF_PORT) - self._ssl = entry.data.get(CONF_SSL) + self._host: str = entry.data[CONF_HOST] + self._port: int = entry.data[CONF_PORT] + self._ssl: bool = entry.data[CONF_SSL] self._username = entry.data.get(CONF_USERNAME) self._password = entry.data[CONF_PASSWORD] @@ -85,9 +87,9 @@ class NetgearRouter: self._api: Netgear = None self._api_lock = asyncio.Lock() - self.devices = {} + self.devices: dict[str, Any] = {} - def _setup(self) -> None: + def _setup(self) -> bool: """Set up a Netgear router sync portion.""" self._api = get_api( self._password, @@ -134,7 +136,7 @@ class NetgearRouter: if device_entry.via_device_id is None: continue # do not add the router itself - device_mac = dict(device_entry.connections).get(dr.CONNECTION_NETWORK_MAC) + device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC] self.devices[device_mac] = { "mac": device_mac, "name": device_entry.name, @@ -166,14 +168,14 @@ class NetgearRouter: self._api.get_attached_devices_2 ) - async def async_update_device_trackers(self, now=None) -> None: + async def async_update_device_trackers(self, now=None) -> bool: """Update Netgear devices.""" new_device = False ntg_devices = await self.async_get_attached_devices() now = dt_util.utcnow() if ntg_devices is None: - return + return new_device if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) @@ -197,7 +199,7 @@ class NetgearRouter: return new_device - async def async_get_traffic_meter(self) -> None: + async def async_get_traffic_meter(self) -> dict[str, Any] | None: """Get the traffic meter data of the router.""" async with self._api_lock: return await self.hass.async_add_executor_job(self._api.get_traffic_meter) diff --git a/mypy.ini b/mypy.ini index 781bc5c199e..f817a5c58a6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2480,18 +2480,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.netgear] -ignore_errors = true - -[mypy-homeassistant.components.netgear.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.netgear.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.netgear.router] -ignore_errors = true - [mypy-homeassistant.components.nilu.air_quality] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index ef29562578e..dc8dc2188d3 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -94,10 +94,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.netgear", - "homeassistant.components.netgear.config_flow", - "homeassistant.components.netgear.device_tracker", - "homeassistant.components.netgear.router", "homeassistant.components.nilu.air_quality", "homeassistant.components.nzbget", "homeassistant.components.nzbget.config_flow", From 02d4739b28f576e93877ea455fd49cea335fdadc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Feb 2022 14:12:33 -0800 Subject: [PATCH 0090/1054] Bump frontend to 20220226.0 (#67313) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f9ad2bd428d..4b28744d0e3 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220224.0" + "home-assistant-frontend==20220226.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fd8fe9c0681..a36f21efd6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220224.0 +home-assistant-frontend==20220226.0 httpx==0.21.3 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index bd5cfbc2d06..390aab14f6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -814,7 +814,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220224.0 +home-assistant-frontend==20220226.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd08cbc1273..86482356bfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220224.0 +home-assistant-frontend==20220226.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 01fa6e7513360fbf291081298bea76940e29a713 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Feb 2022 00:13:48 +0100 Subject: [PATCH 0091/1054] Eliminate extra include (#67316) --- homeassistant/components/rfxtrx/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index ad3e2503527..3048b9b430b 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,7 +12,6 @@ import RFXtrx as rfxtrxmod import async_timeout import voluptuous as vol -from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -87,9 +86,7 @@ PLATFORMS = [ ] -async def async_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the RFXtrx component.""" hass.data.setdefault(DOMAIN, {}) @@ -163,7 +160,7 @@ def _get_device_lookup(devices): return lookup -async def async_setup_internal(hass, entry: config_entries.ConfigEntry): +async def async_setup_internal(hass, entry: ConfigEntry): """Set up the RFXtrx component.""" config = entry.data @@ -285,7 +282,7 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): async def async_setup_platform_entry( hass: HomeAssistant, - config_entry: config_entries.ConfigEntry, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, supported: Callable[[rfxtrxmod.RFXtrxEvent], bool], constructor: Callable[ From a151d3f9a0743416e495d8680b76d6d0cb0da9e6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 27 Feb 2022 00:14:12 +0100 Subject: [PATCH 0092/1054] Don't trigger device removal for non rfxtrx devices (#67315) --- homeassistant/components/rfxtrx/__init__.py | 33 ++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 3048b9b430b..3587f673fd9 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -6,7 +6,7 @@ import binascii from collections.abc import Callable import copy import logging -from typing import NamedTuple +from typing import NamedTuple, cast import RFXtrx as rfxtrxmod import async_timeout @@ -242,11 +242,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): devices[device_id] = config @callback - def _remove_device(event: Event): - if event.data["action"] != "remove": - return - device_entry = device_registry.deleted_devices[event.data["device_id"]] - device_id = next(iter(device_entry.identifiers))[1:] + def _remove_device(device_id: DeviceTuple): data = { **entry.data, CONF_DEVICES: { @@ -258,8 +254,19 @@ async def async_setup_internal(hass, entry: ConfigEntry): hass.config_entries.async_update_entry(entry=entry, data=data) devices.pop(device_id) + @callback + def _updated_device(event: Event): + if event.data["action"] != "remove": + return + device_entry = device_registry.deleted_devices[event.data["device_id"]] + if entry.entry_id not in device_entry.config_entries: + return + device_id = get_device_tuple_from_identifiers(device_entry.identifiers) + if device_id: + _remove_device(device_id) + entry.async_on_unload( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, _remove_device) + hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) ) def _shutdown_rfxtrx(event): @@ -426,6 +433,18 @@ def get_device_id( return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string) +def get_device_tuple_from_identifiers( + identifiers: set[tuple[str, str]] +) -> DeviceTuple | None: + """Calculate the device tuple from a device entry.""" + identifier = next((x for x in identifiers if x[0] == DOMAIN), None) + if not identifier: + return None + # work around legacy identifier, being a multi tuple value + identifier2 = cast(tuple[str, str, str, str], identifier) + return DeviceTuple(identifier2[1], identifier2[2], identifier2[3]) + + async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry ) -> bool: From 0e74184e4f142cb49639e9a384099ea309e595d7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 15:17:02 -0800 Subject: [PATCH 0093/1054] Simplify google calendar authentication setup (#67314) Simplify google calendar authentication to combine some of the cases together, and reduce unecessarily checks. Make the tests share common authentication setup and reduce use of mocks by introducing a fake for holding on to credentials. This makes future refactoring simpler, so we don't have to care as much about the interactions with the credentials storage. --- homeassistant/components/google/__init__.py | 25 +++++---- tests/components/google/conftest.py | 62 +++++++++++++++++++++ tests/components/google/test_calendar.py | 8 +-- tests/components/google/test_init.py | 41 -------------- 4 files changed, 77 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index f35f79a5f92..0769422366e 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -5,7 +5,6 @@ from collections.abc import Mapping from datetime import datetime, timedelta, timezone from enum import Enum import logging -import os from typing import Any from googleapiclient import discovery as google_discovery @@ -163,7 +162,10 @@ ADD_EVENT_SERVICE_SCHEMA = vol.Schema( def do_authentication( - hass: HomeAssistant, hass_config: ConfigType, config: ConfigType + hass: HomeAssistant, + hass_config: ConfigType, + config: ConfigType, + storage: Storage, ) -> bool: """Notify user of actions and authenticate. @@ -226,7 +228,6 @@ def do_authentication( # not ready yet, call again return - storage = Storage(hass.config.path(TOKEN_FILE)) storage.put(credentials) do_setup(hass, hass_config, config) assert listener @@ -256,16 +257,16 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: # component is set up by tts platform return True - token_file = hass.config.path(TOKEN_FILE) - if not os.path.isfile(token_file): - _LOGGER.debug("Token file does not exist, authenticating for first time") - do_authentication(hass, config, conf) + storage = Storage(hass.config.path(TOKEN_FILE)) + creds = storage.get() + if ( + not creds + or not creds.scopes + or conf[CONF_CALENDAR_ACCESS].scope not in creds.scopes + ): + do_authentication(hass, config, conf, storage) else: - if not check_correct_scopes(hass, token_file, conf): - _LOGGER.debug("Existing scopes are not sufficient, re-authenticating") - do_authentication(hass, config, conf) - else: - do_setup(hass, config, conf) + do_setup(hass, config, conf) return True diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 59de9b2cddf..3c354b36226 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,11 +1,17 @@ """Test configuration and mocks for the google integration.""" +from __future__ import annotations + from collections.abc import Callable +import datetime from typing import Any, Generator, TypeVar from unittest.mock import Mock, patch +from oauth2client.client import Credentials, OAuth2Credentials import pytest from homeassistant.components.google import GoogleCalendarService +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow ApiResult = Callable[[dict[str, Any]], None] T = TypeVar("T") @@ -36,6 +42,62 @@ def test_calendar(): return TEST_CALENDAR +class FakeStorage: + """A fake storage object for persiting creds.""" + + def __init__(self) -> None: + """Initialize FakeStorage.""" + self._creds: Credentials | None = None + + def get(self) -> Credentials | None: + """Get credentials from storage.""" + return self._creds + + def put(self, creds: Credentials) -> None: + """Put credentials in storage.""" + self._creds = creds + + +@pytest.fixture +async def token_scopes() -> list[str]: + """Fixture for scopes used during test.""" + return ["https://www.googleapis.com/auth/calendar"] + + +@pytest.fixture +async def creds(token_scopes: list[str]) -> OAuth2Credentials: + """Fixture that defines creds used in the test.""" + token_expiry = utcnow() + datetime.timedelta(days=7) + return OAuth2Credentials( + access_token="ACCESS_TOKEN", + client_id="client-id", + client_secret="client-secret", + refresh_token="REFRESH_TOKEN", + token_expiry=token_expiry, + token_uri="http://example.com", + user_agent="n/a", + scopes=token_scopes, + ) + + +@pytest.fixture(autouse=True) +async def storage() -> YieldFixture[FakeStorage]: + """Fixture to populate an existing token file for read on startup.""" + storage = FakeStorage() + with patch("homeassistant.components.google.Storage", return_value=storage): + yield storage + + +@pytest.fixture +async def mock_token_read( + hass: HomeAssistant, + creds: OAuth2Credentials, + storage: FakeStorage, +) -> None: + """Fixture to populate an existing token file for read on startup.""" + storage.put(creds) + + @pytest.fixture def mock_next_event(): """Mock the google calendar data.""" diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 0ee257788dd..6f91fed7b24 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -21,7 +21,6 @@ from homeassistant.components.google import ( CONF_TRACK, DEVICE_SCHEMA, SERVICE_SCAN_CALENDARS, - do_setup, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT @@ -86,21 +85,18 @@ def get_calendar_info(calendar): @pytest.fixture(autouse=True) -def mock_google_setup(hass, test_calendar): +def mock_google_setup(hass, test_calendar, mock_token_read): """Mock the google set up functions.""" hass.loop.run_until_complete(async_setup_component(hass, "group", {"group": {}})) calendar = get_calendar_info(test_calendar) calendars = {calendar[CONF_CAL_ID]: calendar} - patch_google_auth = patch( - "homeassistant.components.google.do_authentication", side_effect=do_setup - ) patch_google_load = patch( "homeassistant.components.google.load_config", return_value=calendars ) patch_google_services = patch("homeassistant.components.google.setup_services") async_mock_service(hass, "google", SERVICE_SCAN_CALENDARS) - with patch_google_auth, patch_google_load, patch_google_services: + with patch_google_load, patch_google_services: yield diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index c3754511b04..8fbcb97bfe2 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -53,28 +53,6 @@ async def mock_code_flow( yield mock_flow -@pytest.fixture -async def token_scopes() -> list[str]: - """Fixture for scopes used during test.""" - return ["https://www.googleapis.com/auth/calendar"] - - -@pytest.fixture -async def creds(token_scopes: list[str]) -> OAuth2Credentials: - """Fixture that defines creds used in the test.""" - token_expiry = utcnow() + datetime.timedelta(days=7) - return OAuth2Credentials( - access_token="ACCESS_TOKEN", - client_id="client-id", - client_secret="client-secret", - refresh_token="REFRESH_TOKEN", - token_expiry=token_expiry, - token_uri="http://example.com", - user_agent="n/a", - scopes=token_scopes, - ) - - @pytest.fixture async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: """Fixture for mocking out the exchange for credentials.""" @@ -84,25 +62,6 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: yield mock -@pytest.fixture(autouse=True) -async def mock_token_write(hass: HomeAssistant) -> None: - """Fixture to avoid writing token files to disk.""" - with patch( - "homeassistant.components.google.os.path.isfile", return_value=True - ), patch("homeassistant.components.google.Storage.put"): - yield - - -@pytest.fixture -async def mock_token_read( - hass: HomeAssistant, - creds: OAuth2Credentials, -) -> None: - """Fixture to populate an existing token file.""" - with patch("homeassistant.components.google.Storage.get", return_value=creds): - yield - - @pytest.fixture async def calendars_config() -> list[dict[str, Any]]: """Fixture for tests to override default calendar configuration.""" From f6be0c2b88951d9b5793b1b60fbf45e898475810 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 27 Feb 2022 00:23:56 +0100 Subject: [PATCH 0094/1054] Remove redundant type cast (#67317) --- homeassistant/components/frontend/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c6812f4d9de..803b093fd40 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -7,7 +7,7 @@ import json import logging import os import pathlib -from typing import Any, TypedDict, cast +from typing import Any, TypedDict from aiohttp import hdrs, web, web_urldispatcher import jinja2 @@ -313,7 +313,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: # pylint: disable=import-outside-toplevel import hass_frontend - return cast(pathlib.Path, hass_frontend.where()) + return hass_frontend.where() async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: From 1383034df2ae5aa44de15e150567978b4834953e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 15:27:08 -0800 Subject: [PATCH 0095/1054] Simplify google calendar event filters (#67312) Simplify google calendar event filtering logic currently copied between two methods. --- homeassistant/components/google/calendar.py | 34 ++++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 90b5bbb3d89..d9fcbbd593d 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -41,6 +41,12 @@ DEFAULT_GOOGLE_SEARCH_PARAMS = { MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) +# Events have a transparency that determine whether or not they block time on calendar. +# When an event is opaque, it means "Show me as busy" which is the default. Events that +# are not opaque are ignored by default. +TRANSPARENCY = "transparency" +OPAQUE = "opaque" + def setup_platform( hass: HomeAssistant, @@ -161,6 +167,12 @@ class GoogleCalendarData: return service, params + def _event_filter(self, event: dict[str, Any]) -> bool: + """Return True if the event is visible.""" + if self.ignore_availability: + return True + return event.get(TRANSPARENCY, OPAQUE) == OPAQUE + async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[dict[str, Any]]: @@ -195,12 +207,8 @@ class GoogleCalendarData: result = await hass.async_add_executor_job(events.list(**params).execute) items = result.get("items", []) - for item in items: - if not self.ignore_availability and "transparency" in item: - if item["transparency"] == "opaque": - event_list.append(item) - else: - event_list.append(item) + visible_items = filter(self._event_filter, items) + event_list.extend(visible_items) return result.get("nextPageToken") @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -215,15 +223,5 @@ class GoogleCalendarData: result = events.list(**params).execute() items = result.get("items", []) - - new_event = None - for item in items: - if not self.ignore_availability and "transparency" in item: - if item["transparency"] == "opaque": - new_event = item - break - else: - new_event = item - break - - self.event = new_event + valid_events = filter(self._event_filter, items) + self.event = next(valid_events, None) From 479aa132114048005f8114d7be22c10b50f81ff5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 27 Feb 2022 00:19:29 +0000 Subject: [PATCH 0096/1054] [ci skip] Translation update --- .../azure_event_hub/translations/nl.json | 4 ++-- .../components/dlna_dms/translations/bg.json | 6 +++++ .../components/dlna_dms/translations/nl.json | 24 +++++++++++++++++++ .../components/fritz/translations/nl.json | 3 ++- .../components/homekit/translations/nl.json | 2 +- .../components/iss/translations/nl.json | 2 +- .../components/nanoleaf/translations/nl.json | 8 +++++++ .../radio_browser/translations/bg.json | 7 ++++++ .../components/rfxtrx/translations/bg.json | 7 ++++++ .../components/rfxtrx/translations/et.json | 1 + .../components/rfxtrx/translations/it.json | 1 + .../components/rfxtrx/translations/ja.json | 1 + .../components/rfxtrx/translations/nl.json | 1 + .../components/rfxtrx/translations/pl.json | 1 + .../components/rfxtrx/translations/pt-BR.json | 1 + .../components/rfxtrx/translations/ru.json | 1 + .../rfxtrx/translations/zh-Hant.json | 1 + .../components/sense/translations/bg.json | 20 ++++++++++++++++ .../components/sense/translations/nl.json | 16 ++++++++++++- .../components/sonarr/translations/nl.json | 3 ++- .../components/wiz/translations/nl.json | 2 +- .../xiaomi_miio/translations/zh-Hans.json | 3 ++- 22 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/dlna_dms/translations/nl.json create mode 100644 homeassistant/components/radio_browser/translations/bg.json create mode 100644 homeassistant/components/sense/translations/bg.json diff --git a/homeassistant/components/azure_event_hub/translations/nl.json b/homeassistant/components/azure_event_hub/translations/nl.json index 92b3eee0a8d..646d820c2ad 100644 --- a/homeassistant/components/azure_event_hub/translations/nl.json +++ b/homeassistant/components/azure_event_hub/translations/nl.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "cannot_connect": "Verbinding maken met de credentails uit de configuration.yaml is mislukt, verwijder deze uit de yaml en gebruik de config flow.", + "cannot_connect": "Verbinding maken met de credentials uit de configuration.yaml is mislukt, verwijder deze uit yaml en gebruik de config flow.", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", - "unknown": "Verbinding maken met de credentails uit de configuration.yaml is mislukt met een onbekende fout, verwijder a.u.b. de yaml en gebruik de config flow." + "unknown": "Verbinding maken met de credentials uit de configuration.yaml is mislukt met een onbekende fout, verwijder deze uit yaml en gebruik de config flow." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/dlna_dms/translations/bg.json b/homeassistant/components/dlna_dms/translations/bg.json index b43da9ecb18..4356e0973c1 100644 --- a/homeassistant/components/dlna_dms/translations/bg.json +++ b/homeassistant/components/dlna_dms/translations/bg.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/dlna_dms/translations/nl.json b/homeassistant/components/dlna_dms/translations/nl.json new file mode 100644 index 00000000000..d480118b76a --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "bad_ssdp": "SSDP-gegevens missen een vereiste waarde", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_dms": "Apparaat is geen ondersteunde mediaserver" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Wilt u beginnen met instellen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Kies een apparaat om te configureren", + "title": "Ontdekte DLNA DMA-apparaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 06c29cd9d38..c62a3b7044e 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Seconden om een apparaat als \"thuis\" te beschouwen" + "consider_home": "Seconden om een apparaat als \"thuis\" te beschouwen", + "old_discovery": "Oude detectiemethode inschakelen" } } } diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 9788f4904b6..2e16ffde8ef 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domeinen om op te nemen" }, - "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen. Voor elke tv-mediaspeler, camera, activiteiten gebaseerde afstandsbediening en slot wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", + "description": "Kies de domeinen die moeten worden opgenomen. Alle ondersteunde entiteiten in het domein zullen worden opgenomen, behalve de gecategoriseerde entiteiten. Voor elke tv-mediaspeler, camera, activiteiten gebaseerde afstandsbediening en slot wordt een afzonderlijke HomeKit-instantie in accessoiremodus aangemaakt.", "title": "Selecteer domeinen die u wilt opnemen" } } diff --git a/homeassistant/components/iss/translations/nl.json b/homeassistant/components/iss/translations/nl.json index c3e7ade05d9..08a170b1ccc 100644 --- a/homeassistant/components/iss/translations/nl.json +++ b/homeassistant/components/iss/translations/nl.json @@ -9,7 +9,7 @@ "data": { "show_on_map": "Op kaart tonen?" }, - "description": "Wilt u het International Space Station configureren?" + "description": "Wilt u International Space Station configureren?" } } }, diff --git a/homeassistant/components/nanoleaf/translations/nl.json b/homeassistant/components/nanoleaf/translations/nl.json index 29a9f7ac58b..76c471769e3 100644 --- a/homeassistant/components/nanoleaf/translations/nl.json +++ b/homeassistant/components/nanoleaf/translations/nl.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Veeg omlaag", + "swipe_left": "Veeg naar links", + "swipe_right": "Veeg naar rechts", + "swipe_up": "Veeg omhoog" + } } } \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/bg.json b/homeassistant/components/radio_browser/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index 725534d7ac5..9a7983491d6 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -26,6 +26,13 @@ "error": { "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "prompt_options": { + "data": { + "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0438" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json index 1b414db656c..2c4a36b1664 100644 --- a/homeassistant/components/rfxtrx/translations/et.json +++ b/homeassistant/components/rfxtrx/translations/et.json @@ -61,6 +61,7 @@ "debug": "Luba silumine", "device": "Vali seadistatav seade", "event_code": "Sisesta lisatava s\u00fcndmuse kood", + "protocols": "Protokollid", "remove_device": "Vali eemaldatav seade" }, "title": "Rfxtrx valikud" diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index b811985349a..5e5f07a013f 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -66,6 +66,7 @@ "debug": "Attiva il debug", "device": "Seleziona il dispositivo da configurare", "event_code": "Inserire il codice dell'evento da aggiungere", + "protocols": "Protocolli", "remove_device": "Seleziona il dispositivo da eliminare" }, "title": "Opzioni Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index bfbf14b31fb..cb79cc60a8e 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -61,6 +61,7 @@ "debug": "\u30c7\u30d0\u30c3\u30b0\u306e\u6709\u52b9\u5316", "device": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", "event_code": "\u30a4\u30d9\u30f3\u30c8\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u8ffd\u52a0", + "protocols": "\u30d7\u30ed\u30c8\u30b3\u30eb", "remove_device": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" }, "title": "Rfxtrx\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index 92154861f15..e9b4c1a6c04 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -61,6 +61,7 @@ "debug": "Foutopsporing inschakelen", "device": "Selecteer het apparaat om te configureren", "event_code": "Voer de gebeurteniscode in om toe te voegen", + "protocols": "Protocollen", "remove_device": "Apparaat selecteren dat u wilt verwijderen" }, "title": "Rfxtrx-opties" diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index be3e39fca70..b9f83ded473 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -72,6 +72,7 @@ "debug": "W\u0142\u0105cz debugowanie", "device": "Wybierz urz\u0105dzenie do skonfigurowania", "event_code": "Podaj kod zdarzenia do dodania", + "protocols": "Protoko\u0142y", "remove_device": "Wybierz urz\u0105dzenie do usuni\u0119cia" }, "title": "Opcje Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/pt-BR.json b/homeassistant/components/rfxtrx/translations/pt-BR.json index 6f867a22a55..83252586d6a 100644 --- a/homeassistant/components/rfxtrx/translations/pt-BR.json +++ b/homeassistant/components/rfxtrx/translations/pt-BR.json @@ -61,6 +61,7 @@ "debug": "Habilitar a depura\u00e7\u00e3o", "device": "Selecione o dispositivo para configurar", "event_code": "Insira o c\u00f3digo do evento para adicionar", + "protocols": "Protocolos", "remove_device": "Selecione o dispositivo para excluir" }, "title": "Op\u00e7\u00f5es de Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json index 4a56f37687a..cb0fa979197 100644 --- a/homeassistant/components/rfxtrx/translations/ru.json +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -61,6 +61,7 @@ "debug": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043b\u0430\u0434\u043a\u0438", "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", "event_code": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u0431\u044b\u0442\u0438\u044f", + "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b", "remove_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index d66b0b1cf7c..8f4c9f2aa1a 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -61,6 +61,7 @@ "debug": "\u958b\u555f\u9664\u932f", "device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e", + "protocols": "\u901a\u8a0a\u5354\u5b9a", "remove_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u522a\u9664" }, "title": "Rfxtrx \u9078\u9805" diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json new file mode 100644 index 00000000000..d42d6dba5c1 --- /dev/null +++ b/homeassistant/components/sense/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u0437\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json index 59e0e3ade8a..11b1c07350c 100644 --- a/homeassistant/components/sense/translations/nl.json +++ b/homeassistant/components/sense/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_validate": { + "data": { + "password": "Wachtwoord" + }, + "description": "De Sense-integratie moet uw account {email} opnieuw verifi\u00ebren.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "email": "E-mail", @@ -16,6 +24,12 @@ "timeout": "Timeout" }, "title": "Maak verbinding met uw Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Verificatiecode" + }, + "title": "Sense Multi-factor authenticatie" } } } diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index d2ded412f1b..58bf04e283d 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {host}", + "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {url}", "title": "Verifieer de integratie opnieuw" }, "user": { @@ -22,6 +22,7 @@ "host": "Host", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", + "url": "URL", "verify_ssl": "SSL-certificaat verifi\u00ebren" } } diff --git a/homeassistant/components/wiz/translations/nl.json b/homeassistant/components/wiz/translations/nl.json index 79fc5c3db1f..3b59dd6a672 100644 --- a/homeassistant/components/wiz/translations/nl.json +++ b/homeassistant/components/wiz/translations/nl.json @@ -6,7 +6,7 @@ "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { - "bulb_time_out": "Kan geen verbinding maken met de lamp. Misschien is de lamp offline of is er een verkeerde IP/host ingevoerd. Doe het licht aan en probeer het opnieuw!", + "bulb_time_out": "Kan geen verbinding maken met de lamp. Misschien is de lamp offline of is er een verkeerde IP-adres ingevoerd. Doe het licht aan en probeer het opnieuw!", "cannot_connect": "Kan geen verbinding maken", "no_ip": "Geen geldig IP-adres.", "no_wiz_light": "De lamp kan niet worden aangesloten via WiZ Platform integratie.", diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json index 02519414492..f488618b945 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -13,7 +13,8 @@ "cloud_login_error": "\u65e0\u6cd5\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\uff0c\u8bf7\u68c0\u67e5\u51ed\u636e\u3002", "cloud_no_devices": "\u672a\u5728\u5c0f\u7c73\u5e10\u6237\u4e2d\u53d1\u73b0\u8bbe\u5907\u3002", "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002", - "unknown_device": "\u8be5\u8bbe\u5907\u578b\u53f7\u6682\u672a\u9002\u914d\uff0c\u56e0\u6b64\u65e0\u6cd5\u901a\u8fc7\u914d\u7f6e\u5411\u5bfc\u6dfb\u52a0\u8bbe\u5907\u3002" + "unknown_device": "\u8be5\u8bbe\u5907\u578b\u53f7\u6682\u672a\u9002\u914d\uff0c\u56e0\u6b64\u65e0\u6cd5\u901a\u8fc7\u914d\u7f6e\u5411\u5bfc\u6dfb\u52a0\u8bbe\u5907\u3002", + "wrong_token": "\u6821\u9a8c\u548c\u9519\u8bef\uff0ctoken \u9519\u8bef" }, "flow_title": "Xiaomi Miio: {name}", "step": { From deda9e38e49dc006fa5728a9b2104fd17e6e004e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 26 Feb 2022 16:19:45 -0800 Subject: [PATCH 0097/1054] Simplify google calendar API interactions (#67319) * Simplify google calendar APIs and tests * Simplify authentication logic at startup * Improve readability of diffs * Reduce diffs * Simplify api datetime logic * Remove duplicate test fixtures added in prior commit * Remove duplicate event filter calls * Fix event list argument names * More improvements found from additional testing * Remove unnecessary variables in create event call --- homeassistant/components/google/__init__.py | 63 +++++---------- homeassistant/components/google/api.py | 86 +++++++++++++++++++++ homeassistant/components/google/calendar.py | 80 ++++++------------- tests/components/google/conftest.py | 23 +++--- tests/components/google/test_calendar.py | 30 +++---- tests/components/google/test_init.py | 23 +----- 6 files changed, 152 insertions(+), 153 deletions(-) create mode 100644 homeassistant/components/google/api.py diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 0769422366e..08f6bf247f4 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -7,8 +7,6 @@ from enum import Enum import logging from typing import Any -from googleapiclient import discovery as google_discovery -import httplib2 from oauth2client.client import ( FlowExchangeError, OAuth2DeviceCodeError, @@ -37,6 +35,8 @@ from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.typing import ConfigType from homeassistant.util import convert +from .api import GoogleCalendarService + _LOGGER = logging.getLogger(__name__) DOMAIN = "google" @@ -77,6 +77,7 @@ SERVICE_FOUND_CALENDARS = "found_calendar" SERVICE_ADD_EVENT = "add_event" DATA_CALENDARS = "calendars" +DATA_SERVICE = "service" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" @@ -251,13 +252,16 @@ def do_authentication( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Google platform.""" - hass.data[DOMAIN] = {DATA_CALENDARS: {}} if not (conf := config.get(DOMAIN, {})): # component is set up by tts platform return True storage = Storage(hass.config.path(TOKEN_FILE)) + hass.data[DOMAIN] = { + DATA_CALENDARS: {}, + DATA_SERVICE: GoogleCalendarService(hass, storage), + } creds = storage.get() if ( not creds @@ -271,34 +275,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def check_correct_scopes( - hass: HomeAssistant, token_file: str, config: ConfigType -) -> bool: - """Check for the correct scopes in file.""" - creds = Storage(token_file).get() - if not creds or not creds.scopes: - return False - target_scope = config[CONF_CALENDAR_ACCESS].scope - return target_scope in creds.scopes - - -class GoogleCalendarService: - """Calendar service interface to Google.""" - - def __init__(self, token_file: str) -> None: - """Init the Google Calendar service.""" - self.token_file = token_file - - def get(self) -> google_discovery.Resource: - """Get the calendar service from the storage file token.""" - credentials = Storage(self.token_file).get() - http = credentials.authorize(httplib2.Http()) - service = google_discovery.build( - "calendar", "v3", http=http, cache_discovery=False - ) - return service - - def setup_services( hass: HomeAssistant, hass_config: ConfigType, @@ -328,9 +304,7 @@ def setup_services( def _scan_for_calendars(call: ServiceCall) -> None: """Scan for new calendars.""" - service = calendar_service.get() - cal_list = service.calendarList() - calendars = cal_list.list().execute()["items"] + calendars = calendar_service.list_calendars() for calendar in calendars: calendar["track"] = track_new_found_calendars hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) @@ -339,7 +313,6 @@ def setup_services( def _add_event(call: ServiceCall) -> None: """Add a new event to calendar.""" - service = calendar_service.get() start = {} end = {} @@ -374,14 +347,15 @@ def setup_services( start = {"dateTime": start_dt, "timeZone": str(hass.config.time_zone)} end = {"dateTime": end_dt, "timeZone": str(hass.config.time_zone)} - event = { - "summary": call.data[EVENT_SUMMARY], - "description": call.data[EVENT_DESCRIPTION], - "start": start, - "end": end, - } - service_data = {"calendarId": call.data[EVENT_CALENDAR_ID], "body": event} - event = service.events().insert(**service_data).execute() + calendar_service.create_event( + call.data[EVENT_CALENDAR_ID], + { + "summary": call.data[EVENT_SUMMARY], + "description": call.data[EVENT_DESCRIPTION], + "start": start, + "end": end, + }, + ) # Only expose the add event service if we have the correct permissions if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write: @@ -392,12 +366,11 @@ def setup_services( def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) -> None: """Run the setup after we have everything configured.""" - _LOGGER.debug("Setting up integration") # Load calendars the user has configured calendars = load_config(hass.config.path(YAML_DEVICES)) hass.data[DOMAIN][DATA_CALENDARS] = calendars - calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) + calendar_service = hass.data[DOMAIN][DATA_SERVICE] track_new_found_calendars = convert( config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW ) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py new file mode 100644 index 00000000000..8652f8b15ed --- /dev/null +++ b/homeassistant/components/google/api.py @@ -0,0 +1,86 @@ +"""Client library for talking to Google APIs.""" + +from __future__ import annotations + +import datetime +import logging +from typing import Any + +from googleapiclient import discovery as google_discovery +from oauth2client.file import Storage + +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +_LOGGER = logging.getLogger(__name__) + + +EVENT_PAGE_SIZE = 100 + + +def _api_time_format(time: datetime.datetime | None) -> str | None: + """Convert a datetime to the api string format.""" + return time.isoformat("T") if time else None + + +class GoogleCalendarService: + """Calendar service interface to Google.""" + + def __init__(self, hass: HomeAssistant, storage: Storage) -> None: + """Init the Google Calendar service.""" + self._hass = hass + self._storage = storage + + def _get_service(self) -> google_discovery.Resource: + """Get the calendar service from the storage file token.""" + return google_discovery.build( + "calendar", "v3", credentials=self._storage.get(), cache_discovery=False + ) + + def list_calendars(self) -> list[dict[str, Any]]: + """Return the list of calendars the user has added to their list.""" + cal_list = self._get_service().calendarList() # pylint: disable=no-member + return cal_list.list().execute()["items"] + + def create_event(self, calendar_id: str, event: dict[str, Any]) -> dict[str, Any]: + """Create an event.""" + events = self._get_service().events() # pylint: disable=no-member + return events.insert(calendarId=calendar_id, body=event).execute() + + async def async_list_events( + self, + calendar_id: str, + start_time: datetime.datetime | None = None, + end_time: datetime.datetime | None = None, + search: str | None = None, + page_token: str | None = None, + ) -> tuple[list[dict[str, Any]], str | None]: + """Return the list of events.""" + return await self._hass.async_add_executor_job( + self.list_events, + calendar_id, + start_time, + end_time, + search, + page_token, + ) + + def list_events( + self, + calendar_id: str, + start_time: datetime.datetime | None = None, + end_time: datetime.datetime | None = None, + search: str | None = None, + page_token: str | None = None, + ) -> tuple[list[dict[str, Any]], str | None]: + """Return the list of events.""" + events = self._get_service().events() # pylint: disable=no-member + result = events.list( + calendarId=calendar_id, + timeMin=_api_time_format(start_time if start_time else dt.now()), + timeMax=_api_time_format(end_time), + q=search, + maxResults=EVENT_PAGE_SIZE, + pageToken=page_token, + ).execute() + return (result["items"], result.get("nextPageToken")) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index d9fcbbd593d..e068d817429 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta import logging from typing import Any -from googleapiclient import discovery as google_discovery from httplib2 import ServerNotFoundError from homeassistant.components.calendar import ( @@ -20,17 +19,18 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle, dt +from homeassistant.util import Throttle from . import ( CONF_CAL_ID, CONF_IGNORE_AVAILABILITY, CONF_SEARCH, CONF_TRACK, + DATA_SERVICE, DEFAULT_CONF_OFFSET, - TOKEN_FILE, - GoogleCalendarService, + DOMAIN, ) +from .api import GoogleCalendarService _LOGGER = logging.getLogger(__name__) @@ -61,7 +61,7 @@ def setup_platform( if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]): return - calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) + calendar_service = hass.data[DOMAIN][DATA_SERVICE] entities = [] for data in disc_info[CONF_ENTITIES]: if not data[CONF_TRACK]: @@ -150,23 +150,6 @@ class GoogleCalendarData: self.ignore_availability = ignore_availability self.event: dict[str, Any] | None = None - def _prepare_query( - self, - ) -> tuple[google_discovery.Resource | None, dict[str, Any] | None]: - try: - service = self.calendar_service.get() - except ServerNotFoundError as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return None, None - params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) - params["calendarId"] = self.calendar_id - params["maxResults"] = 100 # Page size - - if self.search: - params["q"] = self.search - - return service, params - def _event_filter(self, event: dict[str, Any]) -> bool: """Return True if the event is visible.""" if self.ignore_availability: @@ -177,51 +160,36 @@ class GoogleCalendarData: self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[dict[str, Any]]: """Get all events in a specific time frame.""" - service, params = await hass.async_add_executor_job(self._prepare_query) - if service is None or params is None: - return [] - params["timeMin"] = start_date.isoformat("T") - params["timeMax"] = end_date.isoformat("T") - event_list: list[dict[str, Any]] = [] - events = await hass.async_add_executor_job(service.events) page_token: str | None = None while True: - page_token = await self.async_get_events_page( - hass, events, params, page_token, event_list - ) + try: + items, page_token = await self.calendar_service.async_list_events( + self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self.search, + page_token=page_token, + ) + except ServerNotFoundError as err: + _LOGGER.error("Unable to connect to Google: %s", err) + return [] + + event_list.extend(filter(self._event_filter, items)) if not page_token: break return event_list - async def async_get_events_page( - self, - hass: HomeAssistant, - events: google_discovery.Resource, - params: dict[str, Any], - page_token: str | None, - event_list: list[dict[str, Any]], - ) -> str | None: - """Get a page of events in a specific time frame.""" - params["pageToken"] = page_token - result = await hass.async_add_executor_job(events.list(**params).execute) - - items = result.get("items", []) - visible_items = filter(self._event_filter, items) - event_list.extend(visible_items) - return result.get("nextPageToken") - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self) -> None: """Get the latest data.""" - service, params = self._prepare_query() - if service is None or params is None: + try: + items, _ = self.calendar_service.list_events( + self.calendar_id, search=self.search + ) + except ServerNotFoundError as err: + _LOGGER.error("Unable to connect to Google: %s", err) return - params["timeMin"] = dt.now().isoformat("T") - events = service.events() - result = events.list(**params).execute() - - items = result.get("items", []) valid_events = filter(self._event_filter, items) self.event = next(valid_events, None) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 3c354b36226..b78f97e8209 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -6,10 +6,10 @@ import datetime from typing import Any, Generator, TypeVar from unittest.mock import Mock, patch +from googleapiclient import discovery as google_discovery from oauth2client.client import Credentials, OAuth2Credentials import pytest -from homeassistant.components.google import GoogleCalendarService from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -108,14 +108,21 @@ def mock_next_event(): yield google_cal_data +@pytest.fixture(autouse=True) +def calendar_resource() -> YieldFixture[google_discovery.Resource]: + """Fixture to mock out the Google discovery API.""" + with patch("homeassistant.components.google.api.google_discovery.build") as mock: + yield mock + + @pytest.fixture def mock_events_list( - google_service: GoogleCalendarService, + calendar_resource: google_discovery.Resource, ) -> Callable[[dict[str, Any]], None]: """Fixture to construct a fake event list API response.""" def _put_result(response: dict[str, Any]) -> None: - google_service.return_value.get.return_value.events.return_value.list.return_value.execute.return_value = ( + calendar_resource.return_value.events.return_value.list.return_value.execute.return_value = ( response ) return @@ -125,12 +132,12 @@ def mock_events_list( @pytest.fixture def mock_calendars_list( - google_service: GoogleCalendarService, + calendar_resource: google_discovery.Resource, ) -> ApiResult: """Fixture to construct a fake calendar list API response.""" def _put_result(response: dict[str, Any]) -> None: - google_service.return_value.get.return_value.calendarList.return_value.list.return_value.execute.return_value = ( + calendar_resource.return_value.calendarList.return_value.list.return_value.execute.return_value = ( response ) return @@ -140,11 +147,9 @@ def mock_calendars_list( @pytest.fixture def mock_insert_event( - google_service: GoogleCalendarService, + calendar_resource: google_discovery.Resource, ) -> Mock: """Fixture to create a mock to capture new events added to the API.""" insert_mock = Mock() - google_service.return_value.get.return_value.events.return_value.insert = ( - insert_mock - ) + calendar_resource.return_value.events.return_value.insert = insert_mock return insert_mock diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 6f91fed7b24..80cf9c3d67d 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -110,17 +110,7 @@ def set_time_zone(): dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) -@pytest.fixture(name="google_service") -def mock_google_service(): - """Mock google service.""" - patch_google_service = patch( - "homeassistant.components.google.calendar.GoogleCalendarService" - ) - with patch_google_service as mock_service: - yield mock_service - - -async def test_all_day_event(hass, mock_next_event): +async def test_all_day_event(hass, mock_next_event, mock_token_read): """Test that we can create an event trigger on device.""" week_from_today = dt_util.dt.date.today() + dt_util.dt.timedelta(days=7) end_event = week_from_today + dt_util.dt.timedelta(days=1) @@ -302,9 +292,9 @@ async def test_all_day_offset_event(hass, mock_next_event): } -async def test_update_error(hass, google_service): +async def test_update_error(hass, calendar_resource): """Test that the calendar handles a server error.""" - google_service.return_value.get = Mock( + calendar_resource.return_value.get = Mock( side_effect=httplib2.ServerNotFoundError("unit test") ) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) @@ -315,7 +305,7 @@ async def test_update_error(hass, google_service): assert state.state == "off" -async def test_calendars_api(hass, hass_client, google_service): +async def test_calendars_api(hass, hass_client): """Test the Rest API returns the calendar.""" assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -332,11 +322,9 @@ async def test_calendars_api(hass, hass_client, google_service): ] -async def test_http_event_api_failure(hass, hass_client, google_service): +async def test_http_event_api_failure(hass, hass_client, calendar_resource): """Test the Rest API response during a calendar failure.""" - google_service.return_value.get = Mock( - side_effect=httplib2.ServerNotFoundError("unit test") - ) + calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -352,7 +340,7 @@ async def test_http_event_api_failure(hass, hass_client, google_service): assert events == [] -async def test_http_api_event(hass, hass_client, google_service, mock_events_list): +async def test_http_api_event(hass, hass_client, mock_events_list): """Test querying the API and fetching events from the server.""" now = dt_util.now() @@ -392,7 +380,7 @@ def create_ignore_avail_calendar() -> dict[str, Any]: @pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) -async def test_opaque_event(hass, hass_client, google_service, mock_events_list): +async def test_opaque_event(hass, hass_client, mock_events_list): """Test querying the API and fetching events from the server.""" now = dt_util.now() @@ -426,7 +414,7 @@ async def test_opaque_event(hass, hass_client, google_service, mock_events_list) @pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) -async def test_transparent_event(hass, hass_client, google_service, mock_events_list): +async def test_transparent_event(hass, hass_client, mock_events_list): """Test querying the API and fetching events from the server.""" now = dt_util.now() diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 8fbcb97bfe2..ae803e9fd3b 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -12,11 +12,7 @@ from oauth2client.client import ( import pytest import yaml -from homeassistant.components.google import ( - DOMAIN, - SERVICE_ADD_EVENT, - GoogleCalendarService, -) +from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -118,15 +114,6 @@ async def component_setup( return _setup_func -@pytest.fixture -async def google_service() -> YieldFixture[GoogleCalendarService]: - """Fixture to capture service calls.""" - with patch("homeassistant.components.google.GoogleCalendarService") as mock, patch( - "homeassistant.components.google.calendar.GoogleCalendarService", mock - ): - yield mock - - async def fire_alarm(hass, point_in_time): """Fire an alarm and wait for callbacks to run.""" with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): @@ -150,7 +137,6 @@ async def test_setup_config_empty( async def test_init_success( hass: HomeAssistant, - google_service: GoogleCalendarService, mock_code_flow: Mock, mock_exchange: Mock, mock_notification: Mock, @@ -243,7 +229,6 @@ async def test_existing_token( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_yaml: None, mock_notification: Mock, ) -> None: @@ -266,7 +251,6 @@ async def test_existing_token_missing_scope( token_scopes: list[str], mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_yaml: None, mock_notification: Mock, mock_code_flow: Mock, @@ -295,7 +279,6 @@ async def test_calendar_yaml_missing_required_fields( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, mock_notification: Mock, @@ -313,7 +296,6 @@ async def test_invalid_calendar_yaml( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, mock_notification: Mock, @@ -332,7 +314,6 @@ async def test_found_calendar_from_api( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_list: ApiResult, test_calendar: dict[str, Any], ) -> None: @@ -354,7 +335,6 @@ async def test_add_event( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_list: ApiResult, test_calendar: dict[str, Any], mock_insert_event: Mock, @@ -416,7 +396,6 @@ async def test_add_event_date_ranges( mock_token_read: None, calendars_config: list[dict[str, Any]], component_setup: ComponentSetup, - google_service: GoogleCalendarService, mock_calendars_list: ApiResult, test_calendar: dict[str, Any], mock_insert_event: Mock, From 494ae9aef6c2014491986f6f740785ca8d60cd0b Mon Sep 17 00:00:00 2001 From: James Hewitt Date: Sun, 27 Feb 2022 00:49:46 +0000 Subject: [PATCH 0098/1054] Clean up RFXtrx tests (#67278) Co-authored-by: Martin Hjelmare --- tests/components/rfxtrx/test_config_flow.py | 45 +++++++++------------ tests/components/rfxtrx/test_init.py | 8 ++-- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 82406b8e587..100b6d0e55b 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -35,6 +35,16 @@ def com_port(): return port +async def start_options_flow(hass, entry): + """Start the options flow with the entry under test.""" + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return await hass.config_entries.options.async_init(entry.entry_id) + + @patch("homeassistant.components.rfxtrx.rfxtrxmod.PyNetworkTransport", autospec=True) async def test_setup_network(transport_mock, hass): """Test we can setup network.""" @@ -293,9 +303,8 @@ async def test_options_global(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(entry.entry_id) + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await start_options_flow(hass, entry) assert result["type"] == "form" assert result["step_id"] == "prompt_options" @@ -329,9 +338,8 @@ async def test_no_protocols(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(entry.entry_id) + with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True): + result = await start_options_flow(hass, entry) assert result["type"] == "form" assert result["step_id"] == "prompt_options" @@ -364,9 +372,7 @@ async def test_options_add_device(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(entry.entry_id) + result = await start_options_flow(hass, entry) assert result["type"] == "form" assert result["step_id"] == "prompt_options" @@ -467,10 +473,7 @@ async def test_options_replace_sensor_device(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await start_options_flow(hass, entry) state = hass.states.get( "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" @@ -633,10 +636,7 @@ async def test_options_replace_control_device(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await start_options_flow(hass, entry) state = hass.states.get("binary_sensor.ac_118cdea_2") assert state @@ -732,9 +732,7 @@ async def test_options_add_and_configure_device(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - result = await hass.config_entries.options.async_init(entry.entry_id) + result = await start_options_flow(hass, entry) assert result["type"] == "form" assert result["step_id"] == "prompt_options" @@ -848,12 +846,7 @@ async def test_options_configure_rfy_cover_device(hass): }, unique_id=DOMAIN, ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) + result = await start_options_flow(hass, entry) assert result["type"] == "form" assert result["step_id"] == "prompt_options" diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 2103db35f13..aff4a25770a 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -131,9 +131,9 @@ async def test_connect(hass): entry_data = create_rfx_test_cfg(device="/dev/ttyUSBfake") mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) - with patch.object(rfxtrxmod, "Connect") as connect: - mock_entry.add_to_hass(hass) + mock_entry.add_to_hass(hass) + with patch.object(rfxtrxmod, "Connect") as connect: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -145,9 +145,9 @@ async def test_connect_with_protocols(hass): entry_data = create_rfx_test_cfg(device="/dev/ttyUSBfake", protocols=SOME_PROTOCOLS) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) - with patch.object(rfxtrxmod, "Connect") as connect: - mock_entry.add_to_hass(hass) + mock_entry.add_to_hass(hass) + with patch.object(rfxtrxmod, "Connect") as connect: await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From e4903f9a138f0f3e932dd4b9a1e4b44bbb45d95c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Feb 2022 15:35:39 +0100 Subject: [PATCH 0099/1054] Remove deprecated HTU21D(F) Sensor integration (#67279) --- .coveragerc | 1 - .github/workflows/wheels.yml | 1 - homeassistant/components/htu21d/__init__.py | 1 - homeassistant/components/htu21d/manifest.json | 9 -- homeassistant/components/htu21d/sensor.py | 124 ------------------ requirements_all.txt | 4 - script/gen_requirements_all.py | 1 - 7 files changed, 141 deletions(-) delete mode 100644 homeassistant/components/htu21d/__init__.py delete mode 100644 homeassistant/components/htu21d/manifest.json delete mode 100644 homeassistant/components/htu21d/sensor.py diff --git a/.coveragerc b/.coveragerc index 6508a46c47a..c25d85eba1f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -482,7 +482,6 @@ omit = homeassistant/components/honeywell/climate.py homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py - homeassistant/components/htu21d/sensor.py homeassistant/components/huawei_lte/__init__.py homeassistant/components/huawei_lte/binary_sensor.py homeassistant/components/huawei_lte/device_tracker.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0dc006885f8..b7a9f3b4c20 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -140,7 +140,6 @@ jobs: sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} - sed -i "s|# i2csense|i2csense|g" ${requirement_file} sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} sed -i "s|# pycups|pycups|g" ${requirement_file} sed -i "s|# homekit|homekit|g" ${requirement_file} diff --git a/homeassistant/components/htu21d/__init__.py b/homeassistant/components/htu21d/__init__.py deleted file mode 100644 index c36c8bfcffb..00000000000 --- a/homeassistant/components/htu21d/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The htu21d component.""" diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json deleted file mode 100644 index c554c775079..00000000000 --- a/homeassistant/components/htu21d/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "htu21d", - "name": "HTU21D(F) Sensor", - "documentation": "https://www.home-assistant.io/integrations/htu21d", - "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["i2csense", "smbus"] -} diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py deleted file mode 100644 index e0f7e6d6fbc..00000000000 --- a/homeassistant/components/htu21d/sensor.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Support for HTU21D temperature and humidity sensor.""" -from __future__ import annotations - -from datetime import timedelta -from functools import partial -import logging - -from i2csense.htu21d import HTU21D # pylint: disable=import-error -import smbus -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_BUS = "i2c_bus" -DEFAULT_I2C_BUS = 1 - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) - -DEFAULT_NAME = "HTU21D Sensor" - -SENSOR_TEMPERATURE = "temperature" -SENSOR_HUMIDITY = "humidity" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_HUMIDITY, - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): vol.Coerce(int), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the HTU21D sensor.""" - _LOGGER.warning( - "The HTU21D(F) Sensor integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config.get(CONF_NAME) - bus_number = config.get(CONF_I2C_BUS) - - bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = await hass.async_add_executor_job(partial(HTU21D, bus, logger=_LOGGER)) - if not sensor.sample_ok: - _LOGGER.error("HTU21D sensor not detected in bus %s", bus_number) - return - - sensor_handler = await hass.async_add_executor_job(HTU21DHandler, sensor) - - entities = [ - HTU21DSensor(sensor_handler, name, description) for description in SENSOR_TYPES - ] - - async_add_entities(entities) - - -class HTU21DHandler: - """Implement HTU21D communication.""" - - def __init__(self, sensor): - """Initialize the sensor handler.""" - self.sensor = sensor - self.sensor.update() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Read raw data and calculate temperature and humidity.""" - self.sensor.update() - - -class HTU21DSensor(SensorEntity): - """Implementation of the HTU21D sensor.""" - - def __init__(self, htu21d_client, name, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self._client = htu21d_client - - self._attr_name = f"{name}_{description.key}" - - async def async_update(self): - """Get the latest data from the HTU21D sensor and update the state.""" - await self.hass.async_add_executor_job(self._client.update) - if self._client.sensor.sample_ok: - if self.entity_description.key == SENSOR_TEMPERATURE: - value = round(self._client.sensor.temperature, 1) - else: - value = round(self._client.sensor.humidity, 1) - self._attr_native_value = value - else: - _LOGGER.warning("Bad sample") diff --git a/requirements_all.txt b/requirements_all.txt index 390aab14f6b..d40f0bae869 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -847,9 +847,6 @@ hydrawiser==0.2 # homeassistant.components.hyperion hyperion-py==0.7.4 -# homeassistant.components.htu21d -# i2csense==0.0.4 - # homeassistant.components.iammeter iammeter==0.1.7 @@ -2174,7 +2171,6 @@ smart-meter-texas==0.4.7 smarthab==0.21 # homeassistant.components.bme680 -# homeassistant.components.htu21d # homeassistant.components.raspihats # smbus-cffi==0.5.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e1cb1ae1fef..197b549e76f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -25,7 +25,6 @@ COMMENT_REQUIREMENTS = ( "evdev", "face_recognition", "homeassistant-pyozw", - "i2csense", "opencv-python-headless", "pybluez", "pycups", From 1c0365a72b5a0a15f2dc6a3c0fad2a7fb4e42bc3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Feb 2022 15:40:25 +0100 Subject: [PATCH 0100/1054] Remove deprecated MCP23017 I/O Expander integration (#67281) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/mcp23017/__init__.py | 3 - .../components/mcp23017/binary_sensor.py | 104 ----------------- .../components/mcp23017/manifest.json | 12 -- homeassistant/components/mcp23017/switch.py | 110 ------------------ requirements_all.txt | 4 - 7 files changed, 235 deletions(-) delete mode 100644 homeassistant/components/mcp23017/__init__.py delete mode 100644 homeassistant/components/mcp23017/binary_sensor.py delete mode 100644 homeassistant/components/mcp23017/manifest.json delete mode 100644 homeassistant/components/mcp23017/switch.py diff --git a/.coveragerc b/.coveragerc index c25d85eba1f..a1f6d339429 100644 --- a/.coveragerc +++ b/.coveragerc @@ -668,7 +668,6 @@ omit = homeassistant/components/map/* homeassistant/components/mastodon/notify.py homeassistant/components/matrix/* - homeassistant/components/mcp23017/* homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py homeassistant/components/melcloud/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index fb191264d89..da5c2b0c23a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -540,7 +540,6 @@ homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mazda/* @bdr99 tests/components/mazda/* @bdr99 -homeassistant/components/mcp23017/* @jardiamj homeassistant/components/media_source/* @hunterjm tests/components/media_source/* @hunterjm homeassistant/components/mediaroom/* @dgomes diff --git a/homeassistant/components/mcp23017/__init__.py b/homeassistant/components/mcp23017/__init__.py deleted file mode 100644 index 14799217a6b..00000000000 --- a/homeassistant/components/mcp23017/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Support for I2C MCP23017 chip.""" - -DOMAIN = "mcp23017" diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py deleted file mode 100644 index 161b1ceaac8..00000000000 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Support for binary sensor using I2C MCP23017 chip.""" -from __future__ import annotations - -import logging - -from adafruit_mcp230xx.mcp23017 import MCP23017 -import board -import busio -import digitalio -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_PINS = "pins" -CONF_PULL_MODE = "pull_mode" - -MODE_UP = "UP" -MODE_DOWN = "DOWN" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 -DEFAULT_PULL_MODE = MODE_UP - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SENSORS_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( - vol.Upper, vol.In([MODE_UP, MODE_DOWN]) - ), - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MCP23017 binary sensors.""" - _LOGGER.warning( - "The MCP23017 I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - pull_mode = config[CONF_PULL_MODE] - invert_logic = config[CONF_INVERT_LOGIC] - i2c_address = config[CONF_I2C_ADDRESS] - - i2c = busio.I2C(board.SCL, board.SDA) - mcp = MCP23017(i2c, address=i2c_address) - - binary_sensors = [] - pins = config[CONF_PINS] - - for pin_num, pin_name in pins.items(): - pin = mcp.get_pin(pin_num) - binary_sensors.append( - MCP23017BinarySensor(pin_name, pin, pull_mode, invert_logic) - ) - - add_devices(binary_sensors, True) - - -class MCP23017BinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses MCP23017.""" - - def __init__(self, name, pin, pull_mode, invert_logic): - """Initialize the MCP23017 binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._pull_mode = pull_mode - self._invert_logic = invert_logic - self._state = None - self._pin.direction = digitalio.Direction.INPUT - self._pin.pull = digitalio.Pull.UP - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update the GPIO state.""" - self._state = self._pin.value diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json deleted file mode 100644 index e6f04ad1171..00000000000 --- a/homeassistant/components/mcp23017/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "mcp23017", - "name": "MCP23017 I/O Expander", - "documentation": "https://www.home-assistant.io/integrations/mcp23017", - "requirements": [ - "RPi.GPIO==0.7.1a4", - "adafruit-circuitpython-mcp230xx==2.2.2" - ], - "codeowners": ["@jardiamj"], - "iot_class": "local_polling", - "loggers": ["adafruit_mcp230xx"] -} diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py deleted file mode 100644 index b67f20e3bf6..00000000000 --- a/homeassistant/components/mcp23017/switch.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for switch sensor using I2C MCP23017 chip.""" -from __future__ import annotations - -import logging - -from adafruit_mcp230xx.mcp23017 import MCP23017 -import board -import busio -import digitalio -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_PINS = "pins" -CONF_PULL_MODE = "pull_mode" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SWITCHES_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the MCP23017 devices.""" - _LOGGER.warning( - "The MCP23017 I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - invert_logic = config.get(CONF_INVERT_LOGIC) - i2c_address = config.get(CONF_I2C_ADDRESS) - - i2c = busio.I2C(board.SCL, board.SDA) - mcp = MCP23017(i2c, address=i2c_address) - - switches = [] - pins = config[CONF_PINS] - for pin_num, pin_name in pins.items(): - pin = mcp.get_pin(pin_num) - switches.append(MCP23017Switch(pin_name, pin, invert_logic)) - add_entities(switches) - - -class MCP23017Switch(SwitchEntity): - """Representation of a MCP23017 output pin.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._invert_logic = invert_logic - self._state = False - - self._pin.direction = digitalio.Direction.OUTPUT - self._pin.value = self._invert_logic - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def assumed_state(self): - """Return true if optimistic updates are used.""" - return True - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._pin.value = not self._invert_logic - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._pin.value = self._invert_logic - self._state = False - self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index d40f0bae869..923c8f45956 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,6 @@ PyViCare==2.16.1 PyXiaomiGateway==0.13.4 # homeassistant.components.bmp280 -# homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio # RPi.GPIO==0.7.1a4 @@ -78,9 +77,6 @@ accuweather==0.3.0 # homeassistant.components.bmp280 adafruit-circuitpython-bmp280==3.1.1 -# homeassistant.components.mcp23017 -adafruit-circuitpython-mcp230xx==2.2.2 - # homeassistant.components.adax adax==0.2.0 From dbbb5655e5df0d72ca6b5534af624b54027cbb6d Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 27 Feb 2022 19:29:29 +0100 Subject: [PATCH 0101/1054] Bump async-upnp-client to 0.25.0 (#66414) Co-authored-by: J. Nick Koston --- homeassistant/components/dlna_dmr/data.py | 12 +++-- .../components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/dms.py | 9 +++- .../components/dlna_dms/manifest.json | 2 +- homeassistant/components/network/__init__.py | 12 +++-- homeassistant/components/ssdp/__init__.py | 46 +++++++++++-------- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/__init__.py | 5 +- .../components/upnp/binary_sensor.py | 4 +- homeassistant/components/upnp/config_flow.py | 4 +- homeassistant/components/upnp/device.py | 4 ++ homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/upnp/sensor.py | 2 +- .../components/yeelight/manifest.json | 2 +- homeassistant/components/yeelight/scanner.py | 5 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dlna_dmr/test_data.py | 10 ++-- tests/components/ssdp/test_init.py | 26 +++++------ tests/components/yeelight/__init__.py | 3 +- tests/components/yeelight/test_init.py | 2 +- tests/components/zeroconf/test_init.py | 8 ++-- 23 files changed, 95 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 07046ba4acc..7afe4304581 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -46,7 +46,9 @@ class DlnaDmrData: """Clean up resources when Home Assistant is stopped.""" LOGGER.debug("Cleaning resources in DlnaDmrData") async with self.lock: - tasks = (server.stop_server() for server in self.event_notifiers.values()) + tasks = ( + server.async_stop_server() for server in self.event_notifiers.values() + ) asyncio.gather(*tasks) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) @@ -76,14 +78,14 @@ class DlnaDmrData: return self.event_notifiers[listen_addr].event_handler # Start event handler + source = (listen_addr.host or "0.0.0.0", listen_addr.port) server = AiohttpNotifyServer( requester=self.requester, - listen_port=listen_addr.port, - listen_host=listen_addr.host, + source=source, callback_url=listen_addr.callback_url, loop=hass.loop, ) - await server.start_server() + await server.async_start_server() LOGGER.debug("Started event handler at %s", server.callback_url) self.event_notifiers[listen_addr] = server @@ -103,7 +105,7 @@ class DlnaDmrData: # Shutdown the server when it has no more users if self.event_notifier_refs[listen_addr] == 0: server = self.event_notifiers.pop(listen_addr) - await server.stop_server() + await server.async_stop_server() # Remove the cleanup listener when there's nothing left to cleanup if not self.event_notifiers: diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 4001fc9dddc..76bf8ac051c 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.25.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index d3a65448f84..74774a84821 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -7,7 +7,12 @@ from dataclasses import dataclass import functools from typing import Any, TypeVar, cast -from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester +from async_upnp_client import ( + UpnpEventHandler, + UpnpFactory, + UpnpNotifyServer, + UpnpRequester, +) from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.const import NotificationSubType from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError @@ -68,7 +73,7 @@ class DlnaDmsData: self.upnp_factory = UpnpFactory(self.requester, non_strict=True) # NOTE: event_handler is not actually used, and is only created to # satisfy the DmsDevice.__init__ signature - self.event_handler = UpnpEventHandler("", self.requester) + self.event_handler = UpnpEventHandler(UpnpNotifyServer(), self.requester) self.devices = {} self.sources = {} diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 84cfc2e69fd..9f4d02d4462 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.25.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/network/__init__.py b/homeassistant/components/network/__init__.py index b3ef88e7ab2..8c1a0020cfb 100644 --- a/homeassistant/components/network/__init__.py +++ b/homeassistant/components/network/__init__.py @@ -60,12 +60,14 @@ async def async_get_enabled_source_ips( if not adapter["enabled"]: continue if adapter["ipv4"]: - sources.extend(IPv4Address(ipv4["address"]) for ipv4 in adapter["ipv4"]) + addrs_ipv4 = [IPv4Address(ipv4["address"]) for ipv4 in adapter["ipv4"]] + sources.extend(addrs_ipv4) if adapter["ipv6"]: - # With python 3.9 add scope_ids can be - # added by enumerating adapter["ipv6"]s - # IPv6Address(f"::%{ipv6['scope_id']}") - sources.extend(IPv6Address(ipv6["address"]) for ipv6 in adapter["ipv6"]) + addrs_ipv6 = [ + IPv6Address(f"{ipv6['address']}%{ipv6['scope_id']}") + for ipv6 in adapter["ipv6"] + ] + sources.extend(addrs_ipv6) return sources diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b1b06832918..80d9a716df0 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -11,10 +11,15 @@ import logging from typing import Any from async_upnp_client.aiohttp import AiohttpSessionRequester -from async_upnp_client.const import DeviceOrServiceType, SsdpHeaders, SsdpSource +from async_upnp_client.const import ( + AddressTupleVXType, + DeviceOrServiceType, + SsdpHeaders, + SsdpSource, +) from async_upnp_client.description_cache import DescriptionCache -from async_upnp_client.ssdp import SSDP_PORT -from async_upnp_client.ssdp_listener import SsdpDevice, SsdpListener +from async_upnp_client.ssdp import SSDP_PORT, determine_source_target, is_ipv4_address +from async_upnp_client.ssdp_listener import SsdpDevice, SsdpDeviceTracker, SsdpListener from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries @@ -372,17 +377,10 @@ class Scanner: async def _async_build_source_set(self) -> set[IPv4Address | IPv6Address]: """Build the list of ssdp sources.""" - adapters = await network.async_get_adapters(self.hass) - sources: set[IPv4Address | IPv6Address] = set() - if network.async_only_default_interface_enabled(adapters): - sources.add(IPv4Address("0.0.0.0")) - return sources - return { source_ip for source_ip in await network.async_get_enabled_source_ips(self.hass) - if not source_ip.is_loopback - and not (isinstance(source_ip, IPv6Address) and source_ip.is_global) + if not source_ip.is_loopback and not source_ip.is_global } async def async_scan(self, *_: Any) -> None: @@ -401,11 +399,8 @@ class Scanner: # address. This matches pysonos' behavior # https://github.com/amelchio/pysonos/blob/d4329b4abb657d106394ae69357805269708c996/pysonos/discovery.py#L120 for listener in self._ssdp_listeners: - try: - IPv4Address(listener.source_ip) - except ValueError: - continue - await listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) + if is_ipv4_address(listener.source): + await listener.async_search((str(IPV4_BROADCAST), SSDP_PORT)) async def async_start(self) -> None: """Start the scanners.""" @@ -425,11 +420,26 @@ class Scanner: async def _async_start_ssdp_listeners(self) -> None: """Start the SSDP Listeners.""" + # Devices are shared between all sources. + device_tracker = SsdpDeviceTracker() for source_ip in await self._async_build_source_set(): + source_ip_str = str(source_ip) + if source_ip.version == 6: + source_tuple: AddressTupleVXType = ( + source_ip_str, + 0, + 0, + int(getattr(source_ip, "scope_id")), + ) + else: + source_tuple = (source_ip_str, 0) + source, target = determine_source_target(source_tuple) self._ssdp_listeners.append( SsdpListener( async_callback=self._ssdp_listener_callback, - source_ip=source_ip, + source=source, + target=target, + device_tracker=device_tracker, ) ) results = await asyncio.gather( @@ -441,7 +451,7 @@ class Scanner: if isinstance(result, Exception): _LOGGER.warning( "Failed to setup listener for %s: %s", - self._ssdp_listeners[idx].source_ip, + self._ssdp_listeners[idx].source, result, ) failed_listeners.append(self._ssdp_listeners[idx]) diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 93512d08238..a4e02759989 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.25.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 5180932080c..a3490ad8037 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -177,8 +177,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - # Create sensors. - LOGGER.debug("Enabling sensors") + # Setup platforms, creating sensors/binary_sensors. hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -188,7 +187,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload a UPnP/IGD device from a config entry.""" LOGGER.debug("Unloading config entry: %s", config_entry.unique_id) - LOGGER.debug("Deleting sensors") + # Unload platforms. return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index 1ff1164cc58..d848aff5a88 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -28,8 +28,6 @@ async def async_setup_entry( """Set up the UPnP/IGD sensors.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - LOGGER.debug("Adding binary sensor") - entities = [ UpnpStatusBinarySensor( coordinator=coordinator, @@ -38,7 +36,7 @@ async def async_setup_entry( for entity_description in BINARYSENSOR_ENTITY_DESCRIPTIONS if coordinator.data.get(entity_description.key) is not None ] - LOGGER.debug("Adding entities: %s", entities) + LOGGER.debug("Adding binary_sensor entities: %s", entities) async_add_entities(entities) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 74acb88983b..1b8507d25bf 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -222,7 +222,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) hostname = discovery_info.ssdp_headers["_host"] - self._abort_if_unique_id_configured(updates={CONFIG_ENTRY_HOSTNAME: hostname}) + self._abort_if_unique_id_configured( + updates={CONFIG_ENTRY_HOSTNAME: hostname}, reload_on_update=False + ) # Handle devices changing their UDN, only allow a single host. existing_entries = self._async_current_entries() diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 3231d34a342..be0ae725396 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -80,6 +80,10 @@ class Device: if service_info.ssdp_location is None: return + if change == SsdpChange.ALIVE: + # We care only about updates. + return + device = self._igd_device.device if service_info.ssdp_location == device.device_url: return diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 5b630973f67..19b0e4529a4 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.23.5"], + "requirements": ["async-upnp-client==0.25.0"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 54176715b84..595a14a5a9f 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -136,7 +136,7 @@ async def async_setup_entry( ] ) - LOGGER.debug("Adding entities: %s", entities) + LOGGER.debug("Adding sensor entities: %s", entities) async_add_entities(entities) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 7aa46881968..b6defca2060 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.9", "async-upnp-client==0.23.5"], + "requirements": ["yeelight==0.7.9", "async-upnp-client==0.25.0"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 1756fbe865c..6876b93a0eb 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -67,12 +67,13 @@ class YeelightScanner: return _async_connected + source = (str(source_ip), 0) self._listeners.append( SsdpSearchListener( async_callback=self._async_process_entry, service_type=SSDP_ST, target=SSDP_TARGET, - source_ip=source_ip, + source=source, async_connect_callback=_wrap_async_connected_idx(idx), ) ) @@ -87,7 +88,7 @@ class YeelightScanner: continue _LOGGER.warning( "Failed to setup listener for %s: %s", - self._listeners[idx].source_ip, + self._listeners[idx].source, result, ) failed_listeners.append(self._listeners[idx]) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a36f21efd6b..a333d603a33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.8 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.23.5 +async-upnp-client==0.25.0 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 923c8f45956..df781d28836 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -327,7 +327,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.5 +async-upnp-client==0.25.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86482356bfd..ff12e10ee01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -256,7 +256,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.23.5 +async-upnp-client==0.25.0 # homeassistant.components.sleepiq asyncsleepiq==1.1.0 diff --git a/tests/components/dlna_dmr/test_data.py b/tests/components/dlna_dmr/test_data.py index 469cfcece88..2a86070ea72 100644 --- a/tests/components/dlna_dmr/test_data.py +++ b/tests/components/dlna_dmr/test_data.py @@ -37,7 +37,10 @@ def aiohttp_notify_servers_mock() -> Iterable[Mock]: # Every server must be stopped if it was started for server in servers: - assert server.start_server.call_count == server.stop_server.call_count + assert ( + server.async_start_server.call_count + == server.async_stop_server.call_count + ) async def test_get_domain_data(hass: HomeAssistant) -> None: @@ -60,7 +63,7 @@ async def test_event_notifier( # Check that the parameters were passed through to the AiohttpNotifyServer aiohttp_notify_servers_mock.assert_called_with( - requester=ANY, listen_port=0, listen_host=None, callback_url=None, loop=ANY + requester=ANY, source=("0.0.0.0", 0), callback_url=None, loop=ANY ) # Same address should give same notifier @@ -79,8 +82,7 @@ async def test_event_notifier( # Check that the parameters were passed through to the AiohttpNotifyServer aiohttp_notify_servers_mock.assert_called_with( requester=ANY, - listen_port=9999, - listen_host="192.88.99.4", + source=("192.88.99.4", 9999), callback_url="http://192.88.99.4:9999/notify", loop=ANY, ) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 758f8fb79ba..b947ce4269a 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access from datetime import datetime, timedelta -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address from unittest.mock import ANY, AsyncMock, patch from async_upnp_client.ssdp import udn_from_headers @@ -25,7 +25,7 @@ from tests.common import async_fire_time_changed def _ssdp_headers(headers): - ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime(2021, 1, 1, 12, 00)) + ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime.now()) ssdp_headers["_udn"] = udn_from_headers(ssdp_headers) return ssdp_headers @@ -386,7 +386,7 @@ async def test_discovery_from_advertisement_sets_ssdp_st( # End compatibility checks -@patch( # XXX TODO: Isn't this duplicate with mock_get_source_ip? +@patch( "homeassistant.components.ssdp.Scanner._async_build_source_set", return_value={IPv4Address("192.168.1.1")}, ) @@ -486,7 +486,7 @@ async def test_scan_with_registered_callback( mock_call_data.ssdp_usn == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st" ) assert mock_call_data.ssdp_headers["x-rincon-bootseq"] == "55" - assert mock_call_data.ssdp_udn == ANY + assert mock_call_data.ssdp_udn == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" assert mock_call_data.ssdp_headers["_timestamp"] == ANY assert mock_call_data.x_homeassistant_matching_domains == set() assert mock_call_data.upnp == { @@ -711,7 +711,7 @@ _ADAPTERS_WITH_MANUAL_CONFIG = [ @patch( "homeassistant.components.ssdp.network.async_get_adapters", return_value=_ADAPTERS_WITH_MANUAL_CONFIG, -) # XXX TODO: Isn't this duplicate with mock_get_source_ip? +) async def test_async_detect_interfaces_setting_empty_route( mock_get_adapters, mock_get_ssdp, hass ): @@ -719,8 +719,8 @@ async def test_async_detect_interfaces_setting_empty_route( await init_ssdp_component(hass) ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners - source_ips = {ssdp_listener.source_ip for ssdp_listener in ssdp_listeners} - assert source_ips == {IPv6Address("2001:db8::"), IPv4Address("192.168.1.5")} + sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners} + assert sources == {("2001:db8::%1", 0, 0, 1), ("192.168.1.5", 0)} @pytest.mark.usefixtures("mock_get_source_ip") @@ -737,14 +737,14 @@ async def test_async_detect_interfaces_setting_empty_route( @patch( "homeassistant.components.ssdp.network.async_get_adapters", return_value=_ADAPTERS_WITH_MANUAL_CONFIG, -) # XXX TODO: Isn't this duplicate with mock_get_source_ip? +) async def test_bind_failure_skips_adapter( mock_get_adapters, mock_get_ssdp, hass, caplog ): """Test that an adapter with a bind failure is skipped.""" async def _async_start(self): - if self.source_ip == IPv6Address("2001:db8::"): + if self.source == ("2001:db8::%1", 0, 0, 1): raise OSError SsdpListener.async_start = _async_start @@ -753,10 +753,8 @@ async def test_bind_failure_skips_adapter( assert "Failed to setup listener for" in caplog.text ssdp_listeners = hass.data[ssdp.DOMAIN]._ssdp_listeners - source_ips = {ssdp_listener.source_ip for ssdp_listener in ssdp_listeners} - assert source_ips == { - IPv4Address("192.168.1.5") - } # Note no SsdpListener for IPv6 address. + sources = {ssdp_listener.source for ssdp_listener in ssdp_listeners} + assert sources == {("192.168.1.5", 0)} # Note no SsdpListener for IPv6 address. @pytest.mark.usefixtures("mock_get_source_ip") @@ -773,7 +771,7 @@ async def test_bind_failure_skips_adapter( @patch( "homeassistant.components.ssdp.network.async_get_adapters", return_value=_ADAPTERS_WITH_MANUAL_CONFIG, -) # XXX TODO: Isn't this duplicate with mock_get_source_ip? +) async def test_ipv4_does_additional_search_for_sonos( mock_get_adapters, mock_get_ssdp, hass ): diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index d0112c5a544..96f8846b174 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,7 +1,6 @@ """Tests for the Yeelight integration.""" import asyncio from datetime import timedelta -from ipaddress import IPv4Address from unittest.mock import AsyncMock, MagicMock, patch from async_upnp_client.search import SsdpSearchListener @@ -162,7 +161,7 @@ def _patched_ssdp_listener(info: ssdp.SsdpHeaders, *args, **kwargs): listener = SsdpSearchListener(*args, **kwargs) async def _async_callback(*_): - if kwargs["source_ip"] == IPv4Address(FAIL_TO_BIND_IP): + if kwargs["source"][0] == FAIL_TO_BIND_IP: raise OSError await listener.async_connect_callback() diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index dc3d602edeb..cba40eb0262 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -241,7 +241,7 @@ async def test_setup_discovery_with_manually_configured_network_adapter_one_fail assert hass.states.get(ENTITY_BINARY_SENSOR) is None assert hass.states.get(ENTITY_LIGHT) is None - assert f"Failed to setup listener for {FAIL_TO_BIND_IP}" in caplog.text + assert f"Failed to setup listener for ('{FAIL_TO_BIND_IP}', 0)" in caplog.text async def test_setup_import(hass: HomeAssistant): diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index b7e99991fdd..28a4f5d969f 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -955,11 +955,11 @@ async def test_async_detect_interfaces_setting_empty_route_linux( await hass.async_block_till_done() assert mock_zc.mock_calls[0] == call( interfaces=[ - "2001:db8::", - "fe80::1234:5678:9abc:def0", + "2001:db8::%1", + "fe80::1234:5678:9abc:def0%1", "192.168.1.5", "172.16.1.5", - "fe80::dead:beef:dead:beef", + "fe80::dead:beef:dead:beef%3", ], ip_version=IPVersion.All, ) @@ -1053,7 +1053,7 @@ async def test_async_detect_interfaces_explicitly_set_ipv6_linux( await hass.async_block_till_done() assert mock_zc.mock_calls[0] == call( - interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef"], + interfaces=["192.168.1.5", "fe80::dead:beef:dead:beef%3"], ip_version=IPVersion.All, ) From 466f1686e4409622daa5b973da9d44e9e2b955bf Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Feb 2022 10:58:00 -0800 Subject: [PATCH 0102/1054] Cleanup google calendar shared test fixtures (#67343) --- tests/components/google/conftest.py | 23 +-- tests/components/google/test_calendar.py | 217 ++++++++++------------- 2 files changed, 102 insertions(+), 138 deletions(-) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index b78f97e8209..f224f3f2f31 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -98,16 +98,6 @@ async def mock_token_read( storage.put(creds) -@pytest.fixture -def mock_next_event(): - """Mock the google calendar data.""" - patch_google_cal = patch( - "homeassistant.components.google.calendar.GoogleCalendarData" - ) - with patch_google_cal as google_cal_data: - yield google_cal_data - - @pytest.fixture(autouse=True) def calendar_resource() -> YieldFixture[google_discovery.Resource]: """Fixture to mock out the Google discovery API.""" @@ -130,6 +120,19 @@ def mock_events_list( return _put_result +@pytest.fixture +def mock_events_list_items( + mock_events_list: Callable[[dict[str, Any]], None] +) -> Callable[list[[dict[str, Any]]], None]: + """Fixture to construct an API response containing event items.""" + + def _put_items(items: list[dict[str, Any]]) -> None: + mock_events_list({"items": items}) + return + + return _put_items + + @pytest.fixture def mock_calendars_list( calendar_resource: google_discovery.Resource, diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 80cf9c3d67d..dda30a1d83e 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -2,7 +2,6 @@ from __future__ import annotations -import copy from http import HTTPStatus from typing import Any from unittest.mock import Mock, patch @@ -110,16 +109,33 @@ def set_time_zone(): dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) -async def test_all_day_event(hass, mock_next_event, mock_token_read): +def upcoming() -> dict[str, Any]: + """Create a test event with an arbitrary start/end time fetched from the api url.""" + now = dt_util.now() + return { + "start": {"dateTime": now.isoformat()}, + "end": {"dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat()}, + } + + +def upcoming_event_url() -> str: + """Return a calendar API to return events created by upcoming().""" + now = dt_util.now() + start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() + end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + return f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}" + + +async def test_all_day_event(hass, mock_events_list_items, mock_token_read): """Test that we can create an event trigger on device.""" week_from_today = dt_util.dt.date.today() + dt_util.dt.timedelta(days=7) end_event = week_from_today + dt_util.dt.timedelta(days=1) - event = copy.deepcopy(TEST_EVENT) - start = week_from_today.isoformat() - end = end_event.isoformat() - event["start"]["date"] = start - event["end"]["date"] = end - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"date": week_from_today.isoformat()}, + "end": {"date": end_event.isoformat()}, + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -139,16 +155,16 @@ async def test_all_day_event(hass, mock_next_event, mock_token_read): } -async def test_future_event(hass, mock_next_event): +async def test_future_event(hass, mock_events_list_items): """Test that we can create an event trigger on device.""" one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) end_event = one_hour_from_now + dt_util.dt.timedelta(minutes=60) - start = one_hour_from_now.isoformat() - end = end_event.isoformat() - event = copy.deepcopy(TEST_EVENT) - event["start"]["dateTime"] = start - event["end"]["dateTime"] = end - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"dateTime": one_hour_from_now.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -168,16 +184,16 @@ async def test_future_event(hass, mock_next_event): } -async def test_in_progress_event(hass, mock_next_event): +async def test_in_progress_event(hass, mock_events_list_items): """Test that we can create an event trigger on device.""" middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) - start = middle_of_event.isoformat() - end = end_event.isoformat() - event = copy.deepcopy(TEST_EVENT) - event["start"]["dateTime"] = start - event["end"]["dateTime"] = end - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"dateTime": middle_of_event.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -197,18 +213,18 @@ async def test_in_progress_event(hass, mock_next_event): } -async def test_offset_in_progress_event(hass, mock_next_event): +async def test_offset_in_progress_event(hass, mock_events_list_items): """Test that we can create an event trigger on device.""" middle_of_event = dt_util.now() + dt_util.dt.timedelta(minutes=14) end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) - start = middle_of_event.isoformat() - end = end_event.isoformat() event_summary = "Test Event in Progress" - event = copy.deepcopy(TEST_EVENT) - event["start"]["dateTime"] = start - event["end"]["dateTime"] = end - event["summary"] = f"{event_summary} !!-15" - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"dateTime": middle_of_event.isoformat()}, + "end": {"dateTime": end_event.isoformat()}, + "summary": f"{event_summary} !!-15", + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -229,18 +245,18 @@ async def test_offset_in_progress_event(hass, mock_next_event): @pytest.mark.skip -async def test_all_day_offset_in_progress_event(hass, mock_next_event): +async def test_all_day_offset_in_progress_event(hass, mock_events_list_items): """Test that we can create an event trigger on device.""" tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=1) end_event = tomorrow + dt_util.dt.timedelta(days=1) - start = tomorrow.isoformat() - end = end_event.isoformat() event_summary = "Test All Day Event Offset In Progress" - event = copy.deepcopy(TEST_EVENT) - event["start"]["date"] = start - event["end"]["date"] = end - event["summary"] = f"{event_summary} !!-25:0" - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"date": tomorrow.isoformat()}, + "end": {"date": end_event.isoformat()}, + "summary": f"{event_summary} !!-25:0", + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -260,19 +276,19 @@ async def test_all_day_offset_in_progress_event(hass, mock_next_event): } -async def test_all_day_offset_event(hass, mock_next_event): +async def test_all_day_offset_event(hass, mock_events_list_items): """Test that we can create an event trigger on device.""" tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=2) end_event = tomorrow + dt_util.dt.timedelta(days=1) - start = tomorrow.isoformat() - end = end_event.isoformat() offset_hours = 1 + dt_util.now().hour event_summary = "Test All Day Event Offset" - event = copy.deepcopy(TEST_EVENT) - event["start"]["date"] = start - event["end"]["date"] = end - event["summary"] = f"{event_summary} !!-{offset_hours}:0" - mock_next_event.return_value.event = event + event = { + **TEST_EVENT, + "start": {"date": tomorrow.isoformat()}, + "end": {"date": end_event.isoformat()}, + "summary": f"{event_summary} !!-{offset_hours}:0", + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() @@ -329,47 +345,31 @@ async def test_http_event_api_failure(hass, hass_client, calendar_resource): assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() - start = dt_util.now().isoformat() - end = (dt_util.now() + dt_util.dt.timedelta(minutes=60)).isoformat() - client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK # A failure to talk to the server results in an empty list of events events = await response.json() assert events == [] -async def test_http_api_event(hass, hass_client, mock_events_list): +async def test_http_api_event(hass, hass_client, mock_events_list_items): """Test querying the API and fetching events from the server.""" - now = dt_util.now() - - mock_events_list( - { - "items": [ - { - "summary": "Event title", - "start": {"dateTime": now.isoformat()}, - "end": { - "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() - }, - } - ], - } - ) + event = { + **TEST_EVENT, + **upcoming(), + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() - client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK events = await response.json() assert len(events) == 1 assert "summary" in events[0] - assert events[0]["summary"] == "Event title" + assert events[0]["summary"] == event["summary"] def create_ignore_avail_calendar() -> dict[str, Any]: @@ -379,67 +379,28 @@ def create_ignore_avail_calendar() -> dict[str, Any]: return calendar -@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) -async def test_opaque_event(hass, hass_client, mock_events_list): +@pytest.mark.parametrize( + "test_calendar,transparency,expect_visible_event", + [ + (create_ignore_avail_calendar(), "opaque", True), + (create_ignore_avail_calendar(), "transparent", False), + ], +) +async def test_opaque_event( + hass, hass_client, mock_events_list_items, transparency, expect_visible_event +): """Test querying the API and fetching events from the server.""" - now = dt_util.now() - - mock_events_list( - { - "items": [ - { - "summary": "Event title", - "transparency": "opaque", - "start": {"dateTime": now.isoformat()}, - "end": { - "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() - }, - } - ], - } - ) + event = { + **TEST_EVENT, + **upcoming(), + "transparency": transparency, + } + mock_events_list_items([event]) assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) await hass.async_block_till_done() - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() - client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK events = await response.json() - assert len(events) == 1 - assert "summary" in events[0] - assert events[0]["summary"] == "Event title" - - -@pytest.mark.parametrize("test_calendar", [create_ignore_avail_calendar()]) -async def test_transparent_event(hass, hass_client, mock_events_list): - """Test querying the API and fetching events from the server.""" - now = dt_util.now() - - mock_events_list( - { - "items": [ - { - "summary": "Event title", - "transparency": "transparent", - "start": {"dateTime": now.isoformat()}, - "end": { - "dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat() - }, - } - ], - } - ) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() - - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() - - client = await hass_client() - response = await client.get(f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}") - assert response.status == HTTPStatus.OK - events = await response.json() - assert events == [] + assert (len(events) > 0) == expect_visible_event From e77de3aeeace3a768382cec14291f6b1f5b6f830 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Feb 2022 20:16:13 +0100 Subject: [PATCH 0103/1054] Remove deprecated Bosch BMP280 Environmental Sensor integration (#67280) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/bmp280/__init__.py | 1 - homeassistant/components/bmp280/manifest.json | 9 -- homeassistant/components/bmp280/sensor.py | 148 ------------------ requirements_all.txt | 4 - 6 files changed, 164 deletions(-) delete mode 100644 homeassistant/components/bmp280/__init__.py delete mode 100644 homeassistant/components/bmp280/manifest.json delete mode 100644 homeassistant/components/bmp280/sensor.py diff --git a/.coveragerc b/.coveragerc index a1f6d339429..0a1c0b3d2b7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -117,7 +117,6 @@ omit = homeassistant/components/bluesound/* homeassistant/components/bluetooth_tracker/* homeassistant/components/bme680/sensor.py - homeassistant/components/bmp280/sensor.py homeassistant/components/bmw_connected_drive/__init__.py homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/button.py diff --git a/CODEOWNERS b/CODEOWNERS index da5c2b0c23a..25b44d84267 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -117,7 +117,6 @@ tests/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core tests/components/blueprint/* @home-assistant/core homeassistant/components/bluesound/* @thrawnarn -homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe tests/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bond/* @bdraco @prystupa @joshs85 diff --git a/homeassistant/components/bmp280/__init__.py b/homeassistant/components/bmp280/__init__.py deleted file mode 100644 index 0c884eafbf1..00000000000 --- a/homeassistant/components/bmp280/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The Bosch BMP280 sensor integration.""" diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json deleted file mode 100644 index 5347c93f4fa..00000000000 --- a/homeassistant/components/bmp280/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "bmp280", - "name": "Bosch BMP280 Environmental Sensor", - "documentation": "https://www.home-assistant.io/integrations/bmp280", - "codeowners": ["@belidzs"], - "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.1a4"], - "quality_scale": "silver", - "iot_class": "local_polling" -} diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py deleted file mode 100644 index 5138590c1dd..00000000000 --- a/homeassistant/components/bmp280/sensor.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Platform for Bosch BMP280 Environmental Sensor integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -from adafruit_bmp280 import Adafruit_BMP280_I2C -import board -from busio import I2C -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.const import CONF_NAME, PRESSURE_HPA, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "BMP280" -SCAN_INTERVAL = timedelta(seconds=15) - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) - -MIN_I2C_ADDRESS = 0x76 -MAX_I2C_ADDRESS = 0x77 - -CONF_I2C_ADDRESS = "i2c_address" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_I2C_ADDRESS): vol.All( - vol.Coerce(int), vol.Range(min=MIN_I2C_ADDRESS, max=MAX_I2C_ADDRESS) - ), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sensor platform.""" - _LOGGER.warning( - "The Bosch BMP280 Environmental Sensor integration is deprecated and " - "will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0019, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - try: - # initializing I2C bus using the auto-detected pins - i2c = I2C(board.SCL, board.SDA) - # initializing the sensor - bmp280 = Adafruit_BMP280_I2C(i2c, address=config[CONF_I2C_ADDRESS]) - except ValueError as error: - # this usually happens when the board is I2C capable, but the device can't be found at the configured address - if str(error.args[0]).startswith("No I2C device at address"): - _LOGGER.error( - "%s. Hint: Check wiring and make sure that the SDO pin is tied to either ground (0x76) or VCC (0x77)", - error.args[0], - ) - raise PlatformNotReady() from error - _LOGGER.error(error) - return - # use custom name if there's any - name = config[CONF_NAME] - # BMP280 has both temperature and pressure sensing capability - add_entities( - [Bmp280TemperatureSensor(bmp280, name), Bmp280PressureSensor(bmp280, name)] - ) - - -class Bmp280Sensor(SensorEntity): - """Base class for BMP280 entities.""" - - def __init__( - self, - bmp280: Adafruit_BMP280_I2C, - name: str, - unit_of_measurement: str, - device_class: str, - ) -> None: - """Initialize the sensor.""" - self._bmp280 = bmp280 - self._attr_name = name - self._attr_native_unit_of_measurement = unit_of_measurement - - -class Bmp280TemperatureSensor(Bmp280Sensor): - """Representation of a Bosch BMP280 Temperature Sensor.""" - - def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: - """Initialize the entity.""" - super().__init__( - bmp280, f"{name} Temperature", TEMP_CELSIUS, SensorDeviceClass.TEMPERATURE - ) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for the sensor.""" - try: - self._attr_native_value = round(self._bmp280.temperature, 1) - if not self.available: - _LOGGER.warning("Communication restored with temperature sensor") - self._attr_available = True - except OSError: - # this is thrown when a working sensor is unplugged between two updates - _LOGGER.warning( - "Unable to read temperature data due to a communication problem" - ) - self._attr_available = False - - -class Bmp280PressureSensor(Bmp280Sensor): - """Representation of a Bosch BMP280 Barometric Pressure Sensor.""" - - def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str) -> None: - """Initialize the entity.""" - super().__init__( - bmp280, f"{name} Pressure", PRESSURE_HPA, SensorDeviceClass.PRESSURE - ) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Fetch new state data for the sensor.""" - try: - self._attr_native_value = round(self._bmp280.pressure) - if not self.available: - _LOGGER.warning("Communication restored with pressure sensor") - self._attr_available = True - except OSError: - # this is thrown when a working sensor is unplugged between two updates - _LOGGER.warning( - "Unable to read pressure data due to a communication problem" - ) - self._attr_available = False diff --git a/requirements_all.txt b/requirements_all.txt index df781d28836..8d17ff45dfa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,6 @@ PyViCare==2.16.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 -# homeassistant.components.bmp280 # homeassistant.components.rpi_gpio # RPi.GPIO==0.7.1a4 @@ -74,9 +73,6 @@ abodepy==1.2.0 # homeassistant.components.accuweather accuweather==0.3.0 -# homeassistant.components.bmp280 -adafruit-circuitpython-bmp280==3.1.1 - # homeassistant.components.adax adax==0.2.0 From e0172cb8da66263eec35c23c26acdf36f6a0ce04 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 27 Feb 2022 11:19:20 -0800 Subject: [PATCH 0104/1054] Bump pyoverkiz to 1.3.9 in Overkiz integration (#67339) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 1c5d6b1b685..5e8fe27e21e 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": [ - "pyoverkiz==1.3.8" + "pyoverkiz==1.3.9" ], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 8d17ff45dfa..b57fbacfe6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1701,7 +1701,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.8 +pyoverkiz==1.3.9 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff12e10ee01..cea283bb3e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1118,7 +1118,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.8 +pyoverkiz==1.3.9 # homeassistant.components.openweathermap pyowm==3.2.0 From f9b02d5cabe914a9a6fcc5ffbc29fec98a5a68c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Feb 2022 12:04:22 -0800 Subject: [PATCH 0105/1054] Guard for index error in picnic (#67345) --- homeassistant/components/picnic/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/picnic/coordinator.py b/homeassistant/components/picnic/coordinator.py index 773142a0109..9f387858e5f 100644 --- a/homeassistant/components/picnic/coordinator.py +++ b/homeassistant/components/picnic/coordinator.py @@ -112,7 +112,7 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator): next_delivery = ( copy.deepcopy(next_deliveries[-1]) if next_deliveries else {} ) - last_order = copy.deepcopy(deliveries[0]) + last_order = copy.deepcopy(deliveries[0]) if deliveries else {} except (KeyError, TypeError): # A KeyError or TypeError indicate that the response contains unexpected data return {}, {} From 9c440d8aa64ee4df9f8c4b8594b5f55703f82445 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Feb 2022 12:59:05 -0800 Subject: [PATCH 0106/1054] Guard for non-string inputs in Alexa (#67348) --- homeassistant/components/alexa/capabilities.py | 2 ++ tests/components/alexa/test_capabilities.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 133ad4f2bda..327d5973892 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -822,6 +822,8 @@ class AlexaInputController(AlexaCapability): """Return list of supported inputs.""" input_list = [] for source in source_list: + if not isinstance(source, str): + continue formatted_source = ( source.lower().replace("-", "").replace("_", "").replace(" ", "") ) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 8a9a40e3217..d24849e1006 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -182,7 +182,7 @@ async def test_api_increase_color_temp(hass, result, initial): @pytest.mark.parametrize( "domain,payload,source_list,idx", [ - ("media_player", "GAME CONSOLE", ["tv", "game console"], 1), + ("media_player", "GAME CONSOLE", ["tv", "game console", 10000], 1), ("media_player", "SATELLITE TV", ["satellite-tv", "game console"], 0), ("media_player", "SATELLITE TV", ["satellite_tv", "game console"], 0), ("media_player", "BAD DEVICE", ["satellite_tv", "game console"], None), From bb4b7c96d0ff88dcb57a590d1e127a2d0b829b8d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 27 Feb 2022 22:38:39 +0100 Subject: [PATCH 0107/1054] Migrate entry unique id for Sensibo (#67119) --- homeassistant/components/sensibo/__init__.py | 28 +++- .../components/sensibo/config_flow.py | 61 +++----- homeassistant/components/sensibo/strings.json | 7 +- .../components/sensibo/translations/en.json | 5 +- homeassistant/components/sensibo/util.py | 49 +++++++ tests/components/sensibo/test_config_flow.py | 136 ++++++++++++++---- 6 files changed, 212 insertions(+), 74 deletions(-) create mode 100644 homeassistant/components/sensibo/util.py diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index b62482b60b5..ab8e4e85d39 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -1,11 +1,15 @@ """The sensibo component.""" from __future__ import annotations +from pysensibo.exceptions import AuthenticationError + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, LOGGER, PLATFORMS from .coordinator import SensiboDataUpdateCoordinator +from .util import NoDevicesError, NoUsernameError, async_validate_api async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -28,3 +32,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return True return False + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + # Change entry unique id from api_key to username + if entry.version == 1: + api_key = entry.data[CONF_API_KEY] + + try: + new_unique_id = await async_validate_api(hass, api_key) + except (AuthenticationError, ConnectionError, NoDevicesError, NoUsernameError): + return False + + entry.version = 2 + + LOGGER.debug("Migrate Sensibo config entry unique id to %s", new_unique_id) + hass.config_entries.async_update_entry( + entry, + unique_id=new_unique_id, + ) + + return True diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index f970581e2a8..dca5ea62fe6 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -1,25 +1,16 @@ """Adds config flow for Sensibo integration.""" from __future__ import annotations -import asyncio -import logging - -import aiohttp -import async_timeout -from pysensibo import SensiboClient -from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY -from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_NAME, DOMAIN, TIMEOUT - -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_NAME, DOMAIN +from .util import NoDevicesError, NoUsernameError, async_validate_api DATA_SCHEMA = vol.Schema( { @@ -28,39 +19,14 @@ DATA_SCHEMA = vol.Schema( ) -async def async_validate_api(hass: HomeAssistant, api_key: str) -> bool: - """Get data from API.""" - client = SensiboClient( - api_key, - session=async_get_clientsession(hass), - timeout=TIMEOUT, - ) - - try: - async with async_timeout.timeout(TIMEOUT): - if await client.async_get_devices(): - return True - except ( - aiohttp.ClientConnectionError, - asyncio.TimeoutError, - AuthenticationError, - SensiboError, - ) as err: - _LOGGER.error("Failed to get devices from Sensibo servers %s", err) - return False - - class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Sensibo integration.""" - VERSION = 1 + VERSION = 2 async def async_step_import(self, config: dict) -> FlowResult: """Import a configuration from config.yaml.""" - self.context.update( - {"title_placeholders": {"Sensibo": f"YAML import {DOMAIN}"}} - ) return await self.async_step_user(user_input=config) async def async_step_user(self, user_input=None) -> FlowResult: @@ -71,17 +37,24 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input: api_key = user_input[CONF_API_KEY] + try: + username = await async_validate_api(self.hass, api_key) + except AuthenticationError: + errors["base"] = "invalid_auth" + except ConnectionError: + errors["base"] = "cannot_connect" + except NoDevicesError: + errors["base"] = "no_devices" + except NoUsernameError: + errors["base"] = "no_username" + else: + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured() - await self.async_set_unique_id(api_key) - self._abort_if_unique_id_configured() - - validate = await async_validate_api(self.hass, api_key) - if validate: return self.async_create_entry( title=DEFAULT_NAME, data={CONF_API_KEY: api_key}, ) - errors["base"] = "cannot_connect" return self.async_show_form( step_id="user", diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 22751964999..9b035bc7f05 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -3,8 +3,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, - "error":{ - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "no_devices": "No devices discovered", + "no_username": "Could not get username" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index d3ee9fb1336..102ffa35879 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -4,7 +4,10 @@ "already_configured": "Account is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "no_devices": "No devices discovered", + "no_username": "Could not get username" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py new file mode 100644 index 00000000000..fda9d4a210e --- /dev/null +++ b/homeassistant/components/sensibo/util.py @@ -0,0 +1,49 @@ +"""Utils for Sensibo integration.""" +from __future__ import annotations + +import async_timeout +from pysensibo import SensiboClient +from pysensibo.exceptions import AuthenticationError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import LOGGER, SENSIBO_ERRORS, TIMEOUT + + +async def async_validate_api(hass: HomeAssistant, api_key: str) -> str: + """Get data from API.""" + client = SensiboClient( + api_key, + session=async_get_clientsession(hass), + timeout=TIMEOUT, + ) + + try: + async with async_timeout.timeout(TIMEOUT): + device_query = await client.async_get_devices() + user_query = await client.async_get_me() + except AuthenticationError as err: + LOGGER.error("Could not authenticate on Sensibo servers %s", err) + raise AuthenticationError from err + except SENSIBO_ERRORS as err: + LOGGER.error("Failed to get information from Sensibo servers %s", err) + raise ConnectionError from err + + devices = device_query["result"] + user = user_query["result"].get("username") + if not devices: + LOGGER.error("Could not retrieve any devices from Sensibo servers") + raise NoDevicesError + if not user: + LOGGER.error("Could not retrieve username from Sensibo servers") + raise NoUsernameError + return user + + +class NoDevicesError(Exception): + """No devices from Sensibo api.""" + + +class NoUsernameError(Exception): + """No username from Sensibo api.""" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 9c59fc70763..9cc96c1c04b 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -5,7 +5,7 @@ import asyncio from unittest.mock import patch import aiohttp -from pysensibo import AuthenticationError, SensiboError +from pysensibo.exceptions import AuthenticationError, SensiboError import pytest from homeassistant import config_entries @@ -22,11 +22,6 @@ from tests.common import MockConfigEntry DOMAIN = "sensibo" -def devices(): - """Return list of test devices.""" - return (yield from [{"id": "xyzxyz"}, {"id": "abcabc"}]) - - async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -38,8 +33,11 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", - return_value=devices(), + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, ), patch( "homeassistant.components.sensibo.async_setup_entry", return_value=True, @@ -53,6 +51,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["version"] == 2 assert result2["data"] == { "api_key": "1234567890", } @@ -64,8 +63,11 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: """Test a successful import of yaml.""" with patch( - "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", - return_value=devices(), + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, ), patch( "homeassistant.components.sensibo.async_setup_entry", return_value=True, @@ -95,15 +97,18 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: data={ CONF_API_KEY: "1234567890", }, - unique_id="1234567890", + unique_id="username", ).add_to_hass(hass) with patch( "homeassistant.components.sensibo.async_setup_entry", return_value=True, ), patch( - "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", - return_value=devices(), + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, ): result3 = await hass.config_entries.flow.async_init( DOMAIN, @@ -119,33 +124,112 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "error_message", + "error_message, p_error", [ - (aiohttp.ClientConnectionError), - (asyncio.TimeoutError), - (AuthenticationError), - (SensiboError), + (aiohttp.ClientConnectionError, "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (AuthenticationError, "invalid_auth"), + (SensiboError, "cannot_connect"), ], ) -async def test_flow_fails(hass: HomeAssistant, error_message) -> None: +async def test_flow_fails( + hass: HomeAssistant, error_message: Exception, p_error: str +) -> None: """Test config flow errors.""" - result4 = await hass.config_entries.flow.async_init( + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM - assert result4["step_id"] == config_entries.SOURCE_USER + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER with patch( - "homeassistant.components.sensibo.config_flow.SensiboClient.async_get_devices", + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", side_effect=error_message, ): - result4 = await hass.config_entries.flow.async_configure( - result4["flow_id"], + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={ CONF_API_KEY: "1234567890", }, ) - assert result4["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": p_error} + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567891", + }, + ) + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Sensibo" + assert result3["data"] == { + "api_key": "1234567891", + } + + +async def test_flow_get_no_devices(hass: HomeAssistant) -> None: + """Test config flow get no devices from api.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": []}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {}}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result2["errors"] == {"base": "no_devices"} + + +async def test_flow_get_no_username(hass: HomeAssistant) -> None: + """Test config flow get no username from api.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {}}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + + assert result2["errors"] == {"base": "no_username"} From afaaabd2feeee5e02cb36d8bd9a0aaa9520d27b0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Feb 2022 15:01:54 -0800 Subject: [PATCH 0108/1054] Cleanup google calendar by removing some device abstractions (#67356) * Remove unnecessary abstraction in google calendar * Simplify diffs for calendar event filtering --- homeassistant/components/google/calendar.py | 65 +++++---------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index e068d817429..d49ec48fe82 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -88,12 +88,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice): entity_id: str, ) -> None: """Create the Calendar event device.""" - self.data = GoogleCalendarData( - calendar_service, - calendar_id, - data.get(CONF_SEARCH), - data.get(CONF_IGNORE_AVAILABILITY, False), - ) + self._calendar_service = calendar_service + self._calendar_id = calendar_id + self._search: str | None = data.get(CONF_SEARCH) + self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: dict[str, Any] | None = None self._name: str = data[CONF_NAME] self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) @@ -115,44 +113,9 @@ class GoogleCalendarEventDevice(CalendarEventDevice): """Return the name of the entity.""" return self._name - async def async_get_events( - self, hass: HomeAssistant, start_date: datetime, end_date: datetime - ) -> list[dict[str, Any]]: - """Get all events in a specific time frame.""" - return await self.data.async_get_events(hass, start_date, end_date) - - def update(self) -> None: - """Update event data.""" - self.data.update() - event = copy.deepcopy(self.data.event) - if event is None: - self._event = event - return - event = calculate_offset(event, self._offset) - self._offset_reached = is_offset_reached(event) - self._event = event - - -class GoogleCalendarData: - """Class to utilize calendar service object to get next event.""" - - def __init__( - self, - calendar_service: GoogleCalendarService, - calendar_id: str, - search: str | None, - ignore_availability: bool, - ) -> None: - """Set up how we are going to search the google calendar.""" - self.calendar_service = calendar_service - self.calendar_id = calendar_id - self.search = search - self.ignore_availability = ignore_availability - self.event: dict[str, Any] | None = None - def _event_filter(self, event: dict[str, Any]) -> bool: """Return True if the event is visible.""" - if self.ignore_availability: + if self._ignore_availability: return True return event.get(TRANSPARENCY, OPAQUE) == OPAQUE @@ -164,11 +127,11 @@ class GoogleCalendarData: page_token: str | None = None while True: try: - items, page_token = await self.calendar_service.async_list_events( - self.calendar_id, + items, page_token = await self._calendar_service.async_list_events( + self._calendar_id, start_time=start_date, end_time=end_date, - search=self.search, + search=self._search, page_token=page_token, ) except ServerNotFoundError as err: @@ -184,12 +147,16 @@ class GoogleCalendarData: def update(self) -> None: """Get the latest data.""" try: - items, _ = self.calendar_service.list_events( - self.calendar_id, search=self.search + items, _ = self._calendar_service.list_events( + self._calendar_id, search=self._search ) except ServerNotFoundError as err: _LOGGER.error("Unable to connect to Google: %s", err) return - valid_events = filter(self._event_filter, items) - self.event = next(valid_events, None) + # Pick the first visible evemt. Make a copy since calculate_offset mutates the event + valid_items = filter(self._event_filter, items) + self._event = copy.deepcopy(next(valid_items, None)) + if self._event: + calculate_offset(self._event, self._offset) + self._offset_reached = is_offset_reached(self._event) From 1b97f48c1f1d933be9461e1f7419a24ba4803ef2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 28 Feb 2022 00:19:57 +0000 Subject: [PATCH 0109/1054] [ci skip] Translation update --- .../binary_sensor/translations/tr.json | 4 ++++ .../components/dlna_dms/translations/tr.json | 24 +++++++++++++++++++ .../components/fritz/translations/tr.json | 3 ++- .../components/nanoleaf/translations/tr.json | 8 +++++++ .../radio_browser/translations/tr.json | 12 ++++++++++ .../components/rfxtrx/translations/de.json | 1 + .../components/rfxtrx/translations/id.json | 1 + .../components/rfxtrx/translations/tr.json | 1 + .../components/sense/translations/tr.json | 16 ++++++++++++- .../components/sonarr/translations/tr.json | 3 ++- .../components/zha/translations/pt-BR.json | 8 +++---- 11 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/dlna_dms/translations/tr.json create mode 100644 homeassistant/components/radio_browser/translations/tr.json diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 40dc3bdcb7d..2b7eb33e6f0 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -134,6 +134,10 @@ "off": "\u015earj olmuyor", "on": "\u015earj Oluyor" }, + "carbon_monoxide": { + "off": "Temiz", + "on": "Alg\u0131land\u0131" + }, "co": { "off": "Temiz", "on": "Alg\u0131land\u0131" diff --git a/homeassistant/components/dlna_dms/translations/tr.json b/homeassistant/components/dlna_dms/translations/tr.json new file mode 100644 index 00000000000..dc8a5bf02e8 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "bad_ssdp": "SSDP verilerinde gerekli bir de\u011fer eksik", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_dms": "Cihaz desteklenen bir Medya Sunucusu de\u011fil" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "user": { + "data": { + "host": "Sunucu" + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in", + "title": "Ke\u015ffedilen DLNA DMA cihazlar\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index 686c248cd39..29a200bea69 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Bir cihaz\u0131 'evde' varsaymak i\u00e7in saniye" + "consider_home": "Bir cihaz\u0131 'evde' varsaymak i\u00e7in saniye", + "old_discovery": "Eski ke\u015fif y\u00f6ntemini etkinle\u015ftir" } } } diff --git a/homeassistant/components/nanoleaf/translations/tr.json b/homeassistant/components/nanoleaf/translations/tr.json index d3c3969dc48..847b95a137c 100644 --- a/homeassistant/components/nanoleaf/translations/tr.json +++ b/homeassistant/components/nanoleaf/translations/tr.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "A\u015fa\u011f\u0131 Kayd\u0131r", + "swipe_left": "Sola Kayd\u0131r", + "swipe_right": "Sa\u011fa Kayd\u0131r", + "swipe_up": "Yukar\u0131 Kayd\u0131r" + } } } \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/tr.json b/homeassistant/components/radio_browser/translations/tr.json new file mode 100644 index 00000000000..41b37f2b0c2 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Home Assistant'a Radyo Taray\u0131c\u0131 eklemek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index ee65e371330..a80bde85b0b 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -61,6 +61,7 @@ "debug": "Debugging aktivieren", "device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen", "event_code": "Ereigniscode zum Hinzuf\u00fcgen eingeben", + "protocols": "Protokolle", "remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen" }, "title": "Rfxtrx Optionen" diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json index e04b0685050..cea00e08f9c 100644 --- a/homeassistant/components/rfxtrx/translations/id.json +++ b/homeassistant/components/rfxtrx/translations/id.json @@ -61,6 +61,7 @@ "debug": "Aktifkan debugging", "device": "Pilih perangkat untuk dikonfigurasi", "event_code": "Masukkan kode event untuk ditambahkan", + "protocols": "Protokol", "remove_device": "Pilih perangkat yang akan dihapus" }, "title": "Opsi Rfxtrx" diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json index 2d720196874..fd29e034e31 100644 --- a/homeassistant/components/rfxtrx/translations/tr.json +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -66,6 +66,7 @@ "debug": "Hata ay\u0131klamay\u0131 etkinle\u015ftir", "device": "Yap\u0131land\u0131rmak i\u00e7in cihaz\u0131 se\u00e7in", "event_code": "Eklemek i\u00e7in etkinlik kodunu girin", + "protocols": "Protokoller", "remove_device": "Silinecek cihaz\u0131 se\u00e7in" }, "title": "Rfxtrx Se\u00e7enekleri" diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json index 3261bbec3b9..ea0f653c20a 100644 --- a/homeassistant/components/sense/translations/tr.json +++ b/homeassistant/components/sense/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_validate": { + "data": { + "password": "Parola" + }, + "description": "Sense entegrasyonunun hesab\u0131n\u0131z\u0131 {email} yeniden do\u011frulamas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "email": "E-posta", @@ -16,6 +24,12 @@ "timeout": "Zaman a\u015f\u0131m\u0131" }, "title": "Sense Enerji Monit\u00f6r\u00fcn\u00fcze ba\u011flan\u0131n" + }, + "validation": { + "data": { + "code": "Do\u011frulama kodu" + }, + "title": "Sense \u00c7ok fakt\u00f6rl\u00fc kimlik do\u011frulama" } } } diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json index d1e961cb2b9..c064e4947c3 100644 --- a/homeassistant/components/sonarr/translations/tr.json +++ b/homeassistant/components/sonarr/translations/tr.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr entegrasyonunun, \u015fu adreste bar\u0131nd\u0131r\u0131lan Sonarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekir: {host}", + "description": "Sonarr entegrasyonunun \u015fu adreste bar\u0131nd\u0131r\u0131lan Sonarr API ile manuel olarak yeniden do\u011frulanmas\u0131 gerekiyor: {url}", "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { @@ -22,6 +22,7 @@ "host": "Ana Bilgisayar", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", + "url": "URL", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 2e5aec0d1cc..8c43bf7f3e6 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -15,10 +15,10 @@ }, "pick_radio": { "data": { - "radio_type": "Tipo de r\u00e1dio" + "radio_type": "Tipo de hub zigbee" }, - "description": "Escolha o tipo de seu r\u00e1dio Zigbee", - "title": "Tipo de r\u00e1dio" + "description": "Escolha o tipo de seu hub Zigbee", + "title": "Tipo de hub zigbee" }, "port_config": { "data": { @@ -33,7 +33,7 @@ "data": { "path": "Caminho do dispositivo serial" }, - "description": "Selecione a porta serial para o r\u00e1dio Zigbee", + "description": "Selecione a porta serial para o hub Zigbee", "title": "ZHA" } } From 1cca3172942e01adaa7feb6cfbb67644affe74f2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Feb 2022 02:29:31 +0100 Subject: [PATCH 0110/1054] Bump samsungtvws to 2.0.0 (#67351) --- homeassistant/components/samsungtv/bridge.py | 4 ++-- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index ddb3c0a4e9b..b79ea6af871 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -355,7 +355,7 @@ class SamsungTVWSBridge(SamsungTVBridge): timeout=config[CONF_TIMEOUT], name=config[CONF_NAME], ) as remote: - remote.open("samsung.remote.control") + remote.open() self.token = remote.token LOGGER.debug("Working config: %s", config) return RESULT_SUCCESS @@ -432,7 +432,7 @@ class SamsungTVWSBridge(SamsungTVBridge): name=VALUE_CONF_NAME, ) if not avoid_open: - self._remote.open("samsung.remote.control") + self._remote.open() # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure as err: diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 21e23c74eb1..dd014fab97d 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws==1.7.0", + "samsungtvws[async]==2.0.0", "wakeonlan==2.0.1" ], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index b57fbacfe6d..12fc9db4534 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2099,7 +2099,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.7.0 +samsungtvws[async]==2.0.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cea283bb3e6..9e481a7fe9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws==1.7.0 +samsungtvws[async]==2.0.0 # homeassistant.components.dhcp scapy==2.4.5 From bafa99fe3e4ab1257491928f55db2a00ca05177c Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sun, 27 Feb 2022 20:47:31 -0500 Subject: [PATCH 0111/1054] Add reauth to SleepIQ (#67321) Co-authored-by: J. Nick Koston --- homeassistant/components/sleepiq/__init__.py | 6 +-- .../components/sleepiq/config_flow.py | 44 ++++++++++++++++++- homeassistant/components/sleepiq/strings.json | 10 ++++- .../components/sleepiq/translations/en.json | 10 ++++- tests/components/sleepiq/conftest.py | 18 +++++--- tests/components/sleepiq/test_config_flow.py | 38 ++++++++++++++-- 6 files changed, 110 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index bac88880cdb..a32e61b972c 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -63,9 +63,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await gateway.login(email, password) - except SleepIQLoginException: + except SleepIQLoginException as err: _LOGGER.error("Could not authenticate with SleepIQ server") - return False + raise ConfigEntryAuthFailed(err) from err except SleepIQTimeoutException as err: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index 47e08fdfd5b..49f14eff0b9 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -7,7 +7,7 @@ from typing import Any from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -18,11 +18,15 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a SleepIQ config flow.""" VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._reauth_entry: ConfigEntry | None = None + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a SleepIQ account as a config entry. @@ -75,6 +79,42 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): last_step=True, ) + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm(user_input) + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth.""" + errors: dict[str, str] = {} + assert self._reauth_entry is not None + if user_input is not None: + data = { + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + + if not (error := await try_connection(self.hass, data)): + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=data + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + errors["base"] = error + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], + }, + ) + async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str | None: """Test if the given credentials can successfully login to SleepIQ.""" diff --git a/homeassistant/components/sleepiq/strings.json b/homeassistant/components/sleepiq/strings.json index 21ceead3d0a..223b8c11c4f 100644 --- a/homeassistant/components/sleepiq/strings.json +++ b/homeassistant/components/sleepiq/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -13,6 +14,13 @@ "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The SleepIQ integration needs to re-authenticate your account {username}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } } } diff --git a/homeassistant/components/sleepiq/translations/en.json b/homeassistant/components/sleepiq/translations/en.json index 31de29c8690..fcefeace3c8 100644 --- a/homeassistant/components/sleepiq/translations/en.json +++ b/homeassistant/components/sleepiq/translations/en.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The SleepIQ integration needs to re-authenticate your account {username}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 3669fd5a7fc..9ecc1edd0b6 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,4 +1,6 @@ """Common methods for SleepIQ.""" +from __future__ import annotations + from unittest.mock import create_autospec, patch from asyncsleepiq import SleepIQBed, SleepIQSleeper @@ -20,6 +22,12 @@ SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") +SLEEPIQ_CONFIG = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", +} + + @pytest.fixture def mock_asyncsleepiq(): """Mock an AsyncSleepIQ object.""" @@ -49,14 +57,14 @@ def mock_asyncsleepiq(): yield client -async def setup_platform(hass: HomeAssistant, platform) -> MockConfigEntry: +async def setup_platform( + hass: HomeAssistant, platform: str | None = None +) -> MockConfigEntry: """Set up the SleepIQ platform.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - }, + data=SLEEPIQ_CONFIG, + unique_id=SLEEPIQ_CONFIG[CONF_USERNAME].lower(), ) mock_entry.add_to_hass(hass) diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 516a783f302..bb6742821f6 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -9,10 +9,7 @@ from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -SLEEPIQ_CONFIG = { - CONF_USERNAME: "username", - CONF_PASSWORD: "password", -} +from tests.components.sleepiq.conftest import SLEEPIQ_CONFIG, setup_platform async def test_import(hass: HomeAssistant) -> None: @@ -97,3 +94,36 @@ async def test_success(hass: HomeAssistant) -> None: assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_password(hass): + """Test reauth form.""" + + # set up initially + entry = await setup_platform(hass) + with patch( + "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", + side_effect=SleepIQLoginException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" From a375a4e16e071831624697f1a8fad10fbb49f6e3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Feb 2022 19:56:24 -0800 Subject: [PATCH 0112/1054] Fix recurring events in google calendar (#67355) --- homeassistant/components/google/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 8652f8b15ed..1de6eb4e9aa 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -82,5 +82,7 @@ class GoogleCalendarService: q=search, maxResults=EVENT_PAGE_SIZE, pageToken=page_token, + singleEvents=True, # Flattens recurring events + orderBy="startTime", ).execute() return (result["items"], result.get("nextPageToken")) From 5922a98936067601222b91fe4bf6ebff7ff08725 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Feb 2022 00:50:42 -0600 Subject: [PATCH 0113/1054] Bump plexapi to 4.10.0 (#67364) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 238c25ad917..85a060ae7cd 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.9.2", + "plexapi==4.10.0", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index 12fc9db4534..8d4ff0028e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1214,7 +1214,7 @@ pillow==9.0.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.9.2 +plexapi==4.10.0 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e481a7fe9e..1fe926349a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ pilight==0.1.1 pillow==9.0.1 # homeassistant.components.plex -plexapi==4.9.2 +plexapi==4.10.0 # homeassistant.components.plex plexauth==0.0.6 From 0d8ff3d724eb49f6645c5ea7668f0f4452e23287 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 28 Feb 2022 09:06:16 +0100 Subject: [PATCH 0114/1054] Add codeowner to internal integrations that are without (#67286) --- CODEOWNERS | 50 +++++++++++++++++++ .../components/air_quality/manifest.json | 2 +- .../alarm_control_panel/manifest.json | 2 +- homeassistant/components/alert/manifest.json | 2 +- .../components/binary_sensor/manifest.json | 2 +- .../components/calendar/manifest.json | 2 +- homeassistant/components/camera/manifest.json | 2 +- .../components/climate/manifest.json | 2 +- .../components/default_config/manifest.json | 4 +- .../components/device_tracker/manifest.json | 2 +- .../components/discovery/manifest.json | 2 +- homeassistant/components/fan/manifest.json | 2 +- .../components/geo_location/manifest.json | 2 +- .../components/image_processing/manifest.json | 2 +- homeassistant/components/light/manifest.json | 2 +- homeassistant/components/lock/manifest.json | 2 +- .../components/logbook/manifest.json | 2 +- .../components/media_player/manifest.json | 2 +- .../components/network/manifest.json | 2 +- homeassistant/components/remote/manifest.json | 2 +- .../rss_feed_template/manifest.json | 2 +- homeassistant/components/sensor/manifest.json | 2 +- homeassistant/components/switch/manifest.json | 2 +- homeassistant/components/vacuum/manifest.json | 2 +- .../components/water_heater/manifest.json | 2 +- .../components/webhook/manifest.json | 2 +- 26 files changed, 76 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 25b44d84267..20ecc0272cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -33,6 +33,8 @@ homeassistant/components/advantage_air/* @Bre77 tests/components/advantage_air/* @Bre77 homeassistant/components/agent_dvr/* @ispysoftware tests/components/agent_dvr/* @ispysoftware +homeassistant/components/air_quality/* @home-assistant/core +tests/components/air_quality/* @home-assistant/core homeassistant/components/airly/* @bieniu tests/components/airly/* @bieniu homeassistant/components/airnow/* @asymworks @@ -43,6 +45,10 @@ homeassistant/components/airtouch4/* @LonePurpleWolf tests/components/airtouch4/* @LonePurpleWolf homeassistant/components/airvisual/* @bachya tests/components/airvisual/* @bachya +homeassistant/components/alarm_control_panel/* @home-assistant/core +tests/components/alarm_control_panel/* @home-assistant/core +homeassistant/components/alert/* @home-assistant/core +tests/components/alert/* @home-assistant/core homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy tests/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob @@ -108,6 +114,8 @@ homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/balboa/* @garbled1 tests/components/balboa/* @garbled1 homeassistant/components/beewi_smartclim/* @alemuro +homeassistant/components/binary_sensor/* @home-assistant/core +tests/components/binary_sensor/* @home-assistant/core homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blebox/* @bbx-a @bbx-jp @@ -138,6 +146,10 @@ homeassistant/components/buienradar/* @mjj4791 @ties @Robbie1221 tests/components/buienradar/* @mjj4791 @ties @Robbie1221 homeassistant/components/button/* @home-assistant/core tests/components/button/* @home-assistant/core +homeassistant/components/calendar/* @home-assistant/core +tests/components/calendar/* @home-assistant/core +homeassistant/components/camera/* @home-assistant/core +tests/components/camera/* @home-assistant/core homeassistant/components/cast/* @emontnemery tests/components/cast/* @emontnemery homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren @@ -148,6 +160,8 @@ homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/climacell/* @raman325 tests/components/climacell/* @raman325 +homeassistant/components/climate/* @home-assistant/core +tests/components/climate/* @home-assistant/core homeassistant/components/cloud/* @home-assistant/cloud tests/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus @ctalkington @@ -189,6 +203,8 @@ homeassistant/components/debugpy/* @frenck tests/components/debugpy/* @frenck homeassistant/components/deconz/* @Kane610 tests/components/deconz/* @Kane610 +homeassistant/components/default_config/* @home-assistant/core +tests/components/default_config/* @home-assistant/core homeassistant/components/delijn/* @bollewolle @Emilv2 homeassistant/components/demo/* @home-assistant/core tests/components/demo/* @home-assistant/core @@ -198,6 +214,8 @@ homeassistant/components/derivative/* @afaucogney tests/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core tests/components/device_automation/* @home-assistant/core +homeassistant/components/device_tracker/* @home-assistant/core +tests/components/device_tracker/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun tests/components/devolo_home_control/* @2Fake @Shutgun homeassistant/components/devolo_home_network/* @2Fake @Shutgun @@ -210,6 +228,8 @@ homeassistant/components/diagnostics/* @home-assistant/core tests/components/diagnostics/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek +homeassistant/components/discovery/* @home-assistant/core +tests/components/discovery/* @home-assistant/core homeassistant/components/dlna_dmr/* @StevenLooman @chishm tests/components/dlna_dmr/* @StevenLooman @chishm homeassistant/components/dlna_dms/* @chishm @@ -277,6 +297,8 @@ homeassistant/components/ezviz/* @RenierM26 @baqs tests/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 tests/components/faa_delays/* @ntilley905 +homeassistant/components/fan/* @home-assistant/core +tests/components/fan/* @home-assistant/core homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff tests/components/file/* @fabaff @@ -332,6 +354,8 @@ tests/components/generic_hygrostat/* @Shulyaka homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_json_events/* @exxamalte tests/components/geo_json_events/* @exxamalte +homeassistant/components/geo_location/* @home-assistant/core +tests/components/geo_location/* @home-assistant/core homeassistant/components/geo_rss_events/* @exxamalte tests/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte @@ -434,6 +458,8 @@ homeassistant/components/ign_sismologia/* @exxamalte tests/components/ign_sismologia/* @exxamalte homeassistant/components/image/* @home-assistant/core tests/components/image/* @home-assistant/core +homeassistant/components/image_processing/* @home-assistant/core +tests/components/image_processing/* @home-assistant/core homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff @mdegat01 tests/components/influxdb/* @fabaff @mdegat01 @@ -511,6 +537,8 @@ homeassistant/components/lcn/* @alengwenus tests/components/lcn/* @alengwenus homeassistant/components/lg_netcast/* @Drafteed homeassistant/components/life360/* @pnbruckner +homeassistant/components/light/* @home-assistant/core +tests/components/light/* @home-assistant/core homeassistant/components/linux_battery/* @fabaff homeassistant/components/litejet/* @joncar tests/components/litejet/* @joncar @@ -518,6 +546,10 @@ homeassistant/components/litterrobot/* @natekspencer tests/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg tests/components/local_ip/* @issacg +homeassistant/components/lock/* @home-assistant/core +tests/components/lock/* @home-assistant/core +homeassistant/components/logbook/* @home-assistant/core +tests/components/logbook/* @home-assistant/core homeassistant/components/logger/* @home-assistant/core tests/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd @@ -539,6 +571,8 @@ homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mazda/* @bdr99 tests/components/mazda/* @bdr99 +homeassistant/components/media_player/* @home-assistant/core +tests/components/media_player/* @home-assistant/core homeassistant/components/media_source/* @hunterjm tests/components/media_source/* @hunterjm homeassistant/components/mediaroom/* @dgomes @@ -617,6 +651,8 @@ tests/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/netgear/* @hacf-fr @Quentame @starkillerOG tests/components/netgear/* @hacf-fr @Quentame @starkillerOG +homeassistant/components/network/* @home-assistant/core +tests/components/network/* @home-assistant/core homeassistant/components/nexia/* @bdraco tests/components/nexia/* @bdraco homeassistant/components/nextbus/* @vividboarder @@ -772,6 +808,8 @@ tests/components/recollect_waste/* @bachya homeassistant/components/recorder/* @home-assistant/core tests/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox +homeassistant/components/remote/* @home-assistant/core +tests/components/remote/* @home-assistant/core homeassistant/components/renault/* @epenet tests/components/renault/* @epenet homeassistant/components/repetier/* @MTrab @ShadowBr0ther @@ -797,6 +835,8 @@ homeassistant/components/roon/* @pavoni tests/components/roon/* @pavoni homeassistant/components/rpi_power/* @shenxn @swetoast tests/components/rpi_power/* @shenxn @swetoast +homeassistant/components/rss_feed_template/* @home-assistant/core +tests/components/rss_feed_template/* @home-assistant/core homeassistant/components/rtsp_to_webrtc/* @allenporter tests/components/rtsp_to_webrtc/* @allenporter homeassistant/components/ruckus_unleashed/* @gabe565 @@ -825,6 +865,8 @@ homeassistant/components/senseme/* @mikelawrence @bdraco tests/components/senseme/* @mikelawrence @bdraco homeassistant/components/sensibo/* @andrey-git @gjohansson-ST tests/components/sensibo/* @andrey-git @gjohansson-ST +homeassistant/components/sensor/* @home-assistant/core +tests/components/sensor/* @home-assistant/core homeassistant/components/sentry/* @dcramer @frenck tests/components/sentry/* @dcramer @frenck homeassistant/components/serial/* @fabaff @@ -928,6 +970,8 @@ homeassistant/components/surepetcare/* @benleb @danielhiversen tests/components/surepetcare/* @benleb @danielhiversen homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff +homeassistant/components/switch/* @home-assistant/core +tests/components/switch/* @home-assistant/core homeassistant/components/switchbot/* @danielhiversen @RenierM26 tests/components/switchbot/* @danielhiversen @RenierM26 homeassistant/components/switcher_kis/* @tomerfi @thecode @@ -1019,6 +1063,8 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte tests/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes tests/components/utility_meter/* @dgomes +homeassistant/components/vacuum/* @home-assistant/core +tests/components/vacuum/* @home-assistant/core homeassistant/components/vallox/* @andre-richter @slovdahl @viiru- tests/components/vallox/* @andre-richter @slovdahl @viiru- homeassistant/components/velbus/* @Cereal2nd @brefra @@ -1053,6 +1099,8 @@ tests/components/wake_on_lan/* @ntilley905 homeassistant/components/wallbox/* @hesselonline tests/components/wallbox/* @hesselonline homeassistant/components/waqi/* @andrey-git +homeassistant/components/water_heater/* @home-assistant/core +tests/components/water_heater/* @home-assistant/core homeassistant/components/watson_tts/* @rutkai homeassistant/components/watttime/* @bachya tests/components/watttime/* @bachya @@ -1060,6 +1108,8 @@ homeassistant/components/waze_travel_time/* @eifinger tests/components/waze_travel_time/* @eifinger homeassistant/components/weather/* @fabaff tests/components/weather/* @fabaff +homeassistant/components/webhook/* @home-assistant/core +tests/components/webhook/* @home-assistant/core homeassistant/components/webostv/* @bendavid @thecode tests/components/webostv/* @bendavid @thecode homeassistant/components/websocket_api/* @home-assistant/core diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 55fbdbdafd1..978089d1816 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -2,6 +2,6 @@ "domain": "air_quality", "name": "Air Quality", "documentation": "https://www.home-assistant.io/integrations/air_quality", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index e4cd0e27a39..461094e8ce6 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -2,6 +2,6 @@ "domain": "alarm_control_panel", "name": "Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index f5d3e08f2fe..bf9724ec2b9 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -3,7 +3,7 @@ "name": "Alert", "documentation": "https://www.home-assistant.io/integrations/alert", "after_dependencies": ["notify"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" } diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index be2feb9d207..d1c631ee94b 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -2,6 +2,6 @@ "domain": "binary_sensor", "name": "Binary Sensor", "documentation": "https://www.home-assistant.io/integrations/binary_sensor", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 2455744ee4e..2fb4df84414 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -3,6 +3,6 @@ "name": "Calendar", "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index aaa60b61a47..7fad2044b7c 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -5,6 +5,6 @@ "dependencies": ["http"], "requirements": ["PyTurboJPEG==1.6.5"], "after_dependencies": ["media_player"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index 5d950ccbe2d..8b54d3a91ad 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -2,6 +2,6 @@ "domain": "climate", "name": "Climate", "documentation": "https://www.home-assistant.io/integrations/climate", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 9a65af96852..1ab827529c6 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -35,6 +35,6 @@ "zeroconf", "zone" ], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 6e29d977f66..7abd68b03e2 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/device_tracker", "dependencies": ["zone"], "after_dependencies": [], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 3e7d31fcb1c..6f97993c788 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/discovery", "requirements": ["netdisco==3.0.0"], "after_dependencies": ["zeroconf"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "loggers": ["netdisco"] } diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 76573e08cbb..bb968240f0b 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -2,6 +2,6 @@ "domain": "fan", "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index c222df8b2aa..2e0d7061099 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -2,6 +2,6 @@ "domain": "geo_location", "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 0541f4898c9..0315a69b82a 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,6 +3,6 @@ "name": "Image Processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index 27c504f6b91..c7cf2abc7c8 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -2,6 +2,6 @@ "domain": "light", "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index b44a66613b0..f93d2962ea3 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -2,6 +2,6 @@ "domain": "lock", "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 58bc71959b3..66c0348a2ac 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,6 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index 7a8e47adf20..118d05036cc 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -3,6 +3,6 @@ "name": "Media Player", "documentation": "https://www.home-assistant.io/integrations/media_player", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json index 84e86014036..9f2fa7849f0 100644 --- a/homeassistant/components/network/manifest.json +++ b/homeassistant/components/network/manifest.json @@ -3,7 +3,7 @@ "name": "Network Configuration", "documentation": "https://www.home-assistant.io/integrations/network", "requirements": ["ifaddr==0.1.7"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "dependencies": ["websocket_api"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index e2caf2d5606..d08cb624c16 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -2,6 +2,6 @@ "domain": "remote", "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index 46b449b03dd..ad0ab14cadd 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -3,7 +3,7 @@ "name": "RSS Feed Template", "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" } diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 163bc895975..4726ac790a7 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -2,7 +2,7 @@ "domain": "sensor", "name": "Sensor", "documentation": "https://www.home-assistant.io/integrations/sensor", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "after_dependencies": ["recorder"] } diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 6f0113d1b9c..4c52e596648 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -2,6 +2,6 @@ "domain": "switch", "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 2a874b36a1c..ee4fa6a471e 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -2,6 +2,6 @@ "domain": "vacuum", "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index ab12a8ab820..ac00bc64210 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -2,6 +2,6 @@ "domain": "water_heater", "name": "Water Heater", "documentation": "https://www.home-assistant.io/integrations/water_heater", - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index 509563bb4b0..4ca247ed720 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -3,6 +3,6 @@ "name": "Webhook", "documentation": "https://www.home-assistant.io/integrations/webhook", "dependencies": ["http"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } From 277c6f8803209f93a0b5dd737467f08172b069a8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Feb 2022 12:27:24 +0000 Subject: [PATCH 0115/1054] Bump pychromecast to 10.3.0 (#67370) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 2316a884b73..85457307c94 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==10.2.3"], + "requirements": ["pychromecast==10.3.0"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 8d4ff0028e4..0ea23f68c23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1386,7 +1386,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==10.2.3 +pychromecast==10.3.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fe926349a1..519e803f144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -902,7 +902,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==10.2.3 +pychromecast==10.3.0 # homeassistant.components.climacell pyclimacell==0.18.2 From 0db6a0b248c7edebeebfc4245a2d3fbcad906a33 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Feb 2022 13:29:13 +0100 Subject: [PATCH 0116/1054] Remove deprecated Bosch BME680 Environmental Sensor integration (#67273) --- .coveragerc | 1 - .github/workflows/wheels.yml | 1 - homeassistant/components/bme680/__init__.py | 1 - homeassistant/components/bme680/manifest.json | 9 - homeassistant/components/bme680/sensor.py | 391 ------------------ requirements_all.txt | 4 - script/gen_requirements_all.py | 1 - 7 files changed, 408 deletions(-) delete mode 100644 homeassistant/components/bme680/__init__.py delete mode 100644 homeassistant/components/bme680/manifest.json delete mode 100644 homeassistant/components/bme680/sensor.py diff --git a/.coveragerc b/.coveragerc index 0a1c0b3d2b7..905be37e26a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -116,7 +116,6 @@ omit = homeassistant/components/bloomsky/* homeassistant/components/bluesound/* homeassistant/components/bluetooth_tracker/* - homeassistant/components/bme680/sensor.py homeassistant/components/bmw_connected_drive/__init__.py homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/button.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b7a9f3b4c20..740eafe95ed 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -149,7 +149,6 @@ jobs: sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} - sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" ${requirement_file} done diff --git a/homeassistant/components/bme680/__init__.py b/homeassistant/components/bme680/__init__.py deleted file mode 100644 index dc88286a603..00000000000 --- a/homeassistant/components/bme680/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The bme680 component.""" diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json deleted file mode 100644 index c4db1d640de..00000000000 --- a/homeassistant/components/bme680/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "bme680", - "name": "Bosch BME680 Environmental Sensor", - "documentation": "https://www.home-assistant.io/integrations/bme680", - "requirements": ["bme680==1.0.5", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["bme680", "smbus"] -} diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py deleted file mode 100644 index 8ea3bf32334..00000000000 --- a/homeassistant/components/bme680/sensor.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Support for BME680 Sensor over SMBus.""" -from __future__ import annotations - -import logging -import threading -from time import monotonic, sleep - -import bme680 # pylint: disable=import-error -from smbus import SMBus -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - CONF_NAME, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_OVERSAMPLING_TEMP = "oversampling_temperature" -CONF_OVERSAMPLING_PRES = "oversampling_pressure" -CONF_OVERSAMPLING_HUM = "oversampling_humidity" -CONF_FILTER_SIZE = "filter_size" -CONF_GAS_HEATER_TEMP = "gas_heater_temperature" -CONF_GAS_HEATER_DURATION = "gas_heater_duration" -CONF_AQ_BURN_IN_TIME = "aq_burn_in_time" -CONF_AQ_HUM_BASELINE = "aq_humidity_baseline" -CONF_AQ_HUM_WEIGHTING = "aq_humidity_bias" -CONF_TEMP_OFFSET = "temp_offset" - - -DEFAULT_NAME = "BME680 Sensor" -DEFAULT_I2C_ADDRESS = 0x77 -DEFAULT_I2C_BUS = 1 -DEFAULT_OVERSAMPLING_TEMP = 8 # Temperature oversampling x 8 -DEFAULT_OVERSAMPLING_PRES = 4 # Pressure oversampling x 4 -DEFAULT_OVERSAMPLING_HUM = 2 # Humidity oversampling x 2 -DEFAULT_FILTER_SIZE = 3 # IIR Filter Size -DEFAULT_GAS_HEATER_TEMP = 320 # Temperature in celsius 200 - 400 -DEFAULT_GAS_HEATER_DURATION = 150 # Heater duration in ms 1 - 4032 -DEFAULT_AQ_BURN_IN_TIME = 300 # 300 second burn in time for AQ gas measurement -DEFAULT_AQ_HUM_BASELINE = 40 # 40%, an optimal indoor humidity. -DEFAULT_AQ_HUM_WEIGHTING = 25 # 25% Weighting of humidity to gas in AQ score -DEFAULT_TEMP_OFFSET = 0 # No calibration out of the box. - -SENSOR_TEMP = "temperature" -SENSOR_HUMID = "humidity" -SENSOR_PRESS = "pressure" -SENSOR_GAS = "gas" -SENSOR_AQ = "airquality" -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TEMP, - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key=SENSOR_HUMID, - name="Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - ), - SensorEntityDescription( - key=SENSOR_PRESS, - name="Pressure", - native_unit_of_measurement="mb", - device_class=SensorDeviceClass.PRESSURE, - ), - SensorEntityDescription( - key=SENSOR_GAS, - name="Gas Resistance", - native_unit_of_measurement="Ohms", - ), - SensorEntityDescription( - key=SENSOR_AQ, - name="Air Quality", - native_unit_of_measurement=PERCENTAGE, - ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS, SENSOR_AQ] -OVERSAMPLING_VALUES = {0, 1, 2, 4, 8, 16} -FILTER_VALUES = {0, 1, 3, 7, 15, 31, 63, 127} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): cv.positive_int, - vol.Optional(CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - vol.Optional( - CONF_OVERSAMPLING_TEMP, default=DEFAULT_OVERSAMPLING_TEMP - ): vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional( - CONF_OVERSAMPLING_PRES, default=DEFAULT_OVERSAMPLING_PRES - ): vol.All(vol.Coerce(int), vol.In(OVERSAMPLING_VALUES)), - vol.Optional(CONF_OVERSAMPLING_HUM, default=DEFAULT_OVERSAMPLING_HUM): vol.All( - vol.Coerce(int), vol.In(OVERSAMPLING_VALUES) - ), - vol.Optional(CONF_FILTER_SIZE, default=DEFAULT_FILTER_SIZE): vol.All( - vol.Coerce(int), vol.In(FILTER_VALUES) - ), - vol.Optional(CONF_GAS_HEATER_TEMP, default=DEFAULT_GAS_HEATER_TEMP): vol.All( - vol.Coerce(int), vol.Range(200, 400) - ), - vol.Optional( - CONF_GAS_HEATER_DURATION, default=DEFAULT_GAS_HEATER_DURATION - ): vol.All(vol.Coerce(int), vol.Range(1, 4032)), - vol.Optional( - CONF_AQ_BURN_IN_TIME, default=DEFAULT_AQ_BURN_IN_TIME - ): cv.positive_int, - vol.Optional(CONF_AQ_HUM_BASELINE, default=DEFAULT_AQ_HUM_BASELINE): vol.All( - vol.Coerce(int), vol.Range(1, 100) - ), - vol.Optional(CONF_AQ_HUM_WEIGHTING, default=DEFAULT_AQ_HUM_WEIGHTING): vol.All( - vol.Coerce(int), vol.Range(1, 100) - ), - vol.Optional(CONF_TEMP_OFFSET, default=DEFAULT_TEMP_OFFSET): vol.All( - vol.Coerce(float), vol.Range(-100.0, 100.0) - ), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the BME680 sensor.""" - _LOGGER.warning( - "The Bosch BME680 Environmental Sensor integration is deprecated and " - "will be removed in Home Assistant Core 2022.4; " - "this integration is removed under Architectural Decision Record 0019, " - "more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - name = config[CONF_NAME] - - sensor_handler = await hass.async_add_executor_job(_setup_bme680, config) - if sensor_handler is None: - return - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - BME680Sensor(sensor_handler, name, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - async_add_entities(entities) - - -def _setup_bme680(config): - """Set up and configure the BME680 sensor.""" - - sensor_handler = None - sensor = None - try: - i2c_address = config[CONF_I2C_ADDRESS] - bus = SMBus(config[CONF_I2C_BUS]) - sensor = bme680.BME680(i2c_address, bus) - - # Configure Oversampling - os_lookup = { - 0: bme680.OS_NONE, - 1: bme680.OS_1X, - 2: bme680.OS_2X, - 4: bme680.OS_4X, - 8: bme680.OS_8X, - 16: bme680.OS_16X, - } - sensor.set_temperature_oversample(os_lookup[config[CONF_OVERSAMPLING_TEMP]]) - sensor.set_temp_offset(config[CONF_TEMP_OFFSET]) - sensor.set_humidity_oversample(os_lookup[config[CONF_OVERSAMPLING_HUM]]) - sensor.set_pressure_oversample(os_lookup[config[CONF_OVERSAMPLING_PRES]]) - - # Configure IIR Filter - filter_lookup = { - 0: bme680.FILTER_SIZE_0, - 1: bme680.FILTER_SIZE_1, - 3: bme680.FILTER_SIZE_3, - 7: bme680.FILTER_SIZE_7, - 15: bme680.FILTER_SIZE_15, - 31: bme680.FILTER_SIZE_31, - 63: bme680.FILTER_SIZE_63, - 127: bme680.FILTER_SIZE_127, - } - sensor.set_filter(filter_lookup[config[CONF_FILTER_SIZE]]) - - # Configure the Gas Heater - if ( - SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] - or SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] - ): - sensor.set_gas_status(bme680.ENABLE_GAS_MEAS) - sensor.set_gas_heater_duration(config[CONF_GAS_HEATER_DURATION]) - sensor.set_gas_heater_temperature(config[CONF_GAS_HEATER_TEMP]) - sensor.select_gas_heater_profile(0) - else: - sensor.set_gas_status(bme680.DISABLE_GAS_MEAS) - except (RuntimeError, OSError): - _LOGGER.error("BME680 sensor not detected at 0x%02x", i2c_address) - return None - - sensor_handler = BME680Handler( - sensor, - ( - SENSOR_GAS in config[CONF_MONITORED_CONDITIONS] - or SENSOR_AQ in config[CONF_MONITORED_CONDITIONS] - ), - config[CONF_AQ_BURN_IN_TIME], - config[CONF_AQ_HUM_BASELINE], - config[CONF_AQ_HUM_WEIGHTING], - ) - sleep(0.5) # Wait for device to stabilize - if not sensor_handler.sensor_data.temperature: - _LOGGER.error("BME680 sensor failed to Initialize") - return None - - return sensor_handler - - -class BME680Handler: - """BME680 sensor working in i2C bus.""" - - class SensorData: - """Sensor data representation.""" - - def __init__(self): - """Initialize the sensor data object.""" - self.temperature = None - self.humidity = None - self.pressure = None - self.gas_resistance = None - self.air_quality = None - - def __init__( - self, - sensor, - gas_measurement=False, - burn_in_time=300, - hum_baseline=40, - hum_weighting=25, - ): - """Initialize the sensor handler.""" - self.sensor_data = BME680Handler.SensorData() - self._sensor = sensor - self._gas_sensor_running = False - self._hum_baseline = hum_baseline - self._hum_weighting = hum_weighting - self._gas_baseline = None - - if gas_measurement: - - threading.Thread( - target=self._run_gas_sensor, - kwargs={"burn_in_time": burn_in_time}, - name="BME680Handler_run_gas_sensor", - ).start() - self.update(first_read=True) - - def _run_gas_sensor(self, burn_in_time): - """Calibrate the Air Quality Gas Baseline.""" - if self._gas_sensor_running: - return - - self._gas_sensor_running = True - - # Pause to allow initial data read for device validation. - sleep(1) - - start_time = monotonic() - curr_time = monotonic() - burn_in_data = [] - - _LOGGER.info( - "Beginning %d second gas sensor burn in for Air Quality", burn_in_time - ) - while curr_time - start_time < burn_in_time: - curr_time = monotonic() - if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: - gas_resistance = self._sensor.data.gas_resistance - burn_in_data.append(gas_resistance) - self.sensor_data.gas_resistance = gas_resistance - _LOGGER.debug( - "AQ Gas Resistance Baseline reading %2f Ohms", gas_resistance - ) - sleep(1) - - _LOGGER.debug( - "AQ Gas Resistance Burn In Data (Size: %d): \n\t%s", - len(burn_in_data), - burn_in_data, - ) - self._gas_baseline = sum(burn_in_data[-50:]) / 50.0 - _LOGGER.info("Completed gas sensor burn in for Air Quality") - _LOGGER.info("AQ Gas Resistance Baseline: %f", self._gas_baseline) - while True: - if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: - self.sensor_data.gas_resistance = self._sensor.data.gas_resistance - self.sensor_data.air_quality = self._calculate_aq_score() - sleep(1) - - def update(self, first_read=False): - """Read sensor data.""" - if first_read: - # Attempt first read, it almost always fails first attempt - self._sensor.get_sensor_data() - if self._sensor.get_sensor_data(): - self.sensor_data.temperature = self._sensor.data.temperature - self.sensor_data.humidity = self._sensor.data.humidity - self.sensor_data.pressure = self._sensor.data.pressure - - def _calculate_aq_score(self): - """Calculate the Air Quality Score.""" - hum_baseline = self._hum_baseline - hum_weighting = self._hum_weighting - gas_baseline = self._gas_baseline - - gas_resistance = self.sensor_data.gas_resistance - gas_offset = gas_baseline - gas_resistance - - hum = self.sensor_data.humidity - hum_offset = hum - hum_baseline - - # Calculate hum_score as the distance from the hum_baseline. - if hum_offset > 0: - hum_score = ( - (100 - hum_baseline - hum_offset) / (100 - hum_baseline) * hum_weighting - ) - else: - hum_score = (hum_baseline + hum_offset) / hum_baseline * hum_weighting - - # Calculate gas_score as the distance from the gas_baseline. - if gas_offset > 0: - gas_score = (gas_resistance / gas_baseline) * (100 - hum_weighting) - else: - gas_score = 100 - hum_weighting - - # Calculate air quality score. - return hum_score + gas_score - - -class BME680Sensor(SensorEntity): - """Implementation of the BME680 sensor.""" - - def __init__(self, bme680_client, name, description: SensorEntityDescription): - """Initialize the sensor.""" - self.entity_description = description - self._attr_name = f"{name} {description.name}" - self.bme680_client = bme680_client - - async def async_update(self): - """Get the latest data from the BME680 and update the states.""" - await self.hass.async_add_executor_job(self.bme680_client.update) - sensor_type = self.entity_description.key - if sensor_type == SENSOR_TEMP: - self._attr_native_value = round( - self.bme680_client.sensor_data.temperature, 1 - ) - elif sensor_type == SENSOR_HUMID: - self._attr_native_value = round(self.bme680_client.sensor_data.humidity, 1) - elif sensor_type == SENSOR_PRESS: - self._attr_native_value = round(self.bme680_client.sensor_data.pressure, 1) - elif sensor_type == SENSOR_GAS: - self._attr_native_value = int( - round(self.bme680_client.sensor_data.gas_resistance, 0) - ) - elif sensor_type == SENSOR_AQ: - aq_score = self.bme680_client.sensor_data.air_quality - if aq_score is not None: - self._attr_native_value = round(aq_score, 1) diff --git a/requirements_all.txt b/requirements_all.txt index 0ea23f68c23..0ebae17d10f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,9 +401,6 @@ blockchain==1.4.4 # homeassistant.components.miflora # bluepy==1.3.0 -# homeassistant.components.bme680 -# bme680==1.0.5 - # homeassistant.components.bond bond-api==0.1.16 @@ -2162,7 +2159,6 @@ smart-meter-texas==0.4.7 # homeassistant.components.smarthab smarthab==0.21 -# homeassistant.components.bme680 # homeassistant.components.raspihats # smbus-cffi==0.5.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 197b549e76f..21e681a6939 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -19,7 +19,6 @@ COMMENT_REQUIREMENTS = ( "beacontools", "beewi_smartclim", # depends on bluepy "bluepy", - "bme680", "decora", "decora_wifi", "evdev", From c7d59bb2729ca3da4e810246c7a8f0c1b8f01b1b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Feb 2022 13:19:50 +0000 Subject: [PATCH 0117/1054] Fix race when unsubscribing from MQTT topics (#67376) * Fix race when unsubscribing from MQTT topics * Improve test --- homeassistant/components/mqtt/__init__.py | 8 +++--- tests/components/mqtt/test_init.py | 32 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1982d1f3df5..107bc4660c2 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -967,10 +967,6 @@ class MQTT: self.subscriptions.remove(subscription) self._matching_subscriptions.cache_clear() - if any(other.topic == topic for other in self.subscriptions): - # Other subscriptions on topic remaining - don't unsubscribe. - return - # Only unsubscribe if currently connected. if self.connected: self.hass.async_create_task(self._async_unsubscribe(topic)) @@ -982,6 +978,10 @@ class MQTT: This method is a coroutine. """ + if any(other.topic == topic for other in self.subscriptions): + # Other subscriptions on topic remaining - don't unsubscribe. + return + async with self._paho_lock: result: int | None = None result, mid = await self.hass.async_add_executor_job( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index e589e447a01..7296d4e8101 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1056,6 +1056,38 @@ async def test_not_calling_unsubscribe_with_active_subscribers( assert not mqtt_client_mock.unsubscribe.called +async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): + """Test not calling unsubscribe() when other subscribers are active.""" + # Fake that the client is connected + mqtt_mock().connected = True + + calls_a = MagicMock() + calls_b = MagicMock() + + mqtt_client_mock.reset_mock() + unsub = await mqtt.async_subscribe(hass, "test/state", calls_a) + unsub() + await mqtt.async_subscribe(hass, "test/state", calls_b) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test/state", "online") + await hass.async_block_till_done() + assert not calls_a.called + assert calls_b.called + + # We allow either calls [subscribe, unsubscribe, subscribe] or [subscribe, subscribe] + expected_calls_1 = [ + call.subscribe("test/state", 0), + call.unsubscribe("test/state"), + call.subscribe("test/state", 0), + ] + expected_calls_2 = [ + call.subscribe("test/state", 0), + call.subscribe("test/state", 0), + ] + assert mqtt_client_mock.mock_calls in (expected_calls_1, expected_calls_2) + + @pytest.mark.parametrize( "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], From 5e4b16c69a77f4238f0434ea985e8eb5a0d096b2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Feb 2022 14:50:49 +0000 Subject: [PATCH 0118/1054] Remove custom WS command for removing MQTT devices (#67381) * Remove custom WS command for removing MQTT devices * Re-add removed test --- homeassistant/components/mqtt/__init__.py | 39 +------- .../mqtt/test_device_tracker_discovery.py | 7 +- tests/components/mqtt/test_device_trigger.py | 8 +- tests/components/mqtt/test_discovery.py | 8 +- tests/components/mqtt/test_init.py | 97 ++----------------- tests/components/mqtt/test_tag.py | 9 +- 6 files changed, 35 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 107bc4660c2..b98fe990fe8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -50,12 +50,7 @@ from homeassistant.core import ( ) from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - event, - template, -) +from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity @@ -588,7 +583,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf: ConfigType | None = config.get(DOMAIN) websocket_api.async_register_command(hass, websocket_subscribe) - websocket_api.async_register_command(hass, websocket_remove_device) websocket_api.async_register_command(hass, websocket_mqtt_info) debug_info.initialize(hass) @@ -1198,37 +1192,6 @@ def websocket_mqtt_info(hass, connection, msg): connection.send_result(msg["id"], mqtt_info) -@websocket_api.websocket_command( - {vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str} -) -@websocket_api.async_response -async def websocket_remove_device(hass, connection, msg): - """Delete device.""" - device_id = msg["device_id"] - device_registry = dr.async_get(hass) - - if not (device := device_registry.async_get(device_id)): - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" - ) - return - - for config_entry in device.config_entries: - config_entry = hass.config_entries.async_get_entry(config_entry) - # Only delete the device if it belongs to an MQTT device entry - if config_entry.domain == DOMAIN: - await async_remove_config_entry_device(hass, config_entry, device) - device_registry.async_update_device( - device_id, remove_config_entry_id=config_entry.entry_id - ) - connection.send_message(websocket_api.result_message(msg["id"])) - return - - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Non MQTT device" - ) - - @websocket_api.websocket_command( { vol.Required("type"): "mqtt/subscribe", diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 3b83581b86a..f8ee94b58f9 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components import device_tracker +from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN from homeassistant.setup import async_setup_component @@ -187,7 +188,7 @@ async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): async def test_cleanup_device_tracker( hass, hass_ws_client, device_reg, entity_reg, mqtt_mock ): - """Test discvered device is cleaned up when removed from registry.""" + """Test discovered device is cleaned up when removed from registry.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -210,10 +211,12 @@ async def test_cleanup_device_tracker( assert state is not None # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 8a3719f1707..8cfac3bf993 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -692,10 +692,12 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) @@ -1005,10 +1007,12 @@ async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqt assert triggers[0]["type"] == "foo" # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 463f3d03fff..d0c00f6b2ce 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -592,10 +592,12 @@ async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt assert state is not None # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) @@ -717,10 +719,12 @@ async def test_cleanup_device_multiple_config_entries( assert state is not None # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7296d4e8101..b770122d39a 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -12,7 +12,7 @@ import voluptuous as vol import yaml from homeassistant import config as hass_config -from homeassistant.components import mqtt, websocket_api +from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage @@ -1698,6 +1698,8 @@ async def test_mqtt_ws_remove_discovered_device( hass, device_reg, entity_reg, hass_ws_client, mqtt_mock ): """Test MQTT websocket device removal.""" + assert await async_setup_component(hass, "config", {}) + data = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -1712,8 +1714,14 @@ async def test_mqtt_ws_remove_discovered_device( assert device_entry is not None client = await hass_ws_client(hass) + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, + "device_id": device_entry.id, + } ) response = await client.receive_json() assert response["success"] @@ -1723,91 +1731,6 @@ async def test_mqtt_ws_remove_discovered_device( assert device_entry is None -async def test_mqtt_ws_remove_discovered_device_twice( - hass, device_reg, hass_ws_client, mqtt_mock -): - """Test MQTT websocket device removal.""" - data = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/sensor",' - ' "unique_id": "unique" }' - ) - - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) - await hass.async_block_till_done() - - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert response["success"] - - await client.send_json( - {"id": 6, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - - -async def test_mqtt_ws_remove_discovered_device_same_topic( - hass, device_reg, hass_ws_client, mqtt_mock -): - """Test MQTT websocket device removal.""" - data = ( - '{ "device":{"identifiers":["0AFFD2"]},' - ' "state_topic": "foobar/sensor",' - ' "availability_topic": "foobar/sensor",' - ' "unique_id": "unique" }' - ) - - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) - await hass.async_block_till_done() - - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert response["success"] - - await client.send_json( - {"id": 6, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - - -async def test_mqtt_ws_remove_non_mqtt_device( - hass, device_reg, hass_ws_client, mqtt_mock -): - """Test MQTT websocket device removal of device belonging to other domain.""" - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - - async def test_mqtt_ws_get_device_debug_info( hass, device_reg, hass_ws_client, mqtt_mock ): diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 7d3b4f2e1b2..99e2fffc085 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -6,6 +6,7 @@ from unittest.mock import ANY, patch import pytest from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -378,10 +379,12 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry.id, } ) @@ -537,10 +540,12 @@ async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mo mqtt_mock.async_publish.assert_not_called() # Remove MQTT from the device + mqtt_config_entry = hass.config_entries.async_entries(MQTT_DOMAIN)[0] await ws_client.send_json( { "id": 6, - "type": "mqtt/device/remove", + "type": "config/device_registry/remove_config_entry", + "config_entry_id": mqtt_config_entry.entry_id, "device_id": device_entry1.id, } ) From f9616c2ae3d429bc05910b784e1c8def334fd670 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Feb 2022 17:13:38 +0000 Subject: [PATCH 0119/1054] Remove custom WS command for removing Tasmota devices (#67382) --- homeassistant/components/tasmota/__init__.py | 37 +---------- tests/components/tasmota/test_init.py | 67 +++----------------- 2 files changed, 11 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 44dd2489177..b6e92301554 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -15,15 +15,13 @@ from hatasmota.const import ( from hatasmota.discovery import clear_discovery_topic from hatasmota.models import TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient -import voluptuous as vol -from homeassistant.components import mqtt, websocket_api +from homeassistant.components import mqtt from homeassistant.components.mqtt.subscription import ( async_prepare_subscribe_topics, async_subscribe_topics, async_unsubscribe_topics, ) -from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -39,7 +37,6 @@ from .const import ( CONF_DISCOVERY_PREFIX, DATA_REMOVE_DISCOVER_COMPONENT, DATA_UNSUB, - DOMAIN, PLATFORMS, ) @@ -48,7 +45,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tasmota from a config entry.""" - websocket_api.async_register_command(hass, websocket_remove_device) hass.data[DATA_UNSUB] = [] async def _publish( @@ -218,37 +214,6 @@ async def async_setup_device( _update_device(hass, config_entry, config, device_registry) -@websocket_api.websocket_command( - {vol.Required("type"): "tasmota/device/remove", vol.Required("device_id"): str} -) -@callback -def websocket_remove_device( - hass: HomeAssistant, connection: ActiveConnection, msg: dict -) -> None: - """Delete device.""" - device_id = msg["device_id"] - dev_registry = dr.async_get(hass) - - if not (device := dev_registry.async_get(device_id)): - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" - ) - return - - for config_entry_id in device.config_entries: - config_entry = hass.config_entries.async_get_entry(config_entry_id) - assert config_entry - # Only delete the device if it belongs to a Tasmota device entry - if config_entry.domain == DOMAIN: - dev_registry.async_remove_device(device_id) - connection.send_message(websocket_api.result_message(msg["id"])) - return - - connection.send_error( - msg["id"], websocket_api.const.ERR_NOT_FOUND, "Non Tasmota device" - ) - - async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 1b9c88ee4b1..7cbb12c49fc 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -3,9 +3,9 @@ import copy import json from unittest.mock import call -from homeassistant.components import websocket_api -from homeassistant.components.tasmota.const import DEFAULT_PREFIX +from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from .test_common import DEFAULT_CONFIG @@ -111,6 +111,7 @@ async def test_tasmota_ws_remove_discovered_device( hass, device_reg, entity_reg, hass_ws_client, mqtt_mock, setup_tasmota ): """Test Tasmota websocket device removal.""" + assert await async_setup_component(hass, "config", {}) config = copy.deepcopy(DEFAULT_CONFIG) mac = config["mac"] @@ -124,8 +125,14 @@ async def test_tasmota_ws_remove_discovered_device( assert device_entry is not None client = await hass_ws_client(hass) + tasmota_config_entry = hass.config_entries.async_entries(DOMAIN)[0] await client.send_json( - {"id": 5, "type": "tasmota/device/remove", "device_id": device_entry.id} + { + "id": 5, + "config_entry_id": tasmota_config_entry.entry_id, + "type": "config/device_registry/remove_config_entry", + "device_id": device_entry.id, + } ) response = await client.receive_json() assert response["success"] @@ -135,57 +142,3 @@ async def test_tasmota_ws_remove_discovered_device( set(), {(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is None - - -async def test_tasmota_ws_remove_discovered_device_twice( - hass, device_reg, hass_ws_client, mqtt_mock, setup_tasmota -): - """Test Tasmota websocket device removal.""" - config = copy.deepcopy(DEFAULT_CONFIG) - mac = config["mac"] - - async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) - await hass.async_block_till_done() - - # Verify device entry is created - device_entry = device_reg.async_get_device( - set(), {(dr.CONNECTION_NETWORK_MAC, mac)} - ) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "tasmota/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert response["success"] - - await client.send_json( - {"id": 6, "type": "tasmota/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND - assert response["error"]["message"] == "Device not found" - - -async def test_tasmota_ws_remove_non_tasmota_device( - hass, device_reg, hass_ws_client, mqtt_mock, setup_tasmota -): - """Test Tasmota websocket device removal of device belonging to other domain.""" - config_entry = MockConfigEntry(domain="test") - config_entry.add_to_hass(hass) - - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - assert device_entry is not None - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "tasmota/device/remove", "device_id": device_entry.id} - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND From 430cdc6d4cf4831af1434b4bc44e66e33d5080c9 Mon Sep 17 00:00:00 2001 From: k3mpaxl Date: Mon, 28 Feb 2022 18:52:36 +0100 Subject: [PATCH 0120/1054] Update modbus climate (#62483) Single register values weren't parsed accordingly for climate devices. Co-authored-by: jan Iversen --- homeassistant/components/modbus/climate.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index f2e5035e40c..0ecfc7b43b6 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -28,6 +28,7 @@ from . import get_hub from .base_platform import BaseStructPlatform from .const import ( CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_WRITE_REGISTER, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, CONF_MAX_TEMP, @@ -122,12 +123,21 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): for i in range(0, len(as_bytes), 2) ] registers = self._swap_registers(raw_regs) - result = await self._hub.async_pymodbus_call( - self._slave, - self._target_temperature_register, - registers, - CALL_TYPE_WRITE_REGISTERS, - ) + + if isinstance(registers, list): + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + [int(float(i)) for i in registers], + CALL_TYPE_WRITE_REGISTERS, + ) + else: + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + target_temperature, + CALL_TYPE_WRITE_REGISTER, + ) self._attr_available = result is not None await self.async_update() From c456b4c646110c7445be7c3c57cc913a1a22e9d4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 28 Feb 2022 09:52:52 -0800 Subject: [PATCH 0121/1054] Fix google calendar comment typo (#67389) --- homeassistant/components/google/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index d49ec48fe82..282c80988e4 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -154,7 +154,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice): _LOGGER.error("Unable to connect to Google: %s", err) return - # Pick the first visible evemt. Make a copy since calculate_offset mutates the event + # Pick the first visible event. Make a copy since calculate_offset mutates the event valid_items = filter(self._event_filter, items) self._event = copy.deepcopy(next(valid_items, None)) if self._event: From 2ca97f63090676edbedd06cac3f0b1cf402daaef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Feb 2022 20:03:43 +0100 Subject: [PATCH 0122/1054] Code quality improvements for Worldclock (#67392) --- .strict-typing | 1 + homeassistant/components/worldclock/sensor.py | 37 ++++++------------- mypy.ini | 11 ++++++ 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.strict-typing b/.strict-typing index 621c6c315fc..f6908fc39a6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -217,6 +217,7 @@ homeassistant.components.websocket_api.* homeassistant.components.wemo.* homeassistant.components.whois.* homeassistant.components.wiz.* +homeassistant.components.worldclock.* homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 069ca5c55e7..e626add2534 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -1,6 +1,8 @@ """Support for showing the time in a different time zone.""" from __future__ import annotations +from datetime import tzinfo + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -14,7 +16,6 @@ import homeassistant.util.dt as dt_util CONF_TIME_FORMAT = "time_format" DEFAULT_NAME = "Worldclock Sensor" -ICON = "mdi:clock" DEFAULT_TIME_STR_FORMAT = "%H:%M" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -33,15 +34,13 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the World clock sensor.""" - name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config[CONF_TIME_ZONE]) - async_add_entities( [ WorldClockSensor( time_zone, - name, - config.get(CONF_TIME_FORMAT), + config[CONF_NAME], + config[CONF_TIME_FORMAT], ) ], True, @@ -51,28 +50,16 @@ async def async_setup_platform( class WorldClockSensor(SensorEntity): """Representation of a World clock sensor.""" - def __init__(self, time_zone, name, time_format): + _attr_icon = "mdi:clock" + + def __init__(self, time_zone: tzinfo | None, name: str, time_format: str) -> None: """Initialize the sensor.""" - self._name = name + self._attr_name = name self._time_zone = time_zone - self._state = None self._time_format = time_format - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - async def async_update(self): + async def async_update(self) -> None: """Get the time and updates the states.""" - self._state = dt_util.now(time_zone=self._time_zone).strftime(self._time_format) + self._attr_native_value = dt_util.now(time_zone=self._time_zone).strftime( + self._time_format + ) diff --git a/mypy.ini b/mypy.ini index f817a5c58a6..55d608c8628 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2188,6 +2188,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.worldclock.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true From 690223fb699b8b23d3f2745e983733cd71e53208 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 28 Feb 2022 20:06:32 +0100 Subject: [PATCH 0123/1054] Add tests for Modbus slave binary sensors, up coverage to 100% (#67373) --- .coveragerc | 1 - tests/components/modbus/test_binary_sensor.py | 90 +++++++++++++++---- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/.coveragerc b/.coveragerc index 905be37e26a..a2c09c0dc70 100644 --- a/.coveragerc +++ b/.coveragerc @@ -706,7 +706,6 @@ omit = homeassistant/components/mjpeg/util.py homeassistant/components/mochad/* homeassistant/components/modbus/climate.py - homeassistant/components/modbus/binary_sensor.py homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/moehlenhoff_alpha2/__init__.py diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 15a03b76927..e307d1b6149 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -257,16 +257,68 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 51, - CONF_SLAVE_COUNT: 8, } ] }, ], ) @pytest.mark.parametrize( - "register_words,expected, slaves", + "config_addon,register_words,expected, slaves", [ ( + {CONF_SLAVE_COUNT: 1}, + [0x01], + STATE_ON, + [ + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 1}, + [0x02], + STATE_OFF, + [ + STATE_ON, + ], + ), + ( + {CONF_SLAVE_COUNT: 1}, + [0x04], + STATE_OFF, + [ + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 7}, + [0x01], + STATE_ON, + [ + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 7}, + [0x82], + STATE_OFF, + [ + STATE_ON, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_ON, + ], + ), + ( + {CONF_SLAVE_COUNT: 10}, [0x01, 0x00], STATE_ON, [ @@ -278,23 +330,12 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): STATE_OFF, STATE_OFF, STATE_OFF, - ], - ), - ( - [0x02, 0x00], - STATE_OFF, - [ - STATE_ON, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, - STATE_OFF, STATE_OFF, STATE_OFF, ], ), ( + {CONF_SLAVE_COUNT: 10}, [0x01, 0x01], STATE_ON, [ @@ -306,6 +347,25 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): STATE_OFF, STATE_OFF, STATE_ON, + STATE_OFF, + STATE_OFF, + ], + ), + ( + {CONF_SLAVE_COUNT: 10}, + [0x81, 0x01], + STATE_ON, + [ + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_OFF, + STATE_ON, + STATE_ON, + STATE_OFF, + STATE_OFF, ], ), ], @@ -314,6 +374,6 @@ async def test_slave_binary_sensor(hass, expected, slaves, mock_do_cycle): """Run test for given config.""" assert hass.states.get(ENTITY_ID).state == expected - for i in range(8): + for i in range(len(slaves)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") assert hass.states.get(entity_id).state == slaves[i] From e891df0ff3f51809f8e2186e9ff8845de8410099 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 28 Feb 2022 20:07:55 +0100 Subject: [PATCH 0124/1054] Allow multi read of Modbus sensor (#67378) --- homeassistant/components/modbus/__init__.py | 1 + homeassistant/components/modbus/sensor.py | 86 ++++++++++++++++-- homeassistant/components/modbus/validators.py | 9 ++ tests/components/modbus/test_init.py | 8 ++ tests/components/modbus/test_sensor.py | 90 ++++++++++++++++++- 5 files changed, 185 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index fd8e15b9b7e..eb55066390b 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -253,6 +253,7 @@ SENSOR_SCHEMA = vol.All( vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, } ), ) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index e4249594940..8363de3adf1 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,19 +2,27 @@ from __future__ import annotations from datetime import datetime +import logging from typing import Any from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity from homeassistant.const import CONF_NAME, CONF_SENSORS, CONF_UNIT_OF_MEASUREMENT -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_hub from .base_platform import BaseStructPlatform +from .const import CONF_SLAVE_COUNT from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + PARALLEL_UPDATES = 1 @@ -25,15 +33,18 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Modbus sensors.""" - sensors = [] if discovery_info is None: # pragma: no cover return + sensors: list[ModbusRegisterSensor | SlaveSensor] = [] + hub = get_hub(hass, discovery_info[CONF_NAME]) for entry in discovery_info[CONF_SENSORS]: - hub = get_hub(hass, discovery_info[CONF_NAME]) - sensors.append(ModbusRegisterSensor(hub, entry)) - + slave_count = entry.get(CONF_SLAVE_COUNT, 0) + sensor = ModbusRegisterSensor(hub, entry) + if slave_count > 0: + sensors.extend(await sensor.async_setup_slaves(hass, slave_count, entry)) + sensors.append(sensor) async_add_entities(sensors) @@ -47,9 +58,30 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) + self._coordinator: DataUpdateCoordinator[Any] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) + async def async_setup_slaves( + self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any] + ) -> list[SlaveSensor]: + """Add slaves as needed (1 read for multiple sensors).""" + + # Add a dataCoordinator for each sensor that have slaves + # this ensures that idx = bit position of value in result + # polling is done with the base class + name = self._attr_name if self._attr_name else "modbus_sensor" + self._coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + ) + + slaves: list[SlaveSensor] = [] + for idx in range(0, slave_count): + slaves.append(SlaveSensor(self._coordinator, idx, entry)) + return slaves + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() @@ -60,10 +92,10 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): """Update the state of the sensor.""" # remark "now" is a dummy parameter to avoid problems with # async_track_time_interval - result = await self._hub.async_pymodbus_call( + raw_result = await self._hub.async_pymodbus_call( self._slave, self._address, self._count, self._input_type ) - if result is None: + if raw_result is None: if self._lazy_errors: self._lazy_errors -= 1 return @@ -72,10 +104,48 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() return - self._attr_native_value = self.unpack_structure_result(result.registers) + result = self.unpack_structure_result(raw_result.registers) + if self._coordinator: + if result: + result_array = result.split(",") + self._attr_native_value = result_array[0] + self._coordinator.async_set_updated_data(result_array) + else: + self._attr_native_value = None + self._coordinator.async_set_updated_data(None) + else: + self._attr_native_value = result if self._attr_native_value is None: self._attr_available = False else: self._attr_available = True self._lazy_errors = self._lazy_error_count self.async_write_ha_state() + + +class SlaveSensor(CoordinatorEntity, RestoreEntity, SensorEntity): + """Modbus slave binary sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + ) -> None: + """Initialize the Modbus binary sensor.""" + idx += 1 + self._idx = idx + self._attr_name = f"{entry[CONF_NAME]} {idx}" + self._attr_available = False + super().__init__(coordinator) + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + if state := await self.async_get_last_state(): + self._attr_native_value = state.state + await super().async_added_to_hass() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + result = self.coordinator.data + if result: + self._attr_native_value = result[self._idx] + super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 646e1129c4e..35fc9cf63a9 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -26,6 +26,7 @@ from homeassistant.const import ( from .const import ( CONF_DATA_TYPE, CONF_INPUT_TYPE, + CONF_SLAVE_COUNT, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_NONE, @@ -63,6 +64,7 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: count = config.get(CONF_COUNT, 1) name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) + slave_count = config.get(CONF_SLAVE_COUNT, 0) + 1 swap_type = config.get(CONF_SWAP) if config[CONF_DATA_TYPE] != DataType.CUSTOM: if structure: @@ -75,7 +77,14 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" if CONF_COUNT not in config: config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count + if slave_count > 1: + structure = f">{slave_count}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" + else: + structure = f">{DEFAULT_STRUCT_FORMAT[data_type].struct_id}" else: + if slave_count > 1: + error = f"{name} structure: cannot be mixed with {CONF_SLAVE_COUNT}" + raise vol.Invalid(error) if not structure: error = ( f"Error in sensor {name}. The `{CONF_STRUCTURE}` field can not be empty" diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 127389ee50f..ac9aa964316 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -42,6 +42,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_MSG_WAIT, CONF_PARITY, + CONF_SLAVE_COUNT, CONF_STOPBITS, CONF_SWAP, CONF_SWAP_BYTE, @@ -209,6 +210,13 @@ async def test_ok_struct_validator(do_config): CONF_STRUCTURE: ">f", CONF_SWAP: CONF_SWAP_WORD, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.CUSTOM, + CONF_STRUCTURE: ">f", + CONF_SLAVE_COUNT: 5, + }, ], ) async def test_exception_struct_validator(do_config): diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index e3085ecf91e..aa513d1c473 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.modbus.const import ( CONF_LAZY_ERROR, CONF_PRECISION, CONF_SCALE, + CONF_SLAVE_COUNT, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_NONE, @@ -124,6 +125,16 @@ ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") } ] }, + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT32, + CONF_SLAVE_COUNT: 5, + } + ] + }, ], ) async def test_config_sensor(hass, mock_modbus): @@ -535,6 +546,73 @@ async def test_all_sensor(hass, mock_do_cycle, expected): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_DATA_TYPE: DataType.UINT32, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "config_addon,register_words,expected", + [ + ( + { + CONF_SLAVE_COUNT: 0, + }, + [0x0102, 0x0304], + ["16909060"], + ), + ( + { + CONF_SLAVE_COUNT: 1, + }, + [0x0102, 0x0304, 0x0403, 0x0201], + ["16909060", "67305985"], + ), + ( + { + CONF_SLAVE_COUNT: 3, + }, + [ + 0x0102, + 0x0304, + 0x0506, + 0x0708, + 0x090A, + 0x0B0C, + 0x0D0E, + 0x0F00, + ], + [ + "16909060", + "84281096", + "151653132", + "219025152", + ], + ), + ], +) +async def test_slave_sensor(hass, mock_do_cycle, expected): + """Run test for sensor.""" + assert hass.states.get(ENTITY_ID).state == expected[0] + + for i in range(1, len(expected)): + entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") + assert hass.states.get(entity_id).state == expected[i] + + @pytest.mark.parametrize( "do_config", [ @@ -664,7 +742,7 @@ async def test_struct_sensor(hass, mock_do_cycle, expected): @pytest.mark.parametrize( "mock_test_state", - [(State(ENTITY_ID, "117"),)], + [(State(ENTITY_ID, "117"), State(f"{ENTITY_ID}_1", "119"))], indirect=True, ) @pytest.mark.parametrize( @@ -679,6 +757,16 @@ async def test_struct_sensor(hass, mock_do_cycle, expected): } ] }, + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, + CONF_SLAVE_COUNT: 1, + } + ] + }, ], ) async def test_restore_state_sensor(hass, mock_test_state, mock_modbus): From 148762ce3ff4cf5c907072750ccdc1e5c1605dff Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Feb 2022 19:08:27 +0000 Subject: [PATCH 0125/1054] Improve cast test for removing device (#67385) --- tests/components/cast/test_media_player.py | 23 ++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 663941de77a..fa70ed17eb9 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -633,8 +633,10 @@ async def test_entity_availability(hass: HomeAssistant): @pytest.mark.parametrize("port,entry_type", ((8009, None), (12345, None))) -async def test_device_registry(hass: HomeAssistant, port, entry_type): +async def test_device_registry(hass: HomeAssistant, hass_ws_client, port, entry_type): """Test device registry integration.""" + assert await async_setup_component(hass, "config", {}) + entity_id = "media_player.speaker" reg = er.async_get(hass) dev_reg = dr.async_get(hass) @@ -657,19 +659,32 @@ async def test_device_registry(hass: HomeAssistant, port, entry_type): assert state.state == "off" assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) entity_entry = reg.async_get(entity_id) - assert entity_entry.device_id is not None device_entry = dev_reg.async_get(entity_entry.device_id) + assert entity_entry.device_id == device_entry.id assert device_entry.entry_type == entry_type # Check that the chromecast object is torn down when the device is removed chromecast.disconnect.assert_not_called() - dev_reg.async_update_device( - device_entry.id, remove_config_entry_id=cast_entry.entry_id + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": cast_entry.entry_id, + "device_id": device_entry.id, + } ) + response = await client.receive_json() + assert response["success"] + await hass.async_block_till_done() await hass.async_block_till_done() chromecast.disconnect.assert_called_once() + assert reg.async_get(entity_id) is None + assert dev_reg.async_get(entity_entry.device_id) is None + async def test_entity_cast_status(hass: HomeAssistant): """Test handling of cast status.""" From 974296697ee0618fb2ba5b2c2a0641efad092e29 Mon Sep 17 00:00:00 2001 From: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:13:56 -0800 Subject: [PATCH 0126/1054] Add diagnostics for screenlogic (#67368) --- .coveragerc | 1 + .../components/screenlogic/diagnostics.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 homeassistant/components/screenlogic/diagnostics.py diff --git a/.coveragerc b/.coveragerc index a2c09c0dc70..e211af06bf5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -995,6 +995,7 @@ omit = homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py + homeassistant/components/screenlogic/diagnostics.py homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/sensor.py diff --git a/homeassistant/components/screenlogic/diagnostics.py b/homeassistant/components/screenlogic/diagnostics.py new file mode 100644 index 00000000000..33041597b75 --- /dev/null +++ b/homeassistant/components/screenlogic/diagnostics.py @@ -0,0 +1,21 @@ +"""Diagnostics for Screenlogic.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import ScreenlogicDataUpdateCoordinator +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + return { + "config_entry": config_entry.as_dict(), + "data": coordinator.data, + } From d077c3b8d106e7e102a5a58a8a07ed381ff06567 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Mon, 28 Feb 2022 11:31:34 -0800 Subject: [PATCH 0127/1054] Add sensor to expose Powerwall backup reserve percentage (#66393) --- .../components/powerwall/__init__.py | 1 + homeassistant/components/powerwall/models.py | 1 + homeassistant/components/powerwall/sensor.py | 25 ++++++++++++++++++- tests/components/powerwall/mocks.py | 5 ++++ tests/components/powerwall/test_sensor.py | 12 +++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 10504e2aa06..d2850330b7a 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -229,6 +229,7 @@ def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData: meters=power_wall.get_meters(), grid_services_active=power_wall.is_grid_services_active(), grid_status=power_wall.get_grid_status(), + backup_reserve=power_wall.get_backup_reserve_percentage(), ) diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index 472d9e59304..cb9b84be16a 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -38,6 +38,7 @@ class PowerwallData: meters: MetersAggregates grid_services_active: bool grid_status: GridStatus + backup_reserve: float class PowerwallRuntimeData(TypedDict): diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index bc8ab1c0215..9efa5f380dd 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -45,7 +45,11 @@ async def async_setup_entry( | PowerWallImportSensor | PowerWallExportSensor | PowerWallChargeSensor - ] = [PowerWallChargeSensor(powerwall_data)] + | PowerWallBackupReserveSensor + ] = [ + PowerWallChargeSensor(powerwall_data), + PowerWallBackupReserveSensor(powerwall_data), + ] for meter in data.meters.meters: entities.extend( @@ -111,6 +115,25 @@ class PowerWallEnergySensor(PowerWallEntity, SensorEntity): } +class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity): + """Representation of the Powerwall backup reserve setting.""" + + _attr_name = "Powerwall Backup Reserve" + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_native_unit_of_measurement = PERCENTAGE + _attr_device_class = SensorDeviceClass.BATTERY + + @property + def unique_id(self) -> str: + """Device Uniqueid.""" + return f"{self.base_unique_id}_backup_reserve" + + @property + def native_value(self) -> int: + """Get the current value in percentage.""" + return round(self.data.backup_reserve) + + class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 1eac0319819..ae6601b0215 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -37,6 +37,7 @@ async def _mock_powerwall_with_fixtures(hass): status=PowerwallStatus(status), device_type=DeviceType(device_type["device_type"]), serial_numbers=["TG0123456789AB", "TG9876543210BA"], + backup_reserve_percentage=15.0, ) @@ -50,6 +51,7 @@ def _mock_powerwall_return_value( status=None, device_type=None, serial_numbers=None, + backup_reserve_percentage=None, ): powerwall_mock = MagicMock(Powerwall("1.2.3.4")) powerwall_mock.get_site_info = Mock(return_value=site_info) @@ -60,6 +62,9 @@ def _mock_powerwall_return_value( powerwall_mock.get_status = Mock(return_value=status) powerwall_mock.get_device_type = Mock(return_value=device_type) powerwall_mock.get_serial_numbers = Mock(return_value=serial_numbers) + powerwall_mock.get_backup_reserve_percentage = Mock( + return_value=backup_reserve_percentage + ) powerwall_mock.is_grid_services_active = Mock(return_value=grid_services_active) return powerwall_mock diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 33c186e922c..9ff5bd19e39 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -121,3 +121,15 @@ async def test_sensors(hass): # HA changes the implementation and a new one appears for key, value in expected_attributes.items(): assert state.attributes[key] == value + + state = hass.states.get("sensor.powerwall_backup_reserve") + assert state.state == "15" + expected_attributes = { + "unit_of_measurement": PERCENTAGE, + "friendly_name": "Powerwall Backup Reserve", + "device_class": "battery", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + for key, value in expected_attributes.items(): + assert state.attributes[key] == value From 508ed257d46eb0b7e55f15fcb8114f43f8faa2c4 Mon Sep 17 00:00:00 2001 From: Jeff <34590663+jumbledbytes@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:37:11 -0800 Subject: [PATCH 0128/1054] Support disconnected Powerwall configuration (#67325) Co-authored-by: J. Nick Koston --- homeassistant/components/powerwall/binary_sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 868d9e3076d..fed47823c7f 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -110,6 +110,15 @@ class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): _attr_name = "Powerwall Charging" _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + @property + def available(self) -> bool: + """Powerwall is available.""" + # Return False if no battery is installed + return ( + super().available + and self.data.meters.get_meter(MeterType.BATTERY) is not None + ) + @property def unique_id(self) -> str: """Device Uniqueid.""" From 1556868d562b0426effd0d556b9665d2865a8018 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Feb 2022 20:53:42 +0100 Subject: [PATCH 0129/1054] Use async rest api in SamsungTV (#67369) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 31 +++-- tests/components/samsungtv/conftest.py | 67 +++-------- .../components/samsungtv/test_config_flow.py | 111 ++++++++++-------- .../components/samsungtv/test_media_player.py | 45 +++---- 4 files changed, 121 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index b79ea6af871..55fd44fd45c 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -2,13 +2,14 @@ from __future__ import annotations from abc import ABC, abstractmethod +from asyncio.exceptions import TimeoutError as AsyncioTimeoutError import contextlib from typing import Any -from requests.exceptions import Timeout as RequestsTimeout from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse from samsungtvws import SamsungTVWS +from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.exceptions import ConnectionFailure, HttpApiError from websocket import WebSocketException @@ -21,6 +22,7 @@ from homeassistant.const import ( CONF_TIMEOUT, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .const import ( @@ -294,6 +296,7 @@ class SamsungTVWSBridge(SamsungTVBridge): """Initialize Bridge.""" super().__init__(hass, method, host, port) self.token = token + self._rest_api: SamsungTVAsyncRest | None = None self._app_list: dict[str, str] | None = None self._remote: SamsungTVWS | None = None @@ -375,14 +378,21 @@ class SamsungTVWSBridge(SamsungTVBridge): async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" - return await self.hass.async_add_executor_job(self._device_info) + if not self.port: + return None - def _device_info(self) -> dict[str, Any] | None: - """Try to gather infos of this TV.""" - if remote := self._get_remote(avoid_open=True): - with contextlib.suppress(HttpApiError, RequestsTimeout): - device_info: dict[str, Any] = remote.rest_device_info() - return device_info + if self._rest_api is None: + self._rest_api = SamsungTVAsyncRest( + host=self.host, + session=async_get_clientsession(self.hass), + port=self.port, + timeout=TIMEOUT_WEBSOCKET, + ) + + with contextlib.suppress(HttpApiError, AsyncioTimeoutError): + device_info: dict[str, Any] = await self._rest_api.rest_device_info() + LOGGER.debug("Device info on %s is: %s", self.host, device_info) + return device_info return None @@ -416,7 +426,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # Different reasons, e.g. hostname not resolveable pass - def _get_remote(self, avoid_open: bool = False) -> SamsungTVWS: + def _get_remote(self) -> SamsungTVWS: """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -431,8 +441,7 @@ class SamsungTVWSBridge(SamsungTVBridge): timeout=TIMEOUT_WEBSOCKET, name=VALUE_CONF_NAME, ) - if not avoid_open: - self._remote.open() + self._remote.open() # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure as err: diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index e1cb4f86082..e92caac9fc4 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -32,16 +32,14 @@ def remote_fixture() -> Mock: yield remote -@pytest.fixture(name="remotews") -def remotews_fixture() -> Mock: - """Patch the samsungtvws SamsungTVWS.""" +@pytest.fixture(name="rest_api", autouse=True) +def rest_api_fixture() -> Mock: + """Patch the samsungtvws SamsungTVAsyncRest.""" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class: - remotews = Mock(SamsungTVWS) - remotews.__enter__ = Mock(return_value=remotews) - remotews.__exit__ = Mock() - remotews.rest_device_info.return_value = { + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + autospec=True, + ) as rest_api_class: + rest_api_class.return_value.rest_device_info.return_value = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { "modelName": "82GXARRS", @@ -51,51 +49,24 @@ def remotews_fixture() -> Mock: "networkType": "wireless", }, } + yield rest_api_class.return_value + + +@pytest.fixture(name="remotews") +def remotews_fixture() -> Mock: + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class: + remotews = Mock(SamsungTVWS) + remotews.__enter__ = Mock(return_value=remotews) + remotews.__exit__ = Mock() remotews.app_list.return_value = SAMPLE_APP_LIST remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews yield remotews -@pytest.fixture(name="remotews_no_device_info") -def remotews_no_device_info_fixture() -> Mock: - """Patch the samsungtvws SamsungTVWS.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class: - remotews = Mock(SamsungTVWS) - remotews.__enter__ = Mock(return_value=remotews) - remotews.__exit__ = Mock() - remotews.rest_device_info.return_value = None - remotews.token = "FAKE_TOKEN" - remotews_class.return_value = remotews - yield remotews - - -@pytest.fixture(name="remotews_soundbar") -def remotews_soundbar_fixture() -> Mock: - """Patch the samsungtvws SamsungTVWS.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class: - remotews = Mock(SamsungTVWS) - remotews.__enter__ = Mock(return_value=remotews) - remotews.__exit__ = Mock() - remotews.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "wifiMac": "aa:bb:cc:dd:ee:ff", - "mac": "aa:bb:cc:dd:ee:ff", - "name": "[TV] Living Room", - "type": "Samsung SoundBar", - }, - } - remotews.token = "FAKE_TOKEN" - remotews_class.return_value = remotews - yield remotews - - @pytest.fixture(name="delay") def delay_fixture() -> Mock: """Patch the delay script function.""" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index eb8ca745819..11ca69b91ef 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for Samsung TV config flow.""" import socket -from unittest.mock import Mock, call, patch +from unittest.mock import ANY, AsyncMock, Mock, call, patch import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse @@ -178,10 +178,9 @@ AUTODETECT_WEBSOCKET_SSL = { } DEVICEINFO_WEBSOCKET_SSL = { "host": "fake_host", - "name": "HomeAssistant", + "session": ANY, "port": 8002, "timeout": TIMEOUT_WEBSOCKET, - "token": "123456789", } @@ -456,8 +455,11 @@ async def test_ssdp_websocket_success_populates_mac_address( assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_websocket_not_supported(hass: HomeAssistant) -> None: +async def test_ssdp_websocket_not_supported( + hass: HomeAssistant, rest_api: Mock +) -> None: """Test starting a flow from discovery for not supported device.""" + rest_api.rest_device_info.return_value = None with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), @@ -630,9 +632,10 @@ async def test_import_legacy(hass: HomeAssistant, no_mac_address: Mock) -> None: assert entries[0].data[CONF_PORT] == LEGACY_PORT -@pytest.mark.usefixtures("remote", "remotews_no_device_info", "no_mac_address") -async def test_import_legacy_without_name(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("remote", "remotews", "no_mac_address") +async def test_import_legacy_without_name(hass: HomeAssistant, rest_api: Mock) -> None: """Test importing from yaml without a name.""" + rest_api.rest_device_info.return_value = None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -762,9 +765,19 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews_soundbar") -async def test_zeroconf_ignores_soundbar(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("remotews") +async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from zeroconf where the device is actually a soundbar.""" + rest_api.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "mac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SoundBar", + }, + } result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -775,9 +788,10 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant) -> None: assert result["reason"] == "not_supported" -@pytest.mark.usefixtures("remote", "remotews_no_device_info") -async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("remote", "remotews") +async def test_zeroconf_no_device_info(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from zeroconf where device_info returns None.""" + rest_api.rest_device_info.return_value = None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -815,23 +829,29 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), - ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews, patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + ) as rest_api_class: remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock(return_value=False) remote.app_list.return_value = SAMPLE_APP_LIST - remote.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "networkType": "wireless", - "wifiMac": "aa:bb:cc:dd:ee:ff", - "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "mac": "aa:bb:cc:dd:ee:ff", - "name": "[TV] Living Room", - "type": "Samsung SmartTV", - }, - } + rest_api_class.return_value.rest_device_info = AsyncMock( + return_value={ + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "networkType": "wireless", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "mac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + }, + } + ) remote.token = "123456789" remotews.return_value = remote @@ -841,11 +861,8 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" - assert remotews.call_count == 2 - assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_SSL), - call(**DEVICEINFO_WEBSOCKET_SSL), - ] + remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) + rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) @@ -861,22 +878,27 @@ async def test_websocket_no_mac(hass: HomeAssistant) -> None: ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews, patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + ) as rest_api_class, patch( "getmac.get_mac_address", return_value="gg:hh:ii:ll:mm:nn" ): remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock(return_value=False) remote.app_list.return_value = SAMPLE_APP_LIST - remote.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "networkType": "lan", - "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "name": "[TV] Living Room", - "type": "Samsung SmartTV", - }, - } + rest_api_class.return_value.rest_device_info = AsyncMock( + return_value={ + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "networkType": "lan", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + }, + } + ) + remote.token = "123456789" remotews.return_value = remote @@ -887,11 +909,8 @@ async def test_websocket_no_mac(hass: HomeAssistant) -> None: assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" assert result["data"][CONF_MAC] == "gg:hh:ii:ll:mm:nn" - assert remotews.call_count == 2 - assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_SSL), - call(**DEVICEINFO_WEBSOCKET_SSL), - ] + remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) + rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) @@ -1166,18 +1185,16 @@ async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote") async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( - hass: HomeAssistant, + hass: HomeAssistant, rest_api: Mock ) -> None: """Test missing mac added when there is no unique id.""" + rest_api.rest_device_info.side_effect = HttpApiError entry = MockConfigEntry( domain=DOMAIN, data=MOCK_LEGACY_ENTRY, ) entry.add_to_hass(hass) with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.rest_device_info", - side_effect=HttpApiError, - ), patch( "homeassistant.components.samsungtv.bridge.Remote.__enter__", return_value=True, ), patch( diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index eb7fb650a9a..ddc28097de4 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -159,31 +159,20 @@ async def test_setup_without_turnon(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews") async def test_setup_websocket(hass: HomeAssistant) -> None: """Test setup of platform.""" + with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock() remote.app_list.return_value = SAMPLE_APP_LIST - remote.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "wifiMac": "aa:bb:cc:dd:ee:ff", - "name": "[TV] Living Room", - "type": "Samsung SmartTV", - "networkType": "wireless", - }, - } + remote.token = "123456789" remote_class.return_value = remote await setup_samsungtv(hass, MOCK_CONFIGWS) - assert remote_class.call_count == 2 - assert remote_class.call_args_list == [ - call(**MOCK_CALLS_WS), - call(**MOCK_CALLS_WS), - ] + assert remote_class.call_count == 1 + assert remote_class.call_args_list == [call(**MOCK_CALLS_WS)] assert hass.states.get(ENTITY_ID) await hass.async_block_till_done() @@ -193,7 +182,9 @@ async def test_setup_websocket(hass: HomeAssistant) -> None: assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> None: +async def test_setup_websocket_2( + hass: HomeAssistant, mock_now: datetime, rest_api: Mock +) -> None: """Test setup of platform from config entry.""" entity_id = f"{DOMAIN}.fake" @@ -208,21 +199,21 @@ async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> Non assert len(config_entries) == 1 assert entry is config_entries[0] + rest_api.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + "networkType": "wireless", + }, + } with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: remote = Mock(SamsungTVWS) remote.__enter__ = Mock(return_value=remote) remote.__exit__ = Mock() remote.app_list.return_value = SAMPLE_APP_LIST - remote.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "wifiMac": "aa:bb:cc:dd:ee:ff", - "name": "[TV] Living Room", - "type": "Samsung SmartTV", - "networkType": "wireless", - }, - } remote.token = "987654321" remote_class.return_value = remote assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) @@ -237,7 +228,7 @@ async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> Non state = hass.states.get(entity_id) assert state - assert remote_class.call_count == 3 + assert remote_class.call_count == 2 assert remote_class.call_args_list[0] == call(**MOCK_CALLS_WS) From 353a963890237580acda2559286f67927e56662f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Feb 2022 23:10:14 +0100 Subject: [PATCH 0130/1054] Update shodan to 1.27.0 (#67393) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 49e6a14b715..7d609a5e9c3 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.26.1"], + "requirements": ["shodan==1.27.0"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling", "loggers": ["shodan"] diff --git a/requirements_all.txt b/requirements_all.txt index 0ebae17d10f..5f321f2b784 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2130,7 +2130,7 @@ sharkiqpy==0.1.8 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.26.1 +shodan==1.27.0 # homeassistant.components.sighthound simplehound==0.3 From e3709590cb517fd66bc7fa9d921d885ce62269a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Feb 2022 23:10:40 +0100 Subject: [PATCH 0131/1054] Update sentry-sdk to 1.5.6 (#67394) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 52f7cba7a19..2b67f1fa312 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.5.5"], + "requirements": ["sentry-sdk==1.5.6"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5f321f2b784..5b9948173f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2121,7 +2121,7 @@ sendgrid==6.8.2 sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.5 +sentry-sdk==1.5.6 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 519e803f144..188a56c2aa4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1343,7 +1343,7 @@ screenlogicpy==0.5.4 sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.5 +sentry-sdk==1.5.6 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 487f4dcd909754eed70a0f7be97f845636bc465f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Feb 2022 23:10:58 +0100 Subject: [PATCH 0132/1054] Remove deprecated Raspihats integration (#67380) --- .coveragerc | 1 - .github/workflows/wheels.yml | 2 - .../components/raspihats/__init__.py | 253 ------------------ .../components/raspihats/binary_sensor.py | 146 ---------- .../components/raspihats/manifest.json | 8 - homeassistant/components/raspihats/switch.py | 161 ----------- requirements_all.txt | 6 - script/gen_requirements_all.py | 2 - 8 files changed, 579 deletions(-) delete mode 100644 homeassistant/components/raspihats/__init__.py delete mode 100644 homeassistant/components/raspihats/binary_sensor.py delete mode 100644 homeassistant/components/raspihats/manifest.json delete mode 100644 homeassistant/components/raspihats/switch.py diff --git a/.coveragerc b/.coveragerc index e211af06bf5..1e9d2ba98a0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -948,7 +948,6 @@ omit = homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py - homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 740eafe95ed..0b73ee9fa70 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -135,11 +135,9 @@ jobs: sed -i "s|# bluepy|bluepy|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file} sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} - sed -i "s|# raspihats|raspihats|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} sed -i "s|# pycups|pycups|g" ${requirement_file} sed -i "s|# homekit|homekit|g" ${requirement_file} diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py deleted file mode 100644 index 8f4a8b0aca4..00000000000 --- a/homeassistant/components/raspihats/__init__.py +++ /dev/null @@ -1,253 +0,0 @@ -"""Support for controlling raspihats boards.""" -import logging -import threading -import time - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "raspihats" - -CONF_I2C_HATS = "i2c_hats" -CONF_BOARD = "board" -CONF_CHANNELS = "channels" -CONF_INDEX = "index" -CONF_INVERT_LOGIC = "invert_logic" -CONF_INITIAL_STATE = "initial_state" - -I2C_HAT_NAMES = [ - "Di16", - "Rly10", - "Di6Rly6", - "DI16ac", - "DQ10rly", - "DQ16oc", - "DI6acDQ6rly", -] - -I2C_HATS_MANAGER = "I2CH_MNG" - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the raspihats component.""" - _LOGGER.warning( - "The Raspihats pHAT integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - hass.data[DOMAIN] = {I2C_HATS_MANAGER: I2CHatsManager()} - - def start_i2c_hats_keep_alive(event): - """Start I2C-HATs keep alive.""" - hass.data[DOMAIN][I2C_HATS_MANAGER].start_keep_alive() - - def stop_i2c_hats_keep_alive(event): - """Stop I2C-HATs keep alive.""" - hass.data[DOMAIN][I2C_HATS_MANAGER].stop_keep_alive() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive) - return True - - -def log_message(source, *parts): - """Build log message.""" - message = source.__class__.__name__ - for part in parts: - message += f": {part!s}" - return message - - -class I2CHatsException(Exception): - """I2C-HATs exception.""" - - -class I2CHatsDIScanner: - """Scan Digital Inputs and fire callbacks.""" - - _DIGITAL_INPUTS = "di" - _OLD_VALUE = "old_value" - _CALLBACKS = "callbacks" - - def setup(self, i2c_hat): - """Set up the I2C-HAT instance for digital inputs scanner.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - old_value = None - # Add old value attribute - setattr(digital_inputs, self._OLD_VALUE, old_value) - # Add callbacks dict attribute {channel: callback} - setattr(digital_inputs, self._CALLBACKS, {}) - - def register_callback(self, i2c_hat, channel, callback): - """Register edge callback.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - callbacks = getattr(digital_inputs, self._CALLBACKS) - callbacks[channel] = callback - setattr(digital_inputs, self._CALLBACKS, callbacks) - - def scan(self, i2c_hat): - """Scan I2C-HATs digital inputs and fire callbacks.""" - if hasattr(i2c_hat, self._DIGITAL_INPUTS): - digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) - callbacks = getattr(digital_inputs, self._CALLBACKS) - old_value = getattr(digital_inputs, self._OLD_VALUE) - value = digital_inputs.value # i2c data transfer - if old_value is not None and value != old_value: - for channel in range(0, len(digital_inputs.channels)): - state = (value >> channel) & 0x01 - old_state = (old_value >> channel) & 0x01 - if state != old_state: - callback = callbacks.get(channel) - if callback is not None: - callback(state) - setattr(digital_inputs, self._OLD_VALUE, value) - - -class I2CHatsManager(threading.Thread): - """Manages all I2C-HATs instances.""" - - _EXCEPTION = "exception" - _CALLBACKS = "callbacks" - - def __init__(self): - """Init I2C-HATs Manager.""" - threading.Thread.__init__(self) - self._lock = threading.Lock() - self._i2c_hats = {} - self._run = False - self._di_scanner = I2CHatsDIScanner() - - def register_board(self, board, address): - """Register I2C-HAT.""" - with self._lock: - if (i2c_hat := self._i2c_hats.get(address)) is None: - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - import raspihats.i2c_hats as module - - constructor = getattr(module, board) - i2c_hat = constructor(address) - setattr(i2c_hat, self._CALLBACKS, {}) - - # Setting exception attribute will trigger online callbacks - # when keep alive thread starts. - setattr(i2c_hat, self._EXCEPTION, None) - - self._di_scanner.setup(i2c_hat) - self._i2c_hats[address] = i2c_hat - status_word = i2c_hat.status # read status_word to reset bits - _LOGGER.info(log_message(self, i2c_hat, "registered", status_word)) - - def run(self): - """Keep alive for I2C-HATs.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - _LOGGER.info(log_message(self, "starting")) - - while self._run: - with self._lock: - for i2c_hat in list(self._i2c_hats.values()): - try: - self._di_scanner.scan(i2c_hat) - self._read_status(i2c_hat) - - if hasattr(i2c_hat, self._EXCEPTION): - if getattr(i2c_hat, self._EXCEPTION) is not None: - _LOGGER.warning( - log_message(self, i2c_hat, "online again") - ) - delattr(i2c_hat, self._EXCEPTION) - # trigger online callbacks - callbacks = getattr(i2c_hat, self._CALLBACKS) - for callback in list(callbacks.values()): - callback() - except ResponseException as ex: - if not hasattr(i2c_hat, self._EXCEPTION): - _LOGGER.error(log_message(self, i2c_hat, ex)) - setattr(i2c_hat, self._EXCEPTION, ex) - time.sleep(0.05) - _LOGGER.info(log_message(self, "exiting")) - - def _read_status(self, i2c_hat): - """Read I2C-HATs status.""" - status_word = i2c_hat.status - if status_word.value != 0x00: - _LOGGER.error(log_message(self, i2c_hat, status_word)) - - def start_keep_alive(self): - """Start keep alive mechanism.""" - self._run = True - threading.Thread.start(self) - - def stop_keep_alive(self): - """Stop keep alive mechanism.""" - self._run = False - self.join() - - def register_di_callback(self, address, channel, callback): - """Register I2C-HAT digital input edge callback.""" - with self._lock: - i2c_hat = self._i2c_hats[address] - self._di_scanner.register_callback(i2c_hat, channel, callback) - - def register_online_callback(self, address, channel, callback): - """Register I2C-HAT online callback.""" - with self._lock: - i2c_hat = self._i2c_hats[address] - callbacks = getattr(i2c_hat, self._CALLBACKS) - callbacks[channel] = callback - setattr(i2c_hat, self._CALLBACKS, callbacks) - - def read_di(self, address, channel): - """Read a value from a I2C-HAT digital input.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - value = i2c_hat.di.value - return (value >> channel) & 0x01 - except ResponseException as ex: - raise I2CHatsException(str(ex)) from ex - - def write_dq(self, address, channel, value): - """Write a value to a I2C-HAT digital output.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - i2c_hat.dq.channels[channel] = value - except ResponseException as ex: - raise I2CHatsException(str(ex)) from ex - - def read_dq(self, address, channel): - """Read a value from a I2C-HAT digital output.""" - # This is a Pi module and can't be installed in CI without - # breaking the build. - # pylint: disable=import-outside-toplevel,import-error - from raspihats.i2c_hats import ResponseException - - with self._lock: - i2c_hat = self._i2c_hats[address] - try: - return i2c_hat.dq.channels[channel] - except ResponseException as ex: - raise I2CHatsException(str(ex)) from ex diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py deleted file mode 100644 index f8fbc0d010f..00000000000 --- a/homeassistant/components/raspihats/binary_sensor.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Support for raspihats board binary sensors.""" -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE_CLASS, - CONF_NAME, - DEVICE_DEFAULT_NAME, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - CONF_BOARD, - CONF_CHANNELS, - CONF_I2C_HATS, - CONF_INDEX, - CONF_INVERT_LOGIC, - DOMAIN, - I2C_HAT_NAMES, - I2C_HATS_MANAGER, - I2CHatsException, - I2CHatsManager, -) - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_INVERT_LOGIC = False -DEFAULT_DEVICE_CLASS = None - -_CHANNELS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_INDEX): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string, - } - ] -) - -_I2C_HATS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES), - vol.Required(CONF_ADDRESS): vol.Coerce(int), - vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA, - } - ] -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the raspihats binary_sensor devices.""" - I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[DOMAIN][I2C_HATS_MANAGER] - binary_sensors = [] - i2c_hat_configs = config.get(CONF_I2C_HATS, []) - for i2c_hat_config in i2c_hat_configs: - address = i2c_hat_config[CONF_ADDRESS] - board = i2c_hat_config[CONF_BOARD] - try: - assert I2CHatBinarySensor.I2C_HATS_MANAGER - I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address) - for channel_config in i2c_hat_config[CONF_CHANNELS]: - binary_sensors.append( - I2CHatBinarySensor( - address, - channel_config[CONF_INDEX], - channel_config[CONF_NAME], - channel_config[CONF_INVERT_LOGIC], - channel_config[CONF_DEVICE_CLASS], - ) - ) - except I2CHatsException as ex: - _LOGGER.error( - "Failed to register %s I2CHat@%s %s", board, hex(address), str(ex) - ) - add_entities(binary_sensors) - - -class I2CHatBinarySensor(BinarySensorEntity): - """Representation of a binary sensor that uses a I2C-HAT digital input.""" - - I2C_HATS_MANAGER: I2CHatsManager | None = None - - def __init__(self, address, channel, name, invert_logic, device_class): - """Initialize the raspihats sensor.""" - self._address = address - self._channel = channel - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._device_class = device_class - self._state = self.I2C_HATS_MANAGER.read_di(self._address, self._channel) - - def online_callback(): - """Call fired when board is online.""" - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_online_callback( - self._address, self._channel, online_callback - ) - - def edge_callback(state): - """Read digital input state.""" - self._state = state - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_di_callback( - self._address, self._channel, edge_callback - ) - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def name(self): - """Return the name of this sensor.""" - return self._name - - @property - def should_poll(self): - """No polling needed for this sensor.""" - return False - - @property - def is_on(self): - """Return the state of this sensor.""" - return self._state != self._invert_logic diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json deleted file mode 100644 index 984f440e064..00000000000 --- a/homeassistant/components/raspihats/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "raspihats", - "name": "Raspihats", - "documentation": "https://www.home-assistant.io/integrations/raspihats", - "requirements": ["raspihats==2.2.3", "smbus-cffi==0.5.1"], - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py deleted file mode 100644 index 8ca88528543..00000000000 --- a/homeassistant/components/raspihats/switch.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Support for raspihats board switches.""" -from __future__ import annotations - -import logging - -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ADDRESS, CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import ( - CONF_BOARD, - CONF_CHANNELS, - CONF_I2C_HATS, - CONF_INDEX, - CONF_INITIAL_STATE, - CONF_INVERT_LOGIC, - DOMAIN, - I2C_HAT_NAMES, - I2C_HATS_MANAGER, - I2CHatsException, - I2CHatsManager, -) - -_LOGGER = logging.getLogger(__name__) - -_CHANNELS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_INDEX): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean, - vol.Optional(CONF_INITIAL_STATE): cv.boolean, - } - ] -) - -_I2C_HATS_SCHEMA = vol.Schema( - [ - { - vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES), - vol.Required(CONF_ADDRESS): vol.Coerce(int), - vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA, - } - ] -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the raspihats switch devices.""" - I2CHatSwitch.I2C_HATS_MANAGER = hass.data[DOMAIN][I2C_HATS_MANAGER] - switches = [] - i2c_hat_configs = config.get(CONF_I2C_HATS, []) - for i2c_hat_config in i2c_hat_configs: - board = i2c_hat_config[CONF_BOARD] - address = i2c_hat_config[CONF_ADDRESS] - try: - assert I2CHatSwitch.I2C_HATS_MANAGER - I2CHatSwitch.I2C_HATS_MANAGER.register_board(board, address) - for channel_config in i2c_hat_config[CONF_CHANNELS]: - switches.append( - I2CHatSwitch( - board, - address, - channel_config[CONF_INDEX], - channel_config[CONF_NAME], - channel_config[CONF_INVERT_LOGIC], - channel_config.get(CONF_INITIAL_STATE), - ) - ) - except I2CHatsException as ex: - _LOGGER.error( - "Failed to register %s I2CHat@%s %s", board, hex(address), str(ex) - ) - add_entities(switches) - - -class I2CHatSwitch(SwitchEntity): - """Representation a switch that uses a I2C-HAT digital output.""" - - I2C_HATS_MANAGER: I2CHatsManager | None = None - - def __init__(self, board, address, channel, name, invert_logic, initial_state): - """Initialize switch.""" - self._board = board - self._address = address - self._channel = channel - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - if initial_state is not None: - if self._invert_logic: - state = not initial_state - else: - state = initial_state - self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - - def online_callback(): - """Call fired when board is online.""" - self.schedule_update_ha_state() - - self.I2C_HATS_MANAGER.register_online_callback( - self._address, self._channel, online_callback - ) - - def _log_message(self, message): - """Create log message.""" - string = f"{self._name} " - string += f"{self._board}I2CHat@{hex(self._address)} " - string += f"channel:{str(self._channel)}{message}" - return string - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - try: - state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) - return state != self._invert_logic - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Is ON check failed, {ex!s}")) - return False - - def turn_on(self, **kwargs): - """Turn the device on.""" - try: - state = self._invert_logic is False - self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - self.schedule_update_ha_state() - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Turn ON failed, {ex!s}")) - - def turn_off(self, **kwargs): - """Turn the device off.""" - try: - state = self._invert_logic is not False - self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) - self.schedule_update_ha_state() - except I2CHatsException as ex: - _LOGGER.error(self._log_message(f"Turn OFF failed, {ex!s}")) diff --git a/requirements_all.txt b/requirements_all.txt index 5b9948173f9..7a05d33c86c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2032,9 +2032,6 @@ radiotherm==2.1.0 # homeassistant.components.raincloud raincloudy==0.0.7 -# homeassistant.components.raspihats -# raspihats==2.2.3 - # homeassistant.components.raspyrfm raspyrfm-client==1.2.8 @@ -2159,9 +2156,6 @@ smart-meter-texas==0.4.7 # homeassistant.components.smarthab smarthab==0.21 -# homeassistant.components.raspihats -# smbus-cffi==0.5.1 - # homeassistant.components.smhi smhi-pkg==1.0.15 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 21e681a6939..de6e2229d1d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,9 +33,7 @@ COMMENT_REQUIREMENTS = ( "python-gammu", "python-lirc", "pyuserinput", - "raspihats", "RPi.GPIO", - "smbus-cffi", "tensorflow", "tf-models-official", ) From 0ed51dae13f08359e0b322740ab892b1a700b20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Mar 2022 00:48:12 +0100 Subject: [PATCH 0133/1054] Add Backup integration (#66395) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + homeassistant/components/backup/__init__.py | 26 +++ homeassistant/components/backup/const.py | 15 ++ homeassistant/components/backup/http.py | 49 +++++ homeassistant/components/backup/manager.py | 173 ++++++++++++++++++ homeassistant/components/backup/manifest.json | 17 ++ homeassistant/components/backup/websocket.py | 69 +++++++ .../components/default_config/__init__.py | 4 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/backup/__init__.py | 1 + tests/components/backup/common.py | 29 +++ tests/components/backup/test_http.py | 60 ++++++ tests/components/backup/test_init.py | 18 ++ tests/components/backup/test_manager.py | 153 ++++++++++++++++ tests/components/backup/test_websocket.py | 83 +++++++++ 16 files changed, 705 insertions(+) create mode 100644 homeassistant/components/backup/__init__.py create mode 100644 homeassistant/components/backup/const.py create mode 100644 homeassistant/components/backup/http.py create mode 100644 homeassistant/components/backup/manager.py create mode 100644 homeassistant/components/backup/manifest.json create mode 100644 homeassistant/components/backup/websocket.py create mode 100644 tests/components/backup/__init__.py create mode 100644 tests/components/backup/common.py create mode 100644 tests/components/backup/test_http.py create mode 100644 tests/components/backup/test_init.py create mode 100644 tests/components/backup/test_manager.py create mode 100644 tests/components/backup/test_websocket.py diff --git a/CODEOWNERS b/CODEOWNERS index 20ecc0272cf..ce1e69244d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -111,6 +111,8 @@ tests/components/azure_devops/* @timmo001 homeassistant/components/azure_event_hub/* @eavanvalkenburg tests/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_service_bus/* @hfurubotten +homeassistant/components/backup/* @home-assistant/core +tests/components/backup/* @home-assistant/core homeassistant/components/balboa/* @garbled1 tests/components/balboa/* @garbled1 homeassistant/components/beewi_smartclim/* @alemuro diff --git a/homeassistant/components/backup/__init__.py b/homeassistant/components/backup/__init__.py new file mode 100644 index 00000000000..711051507d4 --- /dev/null +++ b/homeassistant/components/backup/__init__.py @@ -0,0 +1,26 @@ +"""The Backup integration.""" +from homeassistant.components.hassio import is_hassio +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, LOGGER +from .http import async_register_http_views +from .manager import BackupManager +from .websocket import async_register_websocket_handlers + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Backup integration.""" + if is_hassio(hass): + LOGGER.error( + "The backup integration is not supported on this installation method, " + "please remove it from your configuration" + ) + return False + + hass.data[DOMAIN] = BackupManager(hass) + + async_register_websocket_handlers(hass) + async_register_http_views(hass) + + return True diff --git a/homeassistant/components/backup/const.py b/homeassistant/components/backup/const.py new file mode 100644 index 00000000000..a4a08fff75d --- /dev/null +++ b/homeassistant/components/backup/const.py @@ -0,0 +1,15 @@ +"""Constants for the Backup integration.""" +from logging import getLogger + +DOMAIN = "backup" +LOGGER = getLogger(__package__) + +EXCLUDE_FROM_BACKUP = [ + "__pycache__/*", + ".DS_Store", + "*.db-shm", + "*.log.*", + "*.log", + "backups/*.tar", + "OZW_Log.txt", +] diff --git a/homeassistant/components/backup/http.py b/homeassistant/components/backup/http.py new file mode 100644 index 00000000000..6f1f0d00167 --- /dev/null +++ b/homeassistant/components/backup/http.py @@ -0,0 +1,49 @@ +"""Http view for the Backup integration.""" +from __future__ import annotations + +from http import HTTPStatus + +from aiohttp.hdrs import CONTENT_DISPOSITION +from aiohttp.web import FileResponse, Request, Response + +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DOMAIN +from .manager import BackupManager + + +@callback +def async_register_http_views(hass: HomeAssistant) -> None: + """Register the http views.""" + hass.http.register_view(DownloadBackupView) + + +class DownloadBackupView(HomeAssistantView): + """Generate backup view.""" + + url = "/api/backup/download/{slug}" + name = "api:backup:download" + + async def get( # pylint: disable=no-self-use + self, + request: Request, + slug: str, + ) -> FileResponse | Response: + """Download a backup file.""" + if not request["hass_user"].is_admin: + return Response(status=HTTPStatus.UNAUTHORIZED) + + manager: BackupManager = request.app["hass"].data[DOMAIN] + backup = await manager.get_backup(slug) + + if backup is None or not backup.path.exists(): + return Response(status=HTTPStatus.NOT_FOUND) + + return FileResponse( + path=backup.path.as_posix(), + headers={ + CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar" + }, + ) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py new file mode 100644 index 00000000000..5861db1ad27 --- /dev/null +++ b/homeassistant/components/backup/manager.py @@ -0,0 +1,173 @@ +"""Backup manager for the Backup integration.""" +from __future__ import annotations + +from dataclasses import asdict, dataclass +import hashlib +import json +from pathlib import Path +from tarfile import TarError +from tempfile import TemporaryDirectory + +from securetar import SecureTarFile, atomic_contents_add + +from homeassistant.const import __version__ as HAVERSION +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt, json as json_util + +from .const import EXCLUDE_FROM_BACKUP, LOGGER + + +@dataclass +class Backup: + """Backup class.""" + + slug: str + name: str + date: str + path: Path + size: float + + def as_dict(self) -> dict: + """Return a dict representation of this backup.""" + return {**asdict(self), "path": self.path.as_posix()} + + +class BackupManager: + """Backup manager for the Backup integration.""" + + _backups: dict[str, Backup] = {} + _loaded = False + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the backup manager.""" + self.hass = hass + self.backup_dir = Path(hass.config.path("backups")) + self.backing_up = False + + async def load_backups(self) -> None: + """Load data of stored backup files.""" + backups = {} + + def _read_backups() -> None: + for backup_path in self.backup_dir.glob("*.tar"): + try: + with SecureTarFile(backup_path, "r", gzip=False) as backup_file: + if data_file := backup_file.extractfile("./backup.json"): + data = json.loads(data_file.read()) + backup = Backup( + slug=data["slug"], + name=data["name"], + date=data["date"], + path=backup_path, + size=round(backup_path.stat().st_size / 1_048_576, 2), + ) + backups[backup.slug] = backup + except (OSError, TarError, json.JSONDecodeError) as err: + LOGGER.warning("Unable to read backup %s: %s", backup_path, err) + + await self.hass.async_add_executor_job(_read_backups) + LOGGER.debug("Loaded %s backups", len(backups)) + self._backups = backups + self._loaded = True + + async def get_backups(self) -> dict[str, Backup]: + """Return backups.""" + if not self._loaded: + await self.load_backups() + + return self._backups + + async def get_backup(self, slug: str) -> Backup | None: + """Return a backup.""" + if not self._loaded: + await self.load_backups() + + if not (backup := self._backups.get(slug)): + return None + + if not backup.path.exists(): + LOGGER.debug( + "Removing tracked backup (%s) that does not exists on the expected path %s", + backup.slug, + backup.path, + ) + self._backups.pop(slug) + return None + + return backup + + async def remove_backup(self, slug: str) -> None: + """Remove a backup.""" + if (backup := await self.get_backup(slug)) is None: + return + + await self.hass.async_add_executor_job(backup.path.unlink, True) + LOGGER.debug("Removed backup located at %s", backup.path) + self._backups.pop(slug) + + async def generate_backup(self) -> Backup: + """Generate a backup.""" + if self.backing_up: + raise HomeAssistantError("Backup already in progress") + + try: + self.backing_up = True + backup_name = f"Core {HAVERSION}" + date_str = dt.now().isoformat() + slug = _generate_slug(date_str, backup_name) + + backup_data = { + "slug": slug, + "name": backup_name, + "date": date_str, + "type": "partial", + "folders": ["homeassistant"], + "homeassistant": {"version": HAVERSION}, + "compressed": True, + } + tar_file_path = Path(self.backup_dir, f"{slug}.tar") + + if not self.backup_dir.exists(): + LOGGER.debug("Creating backup directory") + self.hass.async_add_executor_job(self.backup_dir.mkdir) + + def _create_backup() -> None: + with TemporaryDirectory() as tmp_dir: + tmp_dir_path = Path(tmp_dir) + json_util.save_json( + tmp_dir_path.joinpath("./backup.json").as_posix(), + backup_data, + ) + with SecureTarFile(tar_file_path, "w", gzip=False) as tar_file: + with SecureTarFile( + tmp_dir_path.joinpath("./homeassistant.tar.gz").as_posix(), + "w", + ) as core_tar: + atomic_contents_add( + tar_file=core_tar, + origin_path=Path(self.hass.config.path()), + excludes=EXCLUDE_FROM_BACKUP, + arcname="data", + ) + tar_file.add(tmp_dir_path, arcname=".") + + await self.hass.async_add_executor_job(_create_backup) + backup = Backup( + slug=slug, + name=backup_name, + date=date_str, + path=tar_file_path, + size=round(tar_file_path.stat().st_size / 1_048_576, 2), + ) + if self._loaded: + self._backups[slug] = backup + LOGGER.debug("Generated new backup with slug %s", slug) + return backup + finally: + self.backing_up = False + + +def _generate_slug(date: str, name: str) -> str: + """Generate a backup slug.""" + return hashlib.sha1(f"{date} - {name}".lower().encode()).hexdigest()[:8] diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json new file mode 100644 index 00000000000..9ae12c4a9d7 --- /dev/null +++ b/homeassistant/components/backup/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "backup", + "name": "Backup", + "documentation": "https://www.home-assistant.io/integrations/backup", + "dependencies": [ + "http", + "websocket_api" + ], + "codeowners": [ + "@home-assistant/core" + ], + "requirements": [ + "securetar==2022.2.0" + ], + "iot_class": "calculated", + "quality_scale": "internal" +} \ No newline at end of file diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py new file mode 100644 index 00000000000..2f1381ce217 --- /dev/null +++ b/homeassistant/components/backup/websocket.py @@ -0,0 +1,69 @@ +"""Websocket commands for the Backup integration.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .manager import BackupManager + + +@callback +def async_register_websocket_handlers(hass: HomeAssistant) -> None: + """Register websocket commands.""" + websocket_api.async_register_command(hass, handle_info) + websocket_api.async_register_command(hass, handle_create) + websocket_api.async_register_command(hass, handle_remove) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/info"}) +@websocket_api.async_response +async def handle_info( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +): + """List all stored backups.""" + manager: BackupManager = hass.data[DOMAIN] + backups = await manager.get_backups() + connection.send_result( + msg["id"], + { + "backups": list(backups), + "backing_up": manager.backing_up, + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "backup/remove", + vol.Required("slug"): str, + } +) +@websocket_api.async_response +async def handle_remove( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +): + """Remove a backup.""" + manager: BackupManager = hass.data[DOMAIN] + await manager.remove_backup(msg["slug"]) + connection.send_result(msg["id"]) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required("type"): "backup/generate"}) +@websocket_api.async_response +async def handle_create( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict, +): + """Generate a backup.""" + manager: BackupManager = hass.data[DOMAIN] + backup = await manager.generate_backup() + connection.send_result(msg["id"], backup) diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index 9e6ab268172..574d97c6d29 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -5,6 +5,7 @@ try: except ImportError: av = None +from homeassistant.components.hassio import is_hassio from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -14,6 +15,9 @@ DOMAIN = "default_config" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize default configuration.""" + if not is_hassio(hass): + await async_setup_component(hass, "backup", config) + if av is None: return True diff --git a/requirements_all.txt b/requirements_all.txt index 7a05d33c86c..1ed574664f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2110,6 +2110,9 @@ screenlogicpy==0.5.4 # homeassistant.components.scsgate scsgate==0.1.0 +# homeassistant.components.backup +securetar==2022.2.0 + # homeassistant.components.sendgrid sendgrid==6.8.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 188a56c2aa4..4a406675913 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1338,6 +1338,9 @@ scapy==2.4.5 # homeassistant.components.screenlogic screenlogicpy==0.5.4 +# homeassistant.components.backup +securetar==2022.2.0 + # homeassistant.components.emulated_kasa # homeassistant.components.sense sense_energy==0.10.2 diff --git a/tests/components/backup/__init__.py b/tests/components/backup/__init__.py new file mode 100644 index 00000000000..0c5dcea461a --- /dev/null +++ b/tests/components/backup/__init__.py @@ -0,0 +1 @@ +"""Tests for the Backup integration.""" diff --git a/tests/components/backup/common.py b/tests/components/backup/common.py new file mode 100644 index 00000000000..824057b6500 --- /dev/null +++ b/tests/components/backup/common.py @@ -0,0 +1,29 @@ +"""Common helpers for the Backup integration tests.""" +from __future__ import annotations + +from pathlib import Path +from unittest.mock import patch + +from homeassistant.components.backup import DOMAIN +from homeassistant.components.backup.manager import Backup +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +TEST_BACKUP = Backup( + slug="abc123", + name="Test", + date="1970-01-01T00:00:00.000Z", + path=Path("abc123.tar"), + size=0.0, +) + + +async def setup_backup_integration( + hass: HomeAssistant, + with_hassio: bool = False, + configuration: ConfigType | None = None, +) -> bool: + """Set up the Backup integration.""" + with patch("homeassistant.components.backup.is_hassio", return_value=with_hassio): + return await async_setup_component(hass, DOMAIN, configuration or {}) diff --git a/tests/components/backup/test_http.py b/tests/components/backup/test_http.py new file mode 100644 index 00000000000..708abec057b --- /dev/null +++ b/tests/components/backup/test_http.py @@ -0,0 +1,60 @@ +"""Tests for the Backup integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from aiohttp import ClientSession, web + +from homeassistant.core import HomeAssistant + +from .common import TEST_BACKUP, setup_backup_integration + +from tests.common import MockUser + + +async def test_downloading_backup( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], +) -> None: + """Test downloading a backup file.""" + await setup_backup_integration(hass) + + client = await hass_client() + + with patch( + "homeassistant.components.backup.http.BackupManager.get_backup", + return_value=TEST_BACKUP, + ), patch("pathlib.Path.exists", return_value=True), patch( + "homeassistant.components.backup.http.FileResponse", + return_value=web.Response(text=""), + ): + + resp = await client.get("/api/backup/download/abc123") + assert resp.status == 200 + + +async def test_downloading_backup_not_found( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], +) -> None: + """Test downloading a backup file that does not exist.""" + await setup_backup_integration(hass) + + client = await hass_client() + + resp = await client.get("/api/backup/download/abc123") + assert resp.status == 404 + + +async def test_non_admin( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + hass_admin_user: MockUser, +) -> None: + """Test downloading a backup file that does not exist.""" + hass_admin_user.groups = [] + await setup_backup_integration(hass) + + client = await hass_client() + + resp = await client.get("/api/backup/download/abc123") + assert resp.status == 401 diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py new file mode 100644 index 00000000000..a9e2fe20c6b --- /dev/null +++ b/tests/components/backup/test_init.py @@ -0,0 +1,18 @@ +"""Tests for the Backup integration.""" +import pytest + +from homeassistant.core import HomeAssistant + +from .common import setup_backup_integration + + +async def test_setup_with_hassio( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the setup of the integration with hassio enabled.""" + assert not await setup_backup_integration(hass=hass, with_hassio=True) + assert ( + "The backup integration is not supported on this installation method, please remove it from your configuration" + in caplog.text + ) diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py new file mode 100644 index 00000000000..0c4b21746f0 --- /dev/null +++ b/tests/components/backup/test_manager.py @@ -0,0 +1,153 @@ +"""Tests for the Backup integration.""" +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.backup import BackupManager +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .common import TEST_BACKUP + + +async def test_constructor(hass: HomeAssistant) -> None: + """Test BackupManager constructor.""" + manager = BackupManager(hass) + assert manager.backup_dir.as_posix() == hass.config.path("backups") + + +async def test_load_backups(hass: HomeAssistant) -> None: + """Test loading backups.""" + manager = BackupManager(hass) + with patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), patch( + "tarfile.open", return_value=MagicMock() + ), patch( + "json.loads", + return_value={ + "slug": TEST_BACKUP.slug, + "name": TEST_BACKUP.name, + "date": TEST_BACKUP.date, + }, + ), patch( + "pathlib.Path.stat", return_value=MagicMock(st_size=TEST_BACKUP.size) + ): + await manager.load_backups() + backups = await manager.get_backups() + assert backups == {TEST_BACKUP.slug: TEST_BACKUP} + + +async def test_load_backups_with_exception( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading backups with exception.""" + manager = BackupManager(hass) + with patch("pathlib.Path.glob", return_value=[TEST_BACKUP.path]), patch( + "tarfile.open", side_effect=OSError("Test ecxeption") + ): + await manager.load_backups() + backups = await manager.get_backups() + assert f"Unable to read backup {TEST_BACKUP.path}: Test ecxeption" in caplog.text + assert backups == {} + + +async def test_removing_non_existing_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removing not existing backup.""" + manager = BackupManager(hass) + + await manager.remove_backup("non_existing") + assert "Removed backup located at" not in caplog.text + + +async def test_getting_backup_that_does_not_exist( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +): + """Test getting backup that does not exist.""" + manager = BackupManager(hass) + + with patch( + "homeassistant.components.backup.websocket.BackupManager._backups", + {TEST_BACKUP.slug: TEST_BACKUP}, + ), patch( + "homeassistant.components.backup.websocket.BackupManager._loaded", + True, + ), patch( + "pathlib.Path.exists", return_value=False + ): + backup = await manager.get_backup(TEST_BACKUP.slug) + assert backup is None + + assert ( + f"Removing tracked backup ({TEST_BACKUP.slug}) that " + f"does not exists on the expected path {TEST_BACKUP.path}" in caplog.text + ) + + +async def test_generate_backup_when_backing_up(hass: HomeAssistant) -> None: + """Test generate backup.""" + manager = BackupManager(hass) + manager.backing_up = True + with pytest.raises(HomeAssistantError, match="Backup already in progress"): + await manager.generate_backup() + + +async def test_generate_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test generate backup.""" + manager = BackupManager(hass) + + def _mock_iterdir(path: Path) -> list[Path]: + if not path.name.endswith("testing_config"): + return [] + return [ + Path("test.txt"), + Path(".DS_Store"), + Path(".storage"), + ] + + with patch("tarfile.open", MagicMock()) as mocked_tarfile, patch( + "pathlib.Path.iterdir", _mock_iterdir + ), patch("pathlib.Path.stat", MagicMock(st_size=123)), patch( + "pathlib.Path.is_file", lambda x: x.name != ".storage" + ), patch( + "pathlib.Path.is_dir", + lambda x: x.name == ".storage", + ), patch( + "pathlib.Path.exists", + lambda x: x != manager.backup_dir, + ), patch( + "pathlib.Path.is_symlink", + lambda _: False, + ), patch( + "pathlib.Path.mkdir", + MagicMock(), + ), patch( + "homeassistant.components.backup.manager.json_util.save_json" + ) as mocked_json_util, patch( + "homeassistant.components.backup.manager.HAVERSION", + "2025.1.0", + ), patch( + "homeassistant.components.backup.websocket.BackupManager._loaded", + True, + ): + await manager.generate_backup() + + assert mocked_json_util.call_count == 1 + assert mocked_json_util.call_args[0][1]["homeassistant"] == { + "version": "2025.1.0" + } + + assert ( + manager.backup_dir.as_posix() + in mocked_tarfile.call_args_list[0].kwargs["name"] + ) + + assert "Generated new backup with slug " in caplog.text + assert "Creating backup directory" in caplog.text diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py new file mode 100644 index 00000000000..7c2f9a8c752 --- /dev/null +++ b/tests/components/backup/test_websocket.py @@ -0,0 +1,83 @@ +"""Tests for the Backup integration.""" +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +from aiohttp import ClientWebSocketResponse +import pytest + +from homeassistant.core import HomeAssistant + +from .common import TEST_BACKUP, setup_backup_integration + + +async def test_info( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test getting backup info.""" + await setup_backup_integration(hass) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json({"id": 1, "type": "backup/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"backing_up": False, "backups": []} + + +async def test_remove( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removing a backup file.""" + await setup_backup_integration(hass) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.backup.websocket.BackupManager._backups", + {TEST_BACKUP.slug: TEST_BACKUP}, + ), patch( + "homeassistant.components.backup.websocket.BackupManager._loaded", + True, + ), patch( + "pathlib.Path.unlink" + ), patch( + "pathlib.Path.exists", return_value=True + ): + await client.send_json({"id": 1, "type": "backup/remove", "slug": "abc123"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert f"Removed backup located at {TEST_BACKUP.path}" in caplog.text + + +async def test_generate( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test removing a backup file.""" + await setup_backup_integration(hass) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.backup.websocket.BackupManager._backups", + {TEST_BACKUP.slug: TEST_BACKUP}, + ), patch( + "homeassistant.components.backup.websocket.BackupManager.generate_backup", + return_value=TEST_BACKUP, + ): + await client.send_json({"id": 1, "type": "backup/generate"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == TEST_BACKUP.as_dict() From e963ad96d46e9eb1d68b0b4689bc06b304a5fcdf Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 1 Mar 2022 00:23:47 +0000 Subject: [PATCH 0134/1054] [ci skip] Translation update --- .../components/cast/translations/zh-Hant.json | 4 ++-- .../components/deconz/translations/zh-Hant.json | 2 +- .../components/denonavr/translations/zh-Hant.json | 2 +- .../devolo_home_network/translations/zh-Hant.json | 2 +- .../components/dlna_dmr/translations/zh-Hant.json | 4 ++-- .../components/dlna_dms/translations/zh-Hant.json | 2 +- .../components/elgato/translations/zh-Hant.json | 2 +- .../components/elkm1/translations/zh-Hant.json | 4 ++-- .../components/flux_led/translations/zh-Hant.json | 2 +- .../components/fritz/translations/zh-Hant.json | 2 +- homeassistant/components/hassio/translations/ja.json | 2 +- homeassistant/components/hue/translations/zh-Hant.json | 2 +- homeassistant/components/knx/translations/zh-Hant.json | 4 ++-- .../components/kodi/translations/zh-Hant.json | 2 +- .../components/motion_blinds/translations/zh-Hant.json | 4 ++-- .../components/mqtt/translations/zh-Hant.json | 2 +- .../components/onvif/translations/zh-Hant.json | 2 +- homeassistant/components/ps4/translations/zh-Hant.json | 4 ++-- .../components/pure_energie/translations/zh-Hant.json | 2 +- homeassistant/components/rfxtrx/translations/no.json | 1 + homeassistant/components/sensibo/translations/ca.json | 5 ++++- homeassistant/components/sensibo/translations/de.json | 5 ++++- homeassistant/components/sensibo/translations/el.json | 5 ++++- homeassistant/components/sensibo/translations/et.json | 5 ++++- homeassistant/components/sensibo/translations/ja.json | 5 ++++- homeassistant/components/sensibo/translations/no.json | 5 ++++- homeassistant/components/sensibo/translations/pl.json | 5 ++++- .../components/sensibo/translations/pt-BR.json | 5 ++++- homeassistant/components/sensibo/translations/ru.json | 5 ++++- .../components/sensibo/translations/zh-Hant.json | 5 ++++- homeassistant/components/sleepiq/translations/ca.json | 10 +++++++++- homeassistant/components/sleepiq/translations/de.json | 10 +++++++++- homeassistant/components/sleepiq/translations/el.json | 10 +++++++++- homeassistant/components/sleepiq/translations/et.json | 10 +++++++++- homeassistant/components/sleepiq/translations/ja.json | 10 +++++++++- homeassistant/components/sleepiq/translations/no.json | 10 +++++++++- homeassistant/components/sleepiq/translations/pl.json | 10 +++++++++- .../components/sleepiq/translations/pt-BR.json | 10 +++++++++- homeassistant/components/sleepiq/translations/ru.json | 10 +++++++++- .../components/sleepiq/translations/zh-Hant.json | 10 +++++++++- .../components/smappee/translations/zh-Hant.json | 2 +- .../components/steamist/translations/zh-Hant.json | 2 +- .../components/tasmota/translations/zh-Hant.json | 2 +- .../components/tplink/translations/zh-Hant.json | 2 +- .../components/tuya/translations/zh-Hant.json | 2 +- .../components/unifiprotect/translations/zh-Hant.json | 2 +- .../components/volumio/translations/zh-Hant.json | 2 +- homeassistant/components/wiz/translations/zh-Hant.json | 2 +- .../components/wled/translations/zh-Hant.json | 2 +- .../components/xiaomi_aqara/translations/zh-Hant.json | 6 +++--- .../components/yeelight/translations/zh-Hant.json | 2 +- .../components/zwave_js/translations/zh-Hant.json | 6 +++--- 52 files changed, 172 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index cc538845603..58972071cbf 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -11,7 +11,7 @@ "data": { "known_hosts": "\u5df2\u77e5\u4e3b\u6a5f" }, - "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", + "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u641c\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", "title": "Google Cast \u8a2d\u5b9a" }, "confirm": { @@ -36,7 +36,7 @@ "data": { "known_hosts": "\u5df2\u77e5\u4e3b\u6a5f" }, - "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u63a2\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", + "description": "\u5df2\u77e5\u4e3b\u6a5f - \u4ee5\u9017\u865f\u5206\u9694\u7684 Chromecast \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31 hostnames \u6216 IP \u4f4d\u5740\u3001\u5047\u5982 mDNS \u641c\u7d22\u5931\u6548\u7684\u72c0\u6cc1\u3002", "title": "Google Cast \u8a2d\u5b9a" } } diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index b93cb320993..64817192c64 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", + "no_bridges": "\u672a\u767c\u73fe\u5230 deCONZ Bridfe", "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u88dd\u7f6e\u9023\u7dda", "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u88dd\u7f6e" diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index a8ee7f87fd8..073de46866d 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", - "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", + "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u767c\u73fe\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", "not_denonavr_missing": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u63a2\u7d22\u8cc7\u8a0a\u4e0d\u5b8c\u6574" }, "error": { diff --git a/homeassistant/components/devolo_home_network/translations/zh-Hant.json b/homeassistant/components/devolo_home_network/translations/zh-Hant.json index 17eb11eb070..bccc6aa24e3 100644 --- a/homeassistant/components/devolo_home_network/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_network/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07\u4e3b\u6a5f\u540d\u7a31\u70ba `{host_name}` \u7684 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230 Devolo \u667a\u80fd\u5bb6\u5ead\u7db2\u8def\u88dd\u7f6e" } } } diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index 406b23b573f..f085767565d 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -5,7 +5,7 @@ "alternative_integration": "\u4f7f\u7528\u5176\u4ed6\u6574\u5408\u4ee5\u53d6\u5f97\u66f4\u4f73\u7684\u88dd\u7f6e\u652f\u63f4", "cannot_connect": "\u9023\u7dda\u5931\u6557", "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", - "discovery_error": "DLNA \u88dd\u7f6e\u63a2\u7d22\u5931\u6557", + "discovery_error": "DLNA \u88dd\u7f6e\u641c\u7d22\u5931\u6557", "incomplete_config": "\u6240\u7f3a\u5c11\u7684\u8a2d\u5b9a\u70ba\u5fc5\u9808\u8b8a\u6578", "non_unique_id": "\u627e\u5230\u591a\u7d44\u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u552f\u4e00 ID", "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" @@ -36,7 +36,7 @@ "url": "\u7db2\u5740" }, "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", - "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMR \u88dd\u7f6e" + "title": "\u5df2\u767c\u73fe\u7684 DLNA DMR \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/dlna_dms/translations/zh-Hant.json b/homeassistant/components/dlna_dms/translations/zh-Hant.json index 2f06619c006..1dc212f19ee 100644 --- a/homeassistant/components/dlna_dms/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dms/translations/zh-Hant.json @@ -17,7 +17,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", - "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMA \u88dd\u7f6e" + "title": "\u5df2\u767c\u73fe\u7684 DLNA DMA \u88dd\u7f6e" } } } diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 9d6bd9ab761..7e51ce29807 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato \u7167\u660e\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato \u7167\u660e\u88dd\u7f6e" + "title": "\u6240\u767c\u73fe\u7684 Elgato \u7167\u660e\u88dd\u7f6e" } } } diff --git a/homeassistant/components/elkm1/translations/zh-Hant.json b/homeassistant/components/elkm1/translations/zh-Hant.json index 7b25413c6fc..543f93626ce 100644 --- a/homeassistant/components/elkm1/translations/zh-Hant.json +++ b/homeassistant/components/elkm1/translations/zh-Hant.json @@ -20,7 +20,7 @@ "temperature_unit": "ElkM1 \u6240\u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u9023\u7dda\u81f3\u6240\u63a2\u7d22\u7684\u7cfb\u7d71\uff1a{mac_address} ({host})", + "description": "\u9023\u7dda\u81f3\u6240\u767c\u73fe\u7684\u7cfb\u7d71\uff1a{mac_address} ({host})", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" }, "manual_connection": { @@ -45,7 +45,7 @@ "temperature_unit": "ElkM1 \u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684\u7cfb\u7d71\uff0c\u6216\u5047\u5982\u6c92\u627e\u5230\u7684\u8a71\u9032\u884c\u624b\u52d5\u8f38\u5165\u3002", + "description": "\u9078\u64c7\u6240\u767c\u73fe\u5230\u7684\u7cfb\u7d71\uff0c\u6216\u5047\u5982\u6c92\u627e\u5230\u7684\u8a71\u9032\u884c\u624b\u52d5\u8f38\u5165\u3002", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" } } diff --git a/homeassistant/components/flux_led/translations/zh-Hant.json b/homeassistant/components/flux_led/translations/zh-Hant.json index 4e14b58ff18..789a5aad6e8 100644 --- a/homeassistant/components/flux_led/translations/zh-Hant.json +++ b/homeassistant/components/flux_led/translations/zh-Hant.json @@ -17,7 +17,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } }, diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index a57e0125a49..cb03aa8606c 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -57,7 +57,7 @@ "init": { "data": { "consider_home": "\u8996\u70ba\u5728\u5bb6\u7684\u7b49\u5019\u79d2\u6578", - "old_discovery": "\u958b\u555f\u820a\u63a2\u7d22\u6a21\u5f0f" + "old_discovery": "\u958b\u555f\u820a\u641c\u7d22\u6a21\u5f0f" } } } diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index dbf79471303..da3409d91cd 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -10,7 +10,7 @@ "installed_addons": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u306e\u30a2\u30c9\u30aa\u30f3", "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", - "supported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "supported": "\u30b5\u30dd\u30fc\u30c8", "update_channel": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30c1\u30e3\u30f3\u30cd\u30eb", "version_api": "\u30d0\u30fc\u30b8\u30e7\u30f3API" } diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index d607baf27b5..6c37c699340 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -6,7 +6,7 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", - "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", + "no_bridges": "\u672a\u767c\u73fe\u5230 Philips Hue Bridge", "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 27b167c6551..cc9d29714b6 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -49,8 +49,8 @@ "connection_type": "KNX \u9023\u7dda\u985e\u578b", "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u586b\u5165 0.0.0.0 \u555f\u7528\u81ea\u52d5\u5075\u6e2c\uff09", - "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u7fa4\u7d44", - "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u63a2\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", + "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u7fa4\u7d44", + "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", "rate_limit": "\u6700\u5927\u6bcf\u79d2\u767c\u51fa Telegram", "state_updater": "\u7531 KNX Bus \u8b80\u53d6\u72c0\u614b\u5168\u555f\u7528" } diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 32ecb4164d5..169ad92e96b 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -23,7 +23,7 @@ }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Kodi (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u641c\u7d22\u5230\u7684 Kodi" + "title": "\u5df2\u767c\u73fe\u7684 Kodi" }, "user": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 7aeb111ed3f..df300e0511f 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -6,7 +6,7 @@ "connection_error": "\u9023\u7dda\u5931\u6557" }, "error": { - "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", + "discovery_error": "\u641c\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548" }, "flow_title": "Motion Blinds", @@ -31,7 +31,7 @@ "api_key": "API \u91d1\u9470", "host": "IP \u4f4d\u5740" }, - "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22", + "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u641c\u7d22", "title": "Motion Blinds" } } diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 43d6a5f0b4e..b34b4813499 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -80,7 +80,7 @@ "will_retain": "Will \u8a0a\u606f Retain", "will_topic": "Will \u8a0a\u606f\u4e3b\u984c" }, - "description": "Discovery - \u5047\u5982\u63a2\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u63a2\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", + "description": "Discovery - \u5047\u5982\u641c\u7d22\uff08Discovery\uff09\u529f\u80fd\u958b\u555f\uff08\u5efa\u8b70\uff09\uff0cHome Assistant \u5c07\u6703\u81ea\u52d5\u767c\u73fe\u88dd\u7f6e\u8207\u5be6\u9ad4\u3001\u4e26\u767c\u5e03\u5176\u8a2d\u5b9a\u81f3 MQTT Broker\u3002\u5047\u5982\u641c\u7d22\u95dc\u9589\u7684\u8a71\uff0c\u6240\u6709\u8a2d\u5b9a\u5fc5\u9808\u624b\u52d5\u9032\u884c\u3002\nBirth \u8a0a\u606f - Birth \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u9023\u7dda\u81f3 MQTT Broker \u6642\u50b3\u9001\u3002\nWill \u8a0a\u606f - Will \u8a0a\u606f\u5c07\u6703\u65bc\u6bcf\u6b21 Home Assistant \u81ea Broker \u65b7\u7dda\u6642\u50b3\u9001\u3001\u540c\u6642\u5305\u542b\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u95dc\u6a5f\uff09\u53ca\u975e\u5b89\u5168\u65b7\u7dda\uff08\u4f8b\u5982 Home Assistant \u7576\u6a5f\u6216\u65b7\u7dda\uff09\u72c0\u6cc1\u3002", "title": "MQTT \u9078\u9805" } } diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index fa4d7d632da..23d0f2d12f0 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -37,7 +37,7 @@ }, "device": { "data": { - "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u88dd\u7f6e" + "host": "\u9078\u64c7\u6240\u767c\u73fe\u7684 ONVIF \u88dd\u7f6e" }, "title": "\u9078\u64c7 ONVIF \u88dd\u7f6e" }, diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index 4475700481a..e9c8dac2a26 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -30,10 +30,10 @@ }, "mode": { "data": { - "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", + "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u641c\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pure_energie/translations/zh-Hant.json b/homeassistant/components/pure_energie/translations/zh-Hant.json index 7fa7144f914..56235b16952 100644 --- a/homeassistant/components/pure_energie/translations/zh-Hant.json +++ b/homeassistant/components/pure_energie/translations/zh-Hant.json @@ -16,7 +16,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Pure Energie Meter (`{model}`) \u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Pure Energie Meter \u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230 Pure Energie Meter \u88dd\u7f6e" } } } diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 2f867554442..62a658d4067 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -61,6 +61,7 @@ "debug": "Aktiver feils\u00f8king", "device": "Velg enhet du vil konfigurere", "event_code": "Angi hendelseskode for \u00e5 legge til", + "protocols": "Protokoller", "remove_device": "Velg enhet du vil slette" }, "title": "[%key:component::rfxtrx::title%] alternativer" diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index adff28113ca..72893fb79e5 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -4,7 +4,10 @@ "already_configured": "El compte ja est\u00e0 configurat" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_devices": "No s'ha descobert cap dispositiu", + "no_username": "No s'ha pogut obtenir el nom d'usuari" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index 5bb5dd3cf84..ffcdbc66036 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -4,7 +4,10 @@ "already_configured": "Konto wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_devices": "Keine Ger\u00e4te gefunden", + "no_username": "Benutzername konnte nicht ermittelt werden" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 455c89deba4..1e92956c738 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -4,7 +4,10 @@ "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "no_devices": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2", + "no_username": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index ccee8f1c41d..276ad37bc8c 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -4,7 +4,10 @@ "already_configured": "Konto on juba h\u00e4\u00e4lestatud" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "no_devices": "\u00dchtegi seadet ei leitud", + "no_username": "Kasutajanime ei \u00f5nnestunud hankida" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index 92e6e077f51..1913198827c 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -4,7 +4,10 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_devices": "\u30c7\u30d0\u30a4\u30b9\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", + "no_username": "\u30e6\u30fc\u30b6\u30fc\u540d\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 6c2c4613d0a..117ec5951ce 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -4,7 +4,10 @@ "already_configured": "Kontoen er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "no_devices": "Ingen enheter oppdaget", + "no_username": "Kunne ikke hente brukernavn" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index a65564329d3..fcbf9a20a6e 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -4,7 +4,10 @@ "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_devices": "Nie wykryto urz\u0105dze\u0144", + "no_username": "Nie uda\u0142o si\u0119 uzyska\u0107 nazwy u\u017cytkownika" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index fac1d04755f..e4b8509ede7 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -4,7 +4,10 @@ "already_configured": "A conta j\u00e1 foi configurada" }, "error": { - "cannot_connect": "Falha ao conectar" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices": "Nenhum dispositivo descoberto", + "no_username": "N\u00e3o foi poss\u00edvel obter o nome de usu\u00e1rio" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index 0d6a22bde74..bf8aa9660e7 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -4,7 +4,10 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", + "no_username": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f." }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 4abe3544048..9977208f646 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -4,7 +4,10 @@ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_devices": "\u672a\u767c\u73fe\u4efb\u4f55\u88dd\u7f6e", + "no_username": "\u7121\u6cd5\u53d6\u5f97\u4f7f\u7528\u8005\u540d\u7a31" }, "step": { "user": { diff --git a/homeassistant/components/sleepiq/translations/ca.json b/homeassistant/components/sleepiq/translations/ca.json index 9c37f37a0ef..4b42b0411e9 100644 --- a/homeassistant/components/sleepiq/translations/ca.json +++ b/homeassistant/components/sleepiq/translations/ca.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La integraci\u00f3 SleepIQ ha de tornar a autenticar-se amb el teu compte {username}.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/sleepiq/translations/de.json b/homeassistant/components/sleepiq/translations/de.json index 12a870b4cc9..280b0bf2ba7 100644 --- a/homeassistant/components/sleepiq/translations/de.json +++ b/homeassistant/components/sleepiq/translations/de.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Die SleepIQ-Integration muss dein Konto {username} erneut authentifizieren.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/sleepiq/translations/el.json b/homeassistant/components/sleepiq/translations/el.json index 45b5ef57ba5..fff470c0ec1 100644 --- a/homeassistant/components/sleepiq/translations/el.json +++ b/homeassistant/components/sleepiq/translations/el.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 SleepIQ \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 {username}.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/sleepiq/translations/et.json b/homeassistant/components/sleepiq/translations/et.json index db09683450a..89cc0a85714 100644 --- a/homeassistant/components/sleepiq/translations/et.json +++ b/homeassistant/components/sleepiq/translations/et.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "SleepIQ sidumine peab konto {username} uuesti autentima.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/sleepiq/translations/ja.json b/homeassistant/components/sleepiq/translations/ja.json index 35b6807586d..b1e0ec1d86e 100644 --- a/homeassistant/components/sleepiq/translations/ja.json +++ b/homeassistant/components/sleepiq/translations/ja.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "SleepIQ\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/sleepiq/translations/no.json b/homeassistant/components/sleepiq/translations/no.json index 51f351fb833..ffe74f7048a 100644 --- a/homeassistant/components/sleepiq/translations/no.json +++ b/homeassistant/components/sleepiq/translations/no.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "SleepIQ-integrasjonen m\u00e5 autentisere kontoen din {username} p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/sleepiq/translations/pl.json b/homeassistant/components/sleepiq/translations/pl.json index 49be6d1efde..ae26dfb0167 100644 --- a/homeassistant/components/sleepiq/translations/pl.json +++ b/homeassistant/components/sleepiq/translations/pl.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Integracja SleepIQ wymaga ponownego uwierzytelnienia Twojego konta {username}.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/sleepiq/translations/pt-BR.json b/homeassistant/components/sleepiq/translations/pt-BR.json index 86cf9781d3a..99ed7e0dd88 100644 --- a/homeassistant/components/sleepiq/translations/pt-BR.json +++ b/homeassistant/components/sleepiq/translations/pt-BR.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 foi configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A integra\u00e7\u00e3o do SleepIQ precisa autenticar novamente sua conta {username} .", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/sleepiq/translations/ru.json b/homeassistant/components/sleepiq/translations/ru.json index f74355cdc7d..1cf8dfe0e60 100644 --- a/homeassistant/components/sleepiq/translations/ru.json +++ b/homeassistant/components/sleepiq/translations/ru.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SleepIQ {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/sleepiq/translations/zh-Hant.json b/homeassistant/components/sleepiq/translations/zh-Hant.json index d93bfe0fa68..193f9a5d96f 100644 --- a/homeassistant/components/sleepiq/translations/zh-Hant.json +++ b/homeassistant/components/sleepiq/translations/zh-Hant.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "SleepIQ \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f {username}\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 530dddbb7a4..98c8deb1ac3 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u88dd\u7f6e" + "title": "\u6240\u767c\u73fe\u7684 Smappee \u88dd\u7f6e" } } } diff --git a/homeassistant/components/steamist/translations/zh-Hant.json b/homeassistant/components/steamist/translations/zh-Hant.json index c3d9ad377b7..3820aa5dcf2 100644 --- a/homeassistant/components/steamist/translations/zh-Hant.json +++ b/homeassistant/components/steamist/translations/zh-Hant.json @@ -25,7 +25,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index 3a11beed839..e823937f972 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "invalid_discovery_topic": "\u63a2\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" + "invalid_discovery_topic": "\u641c\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" }, "step": { "config": { diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index bfca7643b32..31bfeea42a2 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -25,7 +25,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index b905eb0c1e3..fe4d2ed2770 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -70,7 +70,7 @@ }, "init": { "data": { - "discovery_interval": "\u63a2\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", + "discovery_interval": "\u641c\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index ab63b89075e..33a447e9f4c 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "discovery_started": "\u958b\u59cb\u63a2\u7d22" + "discovery_started": "\u958b\u59cb\u641c\u7d22" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/volumio/translations/zh-Hant.json b/homeassistant/components/volumio/translations/zh-Hant.json index f792fd85465..79edffd79aa 100644 --- a/homeassistant/components/volumio/translations/zh-Hant.json +++ b/homeassistant/components/volumio/translations/zh-Hant.json @@ -11,7 +11,7 @@ "step": { "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Volumio (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u641c\u7d22\u5230\u7684 Volumio" + "title": "\u5df2\u767c\u73fe\u7684 Volumio" }, "user": { "data": { diff --git a/homeassistant/components/wiz/translations/zh-Hant.json b/homeassistant/components/wiz/translations/zh-Hant.json index b677427996f..9e716b4928e 100644 --- a/homeassistant/components/wiz/translations/zh-Hant.json +++ b/homeassistant/components/wiz/translations/zh-Hant.json @@ -30,7 +30,7 @@ "host": "IP \u4f4d\u5740", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 69f6476a768..6125c3a2cb5 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u88dd\u7f6e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u88dd\u7f6e" + "title": "\u6240\u767c\u73fe\u7684 WLED \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index dcc7b76a0c4..058efd39631 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -3,10 +3,10 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" + "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u641c\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" }, "error": { - "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", + "discovery_error": "\u641c\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", "invalid_key": "\u7db2\u95dc\u91d1\u9470\u7121\u6548", @@ -35,7 +35,7 @@ "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762", "mac": "Mac \u4f4d\u5740\uff08\u9078\u9805\uff09" }, - "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u63a2\u7d22", + "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u641c\u7d22", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc" } } diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index 785584a6f2a..7601a0b9552 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -21,7 +21,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } }, diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 9dd44c4457f..b16f313fbd4 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u641c\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", @@ -9,7 +9,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "discovery_requires_supervisor": "\u63a2\u7d22\u529f\u80fd\u9700\u8981 Supervisor \u6b0a\u9650\u3002", + "discovery_requires_supervisor": "\u641c\u7d22\u529f\u80fd\u9700\u8981 Supervisor \u6b0a\u9650\u3002", "not_zwave_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Z-Wave \u88dd\u7f6e" }, "error": { @@ -90,7 +90,7 @@ }, "options": { "abort": { - "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u641c\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", From aea1209d0d7461fe6eaff5367937f483d7689bc6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Feb 2022 18:38:08 -0600 Subject: [PATCH 0135/1054] Reduce magic in Sonos error handling fixture (#67401) --- homeassistant/components/sonos/helpers.py | 32 ++++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index fbc1d2642ea..a11847d2b0c 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import TYPE_CHECKING, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException @@ -55,15 +55,9 @@ def soco_error( ) return None - # In order of preference: - # * SonosSpeaker instance - # * SoCo instance passed as an arg - # * SoCo instance (as self) - speaker_or_soco = getattr(self, "speaker", args_soco or self) - zone_name = speaker_or_soco.zone_name - # Prefer the entity_id if available, zone name as a fallback - # Needed as SonosSpeaker instances are not entities - target = getattr(self, "entity_id", zone_name) + if (target := _find_target_identifier(self, args_soco)) is None: + raise RuntimeError("Unexpected use of soco_error") from err + message = f"Error calling {function} on {target}: {err}" raise SonosUpdateError(message) from err @@ -80,6 +74,24 @@ def soco_error( return decorator +def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | None: + """Extract the the best available target identifier from the provided instance object.""" + if entity_id := getattr(instance, "entity_id", None): + # SonosEntity instance + return entity_id + if zone_name := getattr(instance, "zone_name", None): + # SonosSpeaker instance + return zone_name + if speaker := getattr(instance, "speaker", None): + # Holds a SonosSpeaker instance attribute + return speaker.zone_name + if soco := getattr(instance, "soco", fallback_soco): + # Holds a SoCo instance attribute + # Only use attributes with no I/O + return soco._player_name or soco.ip_address # pylint: disable=protected-access + return None + + def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" if hostname.startswith("Sonos-"): From 21ce441a97126a89d2377a8f250c0fe013e91770 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Feb 2022 14:39:13 -1000 Subject: [PATCH 0136/1054] Bump zeroconf to 0.38.4 (#67406) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index fa3b8688c47..dc4c7c001ae 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.3"], + "requirements": ["zeroconf==0.38.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a333d603a33..98dd11bbe73 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.12.2 yarl==1.7.2 -zeroconf==0.38.3 +zeroconf==0.38.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 1ed574664f1..82fdc1b25a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2467,7 +2467,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.3 +zeroconf==0.38.4 # homeassistant.components.zha zha-quirks==0.0.67 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a406675913..e5ff3c95388 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1566,7 +1566,7 @@ yeelight==0.7.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.3 +zeroconf==0.38.4 # homeassistant.components.zha zha-quirks==0.0.67 From 076fe971109200e5c8f3fb1ce116492d5672edc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Feb 2022 18:49:44 -1000 Subject: [PATCH 0137/1054] Strict typing for dhcp (#67361) --- .strict-typing | 1 + homeassistant/components/dhcp/__init__.py | 170 +++++++++++++--------- homeassistant/loader.py | 24 ++- mypy.ini | 11 ++ 4 files changed, 133 insertions(+), 73 deletions(-) diff --git a/.strict-typing b/.strict-typing index f6908fc39a6..b1f6db6a532 100644 --- a/.strict-typing +++ b/.strict-typing @@ -68,6 +68,7 @@ homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* homeassistant.components.devolo_home_network.* +homeassistant.components.dhcp.* homeassistant.components.dlna_dmr.* homeassistant.components.dnsip.* homeassistant.components.dsmr.* diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 0b5f8a49a34..1756f620f46 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -2,6 +2,9 @@ from __future__ import annotations from abc import abstractmethod +import asyncio +from collections.abc import Callable, Iterable +import contextlib from dataclasses import dataclass from datetime import timedelta import fnmatch @@ -9,7 +12,7 @@ from ipaddress import ip_address as make_ip_address import logging import os import threading -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final, cast from aiodiscover import DiscoverHosts from aiodiscover.discovery import ( @@ -51,12 +54,16 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_dhcp +from homeassistant.loader import DHCPMatcher, async_get_dhcp from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.network import is_invalid, is_link_local, is_loopback from .const import DOMAIN +if TYPE_CHECKING: + from scapy.packet import Packet + from scapy.sendrecv import AsyncSniffer + FILTER = "udp and (port 67 or 68)" REQUESTED_ADDR = "requested_addr" MESSAGE_TYPE = "message-type" @@ -115,7 +122,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: watchers: list[WatcherBase] = [] address_data: dict[str, dict[str, str]] = {} integration_matchers = await async_get_dhcp(hass) - # For the passive classes we need to start listening # for state changes and connect the dispatchers before # everything else starts up or we will miss events @@ -124,13 +130,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await passive_watcher.async_start() watchers.append(passive_watcher) - async def _initialize(_): + async def _initialize(event: Event) -> None: for active_cls in (DHCPWatcher, NetworkWatcher): active_watcher = active_cls(hass, address_data, integration_matchers) await active_watcher.async_start() watchers.append(active_watcher) - async def _async_stop(*_): + async def _async_stop(event: Event) -> None: for watcher in watchers: await watcher.async_stop() @@ -143,7 +149,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class WatcherBase: """Base class for dhcp and device tracker watching.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__() @@ -152,11 +163,11 @@ class WatcherBase: self._address_data = address_data @abstractmethod - async def async_stop(self): + async def async_stop(self) -> None: """Stop the watcher.""" @abstractmethod - async def async_start(self): + async def async_start(self) -> None: """Start the watcher.""" def process_client(self, ip_address: str, hostname: str, mac_address: str) -> None: @@ -197,8 +208,8 @@ class WatcherBase: data = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} self._address_data[ip_address] = data - lowercase_hostname = data[HOSTNAME].lower() - uppercase_mac = data[MAC_ADDRESS].upper() + lowercase_hostname = hostname.lower() + uppercase_mac = mac_address.upper() _LOGGER.debug( "Processing updated address data for %s: mac=%s hostname=%s", @@ -218,22 +229,24 @@ class WatcherBase: if entry := self.hass.config_entries.async_get_entry(entry_id): device_domains.add(entry.domain) - for entry in self._integration_matchers: - if entry.get(REGISTERED_DEVICES) and not entry["domain"] in device_domains: + for matcher in self._integration_matchers: + domain = matcher["domain"] + + if matcher.get(REGISTERED_DEVICES) and domain not in device_domains: continue - if MAC_ADDRESS in entry and not fnmatch.fnmatch( - uppercase_mac, entry[MAC_ADDRESS] - ): + if ( + matcher_mac := matcher.get(MAC_ADDRESS) + ) is not None and not fnmatch.fnmatch(uppercase_mac, matcher_mac): continue - if HOSTNAME in entry and not fnmatch.fnmatch( - lowercase_hostname, entry[HOSTNAME] - ): + if ( + matcher_hostname := matcher.get(HOSTNAME) + ) is not None and not fnmatch.fnmatch(lowercase_hostname, matcher_hostname): continue - _LOGGER.debug("Matched %s against %s", data, entry) - matched_domains.add(entry["domain"]) + _LOGGER.debug("Matched %s against %s", data, matcher) + matched_domains.add(domain) for domain in matched_domains: discovery_flow.async_create_flow( @@ -243,7 +256,7 @@ class WatcherBase: DhcpServiceInfo( ip=ip_address, hostname=lowercase_hostname, - macaddress=data[MAC_ADDRESS], + macaddress=mac_address, ), ) @@ -251,14 +264,19 @@ class WatcherBase: class NetworkWatcher(WatcherBase): """Class to query ptr records routers.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub = None - self._discover_hosts = None - self._discover_task = None + self._unsub: Callable[[], None] | None = None + self._discover_hosts: DiscoverHosts | None = None + self._discover_task: asyncio.Task | None = None - async def async_stop(self): + async def async_stop(self) -> None: """Stop scanning for new devices on the network.""" if self._unsub: self._unsub() @@ -267,7 +285,7 @@ class NetworkWatcher(WatcherBase): self._discover_task.cancel() self._discover_task = None - async def async_start(self): + async def async_start(self) -> None: """Start scanning for new devices on the network.""" self._discover_hosts = DiscoverHosts() self._unsub = async_track_time_interval( @@ -276,14 +294,15 @@ class NetworkWatcher(WatcherBase): self.async_start_discover() @callback - def async_start_discover(self, *_): + def async_start_discover(self, *_: Any) -> None: """Start a new discovery task if one is not running.""" if self._discover_task and not self._discover_task.done(): return self._discover_task = self.hass.async_create_task(self.async_discover()) - async def async_discover(self): + async def async_discover(self) -> None: """Process discovery.""" + assert self._discover_hosts is not None for host in await self._discover_hosts.async_discover(): self.async_process_client( host[DISCOVERY_IP_ADDRESS], @@ -295,18 +314,23 @@ class NetworkWatcher(WatcherBase): class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub = None + self._unsub: Callable[[], None] | None = None - async def async_stop(self): + async def async_stop(self) -> None: """Stop watching for new device trackers.""" if self._unsub: self._unsub() self._unsub = None - async def async_start(self): + async def async_start(self) -> None: """Stop watching for new device trackers.""" self._unsub = async_track_state_added_domain( self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event @@ -315,12 +339,12 @@ class DeviceTrackerWatcher(WatcherBase): self._async_process_device_state(state) @callback - def _async_process_device_event(self, event: Event): + def _async_process_device_event(self, event: Event) -> None: """Process a device tracker state change event.""" self._async_process_device_state(event.data["new_state"]) @callback - def _async_process_device_state(self, state: State): + def _async_process_device_state(self, state: State) -> None: """Process a device tracker state.""" if state.state != STATE_HOME: return @@ -343,18 +367,23 @@ class DeviceTrackerWatcher(WatcherBase): class DeviceTrackerRegisteredWatcher(WatcherBase): """Class to watch data from device tracker registrations.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._unsub = None + self._unsub: Callable[[], None] | None = None - async def async_stop(self): + async def async_stop(self) -> None: """Stop watching for device tracker registrations.""" if self._unsub: self._unsub() self._unsub = None - async def async_start(self): + async def async_start(self) -> None: """Stop watching for device tracker registrations.""" self._unsub = async_dispatcher_connect( self.hass, CONNECTED_DEVICE_REGISTERED, self._async_process_device_data @@ -376,26 +405,32 @@ class DeviceTrackerRegisteredWatcher(WatcherBase): class DHCPWatcher(WatcherBase): """Class to watch dhcp requests.""" - def __init__(self, hass, address_data, integration_matchers): + def __init__( + self, + hass: HomeAssistant, + address_data: dict[str, dict[str, str]], + integration_matchers: list[DHCPMatcher], + ) -> None: """Initialize class.""" super().__init__(hass, address_data, integration_matchers) - self._sniffer = None + self._sniffer: AsyncSniffer | None = None self._started = threading.Event() - async def async_stop(self): + async def async_stop(self) -> None: """Stop watching for new device trackers.""" await self.hass.async_add_executor_job(self._stop) - def _stop(self): + def _stop(self) -> None: """Stop the thread.""" if self._started.is_set(): + assert self._sniffer is not None self._sniffer.stop() - async def async_start(self): + async def async_start(self) -> None: """Start watching for dhcp packets.""" await self.hass.async_add_executor_job(self._start) - def _start(self): + def _start(self) -> None: """Start watching for dhcp packets.""" # Local import because importing from scapy has side effects such as opening # sockets @@ -417,20 +452,25 @@ class DHCPWatcher(WatcherBase): AsyncSniffer, ) - def _handle_dhcp_packet(packet): + def _handle_dhcp_packet(packet: Packet) -> None: """Process a dhcp packet.""" if DHCP not in packet: return - options = packet[DHCP].options - request_type = _decode_dhcp_option(options, MESSAGE_TYPE) - if request_type != DHCP_REQUEST: + options_dict = _dhcp_options_as_dict(packet[DHCP].options) + if options_dict.get(MESSAGE_TYPE) != DHCP_REQUEST: # Not a DHCP request return - ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) or packet[IP].src - hostname = _decode_dhcp_option(options, HOSTNAME) or "" - mac_address = _format_mac(packet[Ether].src) + ip_address = options_dict.get(REQUESTED_ADDR) or cast(str, packet[IP].src) + assert isinstance(ip_address, str) + hostname = "" + if (hostname_bytes := options_dict.get(HOSTNAME)) and isinstance( + hostname_bytes, bytes + ): + with contextlib.suppress(AttributeError, UnicodeDecodeError): + hostname = hostname_bytes.decode() + mac_address = _format_mac(cast(str, packet[Ether].src)) if ip_address is not None and mac_address is not None: self.process_client(ip_address, hostname, mac_address) @@ -470,29 +510,19 @@ class DHCPWatcher(WatcherBase): self._sniffer.thread.name = self.__class__.__name__ -def _decode_dhcp_option(dhcp_options, key): - """Extract and decode data from a packet option.""" - for option in dhcp_options: - if len(option) < 2 or option[0] != key: - continue - - value = option[1] - if value is None or key != HOSTNAME: - return value - - # hostname is unicode - try: - return value.decode() - except (AttributeError, UnicodeDecodeError): - return None +def _dhcp_options_as_dict( + dhcp_options: Iterable[tuple[str, int | bytes | None]] +) -> dict[str, str | int | bytes | None]: + """Extract data from packet options as a dict.""" + return {option[0]: option[1] for option in dhcp_options if len(option) >= 2} -def _format_mac(mac_address): +def _format_mac(mac_address: str) -> str: """Format a mac address for matching.""" return format_mac(mac_address).replace(":", "") -def _verify_l2socket_setup(cap_filter): +def _verify_l2socket_setup(cap_filter: str) -> None: """Create a socket using the scapy configured l2socket. Try to create the socket @@ -504,7 +534,7 @@ def _verify_l2socket_setup(cap_filter): conf.L2socket(filter=cap_filter) -def _verify_working_pcap(cap_filter): +def _verify_working_pcap(cap_filter: str) -> None: """Verify we can create a packet filter. If we cannot create a filter we will be listening for diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f5c68897e2e..8e4521eddba 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -60,6 +60,24 @@ MAX_LOAD_CONCURRENTLY = 4 MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer") +class DHCPMatcherRequired(TypedDict, total=True): + """Matcher for the dhcp integration for required fields.""" + + domain: str + + +class DHCPMatcherOptional(TypedDict, total=False): + """Matcher for the dhcp integration for optional fields.""" + + macaddress: str + hostname: str + registered_devices: bool + + +class DHCPMatcher(DHCPMatcherRequired, DHCPMatcherOptional): + """Matcher for the dhcp integration.""" + + class Manifest(TypedDict, total=False): """ Integration manifest. @@ -228,16 +246,16 @@ async def async_get_zeroconf( return zeroconf -async def async_get_dhcp(hass: HomeAssistant) -> list[dict[str, str | bool]]: +async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]: """Return cached list of dhcp types.""" - dhcp: list[dict[str, str | bool]] = DHCP.copy() + dhcp = cast(list[DHCPMatcher], DHCP.copy()) integrations = await async_get_custom_components(hass) for integration in integrations.values(): if not integration.dhcp: continue for entry in integration.dhcp: - dhcp.append({"domain": integration.domain, **entry}) + dhcp.append(cast(DHCPMatcher, {"domain": integration.domain, **entry})) return dhcp diff --git a/mypy.ini b/mypy.ini index 55d608c8628..63c1419d175 100644 --- a/mypy.ini +++ b/mypy.ini @@ -549,6 +549,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.dhcp.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.dlna_dmr.*] check_untyped_defs = true disallow_incomplete_defs = true From ac031cb81704d5059cb584f8af789b9fa59981e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Feb 2022 18:50:15 -1000 Subject: [PATCH 0138/1054] Add diagnostics support to bond (#67412) --- homeassistant/components/bond/diagnostics.py | 39 ++++++++++++++++++ tests/components/bond/common.py | 10 +++++ tests/components/bond/test_diagnostics.py | 43 ++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 homeassistant/components/bond/diagnostics.py create mode 100644 tests/components/bond/test_diagnostics.py diff --git a/homeassistant/components/bond/diagnostics.py b/homeassistant/components/bond/diagnostics.py new file mode 100644 index 00000000000..6af62c3fb24 --- /dev/null +++ b/homeassistant/components/bond/diagnostics.py @@ -0,0 +1,39 @@ +"""Diagnostics support for bond.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, HUB +from .utils import BondHub + +TO_REDACT = {"access_token"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data = hass.data[DOMAIN][entry.entry_id] + hub: BondHub = data[HUB] + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + }, + "hub": { + "version": hub._version, # pylint: disable=protected-access + }, + "devices": [ + { + "device_id": device.device_id, + "props": device.props, + "attrs": device._attrs, # pylint: disable=protected-access + "supported_actions": device._supported_actions, # pylint: disable=protected-access + } + for device in hub.devices + ], + } diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 0400b466e34..9c53c0afb8b 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import MagicMock, patch from aiohttp.client_exceptions import ClientResponseError +from bond_api import DeviceType from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN @@ -18,6 +19,15 @@ from homeassistant.util import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +def ceiling_fan_with_breeze(name: str): + """Create a ceiling fan with given name with breeze support.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": ["SetSpeed", "SetDirection", "BreezeOn"], + } + + def patch_setup_entry(domain: str, *, enabled: bool = True): """Patch async_setup_entry for specified domain.""" if not enabled: diff --git a/tests/components/bond/test_diagnostics.py b/tests/components/bond/test_diagnostics.py new file mode 100644 index 00000000000..88d33ff2cc0 --- /dev/null +++ b/tests/components/bond/test_diagnostics.py @@ -0,0 +1,43 @@ +"""Test bond diagnostics.""" + +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN + +from .common import ceiling_fan_with_breeze, setup_platform + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + + entry = await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan_with_breeze("name-1"), + bond_device_id="test-device-id", + props={"max_speed": 6}, + ) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + mock_device = diag["devices"][0] + mock_device["attrs"]["actions"] = set(mock_device["attrs"]["actions"]) + mock_device["supported_actions"] = set(mock_device["supported_actions"]) + + assert diag == { + "devices": [ + { + "attrs": { + "actions": {"SetSpeed", "SetDirection", "BreezeOn"}, + "name": "name-1", + "type": "CF", + }, + "device_id": "test-device-id", + "props": {"max_speed": 6}, + "supported_actions": {"BreezeOn", "SetSpeed", "SetDirection"}, + } + ], + "entry": { + "data": {"access_token": "**REDACTED**", "host": "some host"}, + "title": "Mock Title", + }, + "hub": {"version": {"bondid": "test-bond-id"}}, + } From fc4cb743bdb046c184df6959d54ffecce949009f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Feb 2022 18:50:26 -1000 Subject: [PATCH 0139/1054] Add Signal Strength sensor to WiZ (#67411) --- homeassistant/components/wiz/__init__.py | 8 ++- homeassistant/components/wiz/sensor.py | 65 ++++++++++++++++++++++++ tests/components/wiz/__init__.py | 2 +- tests/components/wiz/test_sensor.py | 37 ++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/wiz/sensor.py create mode 100644 tests/components/wiz/test_sensor.py diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index d739c571c8b..ef6ffeb58b5 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -30,7 +30,13 @@ from .models import WizData _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.NUMBER, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.LIGHT, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] REQUEST_REFRESH_DELAY = 0.35 diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py new file mode 100644 index 00000000000..d16130883d5 --- /dev/null +++ b/homeassistant/components/wiz/sensor.py @@ -0,0 +1,65 @@ +"""Support for WiZ sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import WizEntity +from .models import WizData + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="rssi", + name="Signal Strength", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the wiz sensor.""" + wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + WizSensor(wiz_data, entry.title, description) for description in SENSORS + ) + + +class WizSensor(WizEntity, SensorEntity): + """Defines a WiZ sensor.""" + + entity_description: SensorEntityDescription + + def __init__( + self, wiz_data: WizData, name: str, description: SensorEntityDescription + ) -> None: + """Initialize an WiZ sensor.""" + super().__init__(wiz_data, name) + self.entity_description = description + self._attr_unique_id = f"{self._device.mac}_{description.key}" + self._attr_name = f"{name} {description.name}" + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + self._attr_native_value = self._device.state.pilotResult.get( + self.entity_description.key + ) diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index c4a31b0a394..62920662c6f 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -199,7 +199,7 @@ def _mocked_wizlight(device, extended_white_range, bulb_type) -> wizlight: bulb.turn_on = AsyncMock() bulb.turn_off = AsyncMock() bulb.updateState = AsyncMock(return_value=FAKE_STATE) - bulb.getSupportedScenes = AsyncMock(return_value=list(SCENES)) + bulb.getSupportedScenes = AsyncMock(return_value=list(SCENES.values())) bulb.start_push = AsyncMock(side_effect=_save_setup_callback) bulb.async_close = AsyncMock() bulb.set_speed = AsyncMock() diff --git a/tests/components/wiz/test_sensor.py b/tests/components/wiz/test_sensor.py new file mode 100644 index 00000000000..37a6b04dad3 --- /dev/null +++ b/tests/components/wiz/test_sensor.py @@ -0,0 +1,37 @@ +"""Tests for the sensor platform.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import ( + FAKE_DUAL_HEAD_RGBWW_BULB, + FAKE_MAC, + _patch_discovery, + _patch_wizlight, + async_push_update, + async_setup_integration, +) + + +async def test_signal_strength(hass: HomeAssistant) -> None: + """Test signal strength.""" + bulb, entry = await async_setup_integration( + hass, bulb_type=FAKE_DUAL_HEAD_RGBWW_BULB + ) + entity_id = "sensor.mock_title_signal_strength" + entity_registry = er.async_get(hass) + reg_entry = entity_registry.async_get(entity_id) + assert reg_entry.unique_id == f"{FAKE_MAC}_rssi" + updated_entity = entity_registry.async_update_entity( + entity_id=entity_id, disabled_by=None + ) + assert not updated_entity.disabled + + with _patch_discovery(), _patch_wizlight(device=bulb): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "-55" + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "rssi": -50}) + assert hass.states.get(entity_id).state == "-50" From f2dabf497845ac04a95dd6e6dc6c33717d5f39a6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 1 Mar 2022 07:02:27 +0100 Subject: [PATCH 0140/1054] Set precision to tenths in Sensibo (#67297) --- homeassistant/components/sensibo/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index f829fd9ed39..eeb904cf6a8 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -29,6 +29,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, + PRECISION_TENTHS, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -144,6 +145,7 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): else TEMP_FAHRENHEIT ) self._attr_supported_features = self.get_features() + self._attr_precision = PRECISION_TENTHS self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, name=coordinator.data[device_id]["name"], From 6a0764160056055ca7ea13b3dcf9bfbbafa4b700 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 07:52:38 +0100 Subject: [PATCH 0141/1054] Bump actions/setup-python from 2.3.2 to 3 (#67415) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2.3.2 to 3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.3.2...v3) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 12 ++++++------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index ce545684da5..84a7df27c87 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -104,7 +104,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 60226d3af08..2e996ea8716 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -154,7 +154,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -234,7 +234,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -284,7 +284,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -335,7 +335,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -377,7 +377,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -511,7 +511,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 1e637b02b1a..17fd1325704 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.3.2 + uses: actions/setup-python@v3 with: python-version: ${{ env.DEFAULT_PYTHON }} From 76e890f5233dde3c370304580e7c23e68be4d3dc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Mar 2022 08:57:02 +0100 Subject: [PATCH 0142/1054] Update ephem to 4.1.2 (#67404) --- homeassistant/components/season/__init__.py | 2 +- homeassistant/components/season/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index 095a4704b12..935270486df 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -1 +1 @@ -"""The season component.""" +"""The season integration.""" diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index cfe04f9b1f7..77059619940 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -2,7 +2,7 @@ "domain": "season", "name": "Season", "documentation": "https://www.home-assistant.io/integrations/season", - "requirements": ["ephem==3.7.7.0"], + "requirements": ["ephem==4.1.2"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 82fdc1b25a9..ab5c929608d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -590,7 +590,7 @@ env_canada==0.5.20 envoy_reader==0.20.1 # homeassistant.components.season -ephem==3.7.7.0 +ephem==4.1.2 # homeassistant.components.epson epson-projector==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5ff3c95388..e17eb48025d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -413,7 +413,7 @@ env_canada==0.5.20 envoy_reader==0.20.1 # homeassistant.components.season -ephem==3.7.7.0 +ephem==4.1.2 # homeassistant.components.epson epson-projector==0.4.2 From e4c62a20377b613dc273dedae1b39f5f250b6917 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Mar 2022 08:58:19 +0100 Subject: [PATCH 0143/1054] Update coverage to 6.3.2 (#67395) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index bf98c8a449e..3896de74630 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.3.1 +coverage==6.3.2 freezegun==1.1.0 mock-open==1.4.0 mypy==0.931 From 82948cc6c130547b98b03abbff7a6c9b03679f09 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Mar 2022 10:19:13 +0100 Subject: [PATCH 0144/1054] Update google-cloud-texttospeech to 2.10.0 (#66746) --- .../components/google_cloud/manifest.json | 2 +- homeassistant/components/google_cloud/tts.py | 29 ++++++++++--------- .../components/google_pubsub/__init__.py | 6 ++-- requirements_all.txt | 2 +- script/pip_check | 2 +- tests/components/google_pubsub/test_init.py | 29 +++++++++---------- 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 90c5eebaeb2..83801d50354 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==0.4.0"], + "requirements": ["google-cloud-texttospeech==2.10.0"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 3d65f4eb297..0de580ef7b7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -122,13 +122,9 @@ SUPPORTED_OPTIONS = [ CONF_TEXT_TYPE, ] -GENDER_SCHEMA = vol.All( - vol.Upper, vol.In(texttospeech.enums.SsmlVoiceGender.__members__) -) +GENDER_SCHEMA = vol.All(vol.Upper, vol.In(texttospeech.SsmlVoiceGender.__members__)) VOICE_SCHEMA = cv.matches_regex(VOICE_REGEX) -SCHEMA_ENCODING = vol.All( - vol.Upper, vol.In(texttospeech.enums.AudioEncoding.__members__) -) +SCHEMA_ENCODING = vol.All(vol.Upper, vol.In(texttospeech.AudioEncoding.__members__)) SPEED_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_SPEED, max=MAX_SPEED)) PITCH_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_PITCH, max=MAX_PITCH)) GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) @@ -263,27 +259,32 @@ class GoogleCloudTTSProvider(Provider): try: params = {options[CONF_TEXT_TYPE]: message} - # pylint: disable=no-member - synthesis_input = texttospeech.types.SynthesisInput(**params) + synthesis_input = texttospeech.SynthesisInput(**params) - voice = texttospeech.types.VoiceSelectionParams( + voice = texttospeech.VoiceSelectionParams( language_code=language, - ssml_gender=texttospeech.enums.SsmlVoiceGender[options[CONF_GENDER]], + ssml_gender=texttospeech.SsmlVoiceGender[options[CONF_GENDER]], name=_voice, ) - audio_config = texttospeech.types.AudioConfig( - audio_encoding=texttospeech.enums.AudioEncoding[_encoding], + audio_config = texttospeech.AudioConfig( + audio_encoding=texttospeech.AudioEncoding[_encoding], speaking_rate=options[CONF_SPEED], pitch=options[CONF_PITCH], volume_gain_db=options[CONF_GAIN], effects_profile_id=options[CONF_PROFILES], ) - # pylint: enable=no-member + + request = { + "voice": voice, + "audio_config": audio_config, + "input": synthesis_input, + } async with async_timeout.timeout(10): + assert self.hass response = await self.hass.async_add_executor_job( - self._client.synthesize_speech, synthesis_input, voice, audio_config + self._client.synthesize_speech, request ) return _encoding, response.audio_content diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index 1de7e98d776..cf1b6da704f 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -6,7 +6,7 @@ import json import logging import os -from google.cloud import pubsub_v1 +from google.cloud.pubsub_v1 import PublisherClient import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN @@ -52,9 +52,7 @@ def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: entities_filter = config[CONF_FILTER] - publisher = pubsub_v1.PublisherClient.from_service_account_json( - service_principal_path - ) + publisher = PublisherClient.from_service_account_json(service_principal_path) topic_path = publisher.topic_path( # pylint: disable=no-member project_id, topic_name diff --git a/requirements_all.txt b/requirements_all.txt index ab5c929608d..000e5915840 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -719,7 +719,7 @@ google-api-python-client==2.38.0 google-cloud-pubsub==2.9.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==0.4.0 +google-cloud-texttospeech==2.10.0 # homeassistant.components.nest google-nest-sdm==1.7.1 diff --git a/script/pip_check b/script/pip_check index fa217e89866..033638ed479 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=6 +DEPENDENCY_CONFLICTS=5 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index d31d28e7302..1b6d1dbf4b4 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -42,10 +42,9 @@ async def test_nested(): @pytest.fixture(autouse=True, name="mock_client") def mock_client_fixture(): """Mock the pubsub client.""" - with mock.patch(f"{GOOGLE_PUBSUB_PATH}.pubsub_v1") as client: - client.PublisherClient = mock.MagicMock() + with mock.patch(f"{GOOGLE_PUBSUB_PATH}.PublisherClient") as client: setattr( - client.PublisherClient, + client, "from_service_account_json", mock.MagicMock(return_value=mock.MagicMock()), ) @@ -83,10 +82,10 @@ async def test_minimal_config(hass, mock_client): await hass.async_block_till_done() assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED - assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert mock_client.PublisherClient.from_service_account_json.call_args[0][ - 0 - ] == os.path.join(hass.config.config_dir, "creds") + assert mock_client.from_service_account_json.call_count == 1 + assert mock_client.from_service_account_json.call_args[0][0] == os.path.join( + hass.config.config_dir, "creds" + ) async def test_full_config(hass, mock_client): @@ -110,10 +109,10 @@ async def test_full_config(hass, mock_client): await hass.async_block_till_done() assert hass.bus.listen.called assert hass.bus.listen.call_args_list[0][0][0] == EVENT_STATE_CHANGED - assert mock_client.PublisherClient.from_service_account_json.call_count == 1 - assert mock_client.PublisherClient.from_service_account_json.call_args[0][ - 0 - ] == os.path.join(hass.config.config_dir, "creds") + assert mock_client.from_service_account_json.call_count == 1 + assert mock_client.from_service_account_json.call_args[0][0] == os.path.join( + hass.config.config_dir, "creds" + ) def make_event(entity_id): @@ -154,7 +153,7 @@ async def test_allowlist(hass, mock_client): "include_entities": ["binary_sensor.included"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("climate.excluded", False), @@ -184,7 +183,7 @@ async def test_denylist(hass, mock_client): "exclude_entities": ["binary_sensor.excluded"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("climate.excluded", False), @@ -216,7 +215,7 @@ async def test_filtered_allowlist(hass, mock_client): "exclude_entities": ["light.excluded"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("light.included", True), @@ -246,7 +245,7 @@ async def test_filtered_denylist(hass, mock_client): "exclude_entities": ["light.excluded"], }, ) - publish_client = mock_client.PublisherClient.from_service_account_json("path") + publish_client = mock_client.from_service_account_json("path") tests = [ FilterTest("climate.excluded", False), From d018cbab3d30d5ec524f96cfa253f2fe3fb42e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Mar 2022 12:16:10 +0100 Subject: [PATCH 0145/1054] Enable strict typing for backup integration (#67427) --- .strict-typing | 1 + homeassistant/components/backup/websocket.py | 6 +++--- mypy.ini | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index b1f6db6a532..652d5fe29e1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -45,6 +45,7 @@ homeassistant.components.amcrest.* homeassistant.components.ampio.* homeassistant.components.aseko_pool_live.* homeassistant.components.automation.* +homeassistant.components.backup.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 2f1381ce217..5e2d5a99d31 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -23,7 +23,7 @@ async def handle_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict, -): +) -> None: """List all stored backups.""" manager: BackupManager = hass.data[DOMAIN] backups = await manager.get_backups() @@ -48,7 +48,7 @@ async def handle_remove( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict, -): +) -> None: """Remove a backup.""" manager: BackupManager = hass.data[DOMAIN] await manager.remove_backup(msg["slug"]) @@ -62,7 +62,7 @@ async def handle_create( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict, -): +) -> None: """Generate a backup.""" manager: BackupManager = hass.data[DOMAIN] backup = await manager.generate_backup() diff --git a/mypy.ini b/mypy.ini index 63c1419d175..2d4005afae6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -296,6 +296,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.backup.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.binary_sensor.*] check_untyped_defs = true disallow_incomplete_defs = true From f40932853f0e392b700bbfa75dcedf2bba57ee26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 1 Mar 2022 12:18:49 +0100 Subject: [PATCH 0146/1054] Bump hatasmota to 0.4.0 (#67421) --- homeassistant/components/tasmota/__init__.py | 55 ++++--------------- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_common.py | 27 ++++++++- .../components/tasmota/test_device_trigger.py | 12 ++-- tests/components/tasmota/test_discovery.py | 10 +++- tests/components/tasmota/test_init.py | 48 +++++++++++----- tests/components/tasmota/test_sensor.py | 9 +-- 9 files changed, 88 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index b6e92301554..7e37c6ea7f2 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -12,7 +12,6 @@ from hatasmota.const import ( CONF_NAME, CONF_SW_VERSION, ) -from hatasmota.discovery import clear_discovery_topic from hatasmota.models import TasmotaDeviceConfig from hatasmota.mqtt import TasmotaMQTTClient @@ -23,11 +22,10 @@ from homeassistant.components.mqtt.subscription import ( async_unsubscribe_topics, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, - EVENT_DEVICE_REGISTRY_UPDATED, DeviceRegistry, async_entries_for_config_entry, ) @@ -77,42 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def async_device_updated(event: Event) -> None: - """Handle the removal of a device.""" - device_registry = dr.async_get(hass) - device_id = event.data["device_id"] - if event.data["action"] not in ("remove", "update"): - return - - connections: set[tuple[str, str]] - if event.data["action"] == "update": - if "config_entries" not in event.data["changes"]: - return - - device = device_registry.async_get(device_id) - if not device: - # The device is already removed, do cleanup when we get "remove" event - return - if entry.entry_id in device.config_entries: - # Not removed from device - return - connections = device.connections - else: - deleted_device = device_registry.deleted_devices[event.data["device_id"]] - connections = deleted_device.connections - if entry.entry_id not in deleted_device.config_entries: - return - - macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] - for mac in macs: - await clear_discovery_topic( - mac, entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt - ) - - hass.data[DATA_UNSUB].append( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_updated) - ) - async def start_platforms() -> None: await device_automation.async_setup_entry(hass, entry) await asyncio.gather( @@ -165,7 +127,7 @@ async def _remove_device( tasmota_mqtt: TasmotaMQTTClient, device_registry: DeviceRegistry, ) -> None: - """Remove device from device registry.""" + """Remove a discovered Tasmota device.""" device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) if device is None or config_entry.entry_id not in device.config_entries: @@ -175,9 +137,6 @@ async def _remove_device( device_registry.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id ) - await clear_discovery_topic( - mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt - ) def _update_device( @@ -218,5 +177,13 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove Tasmota config entry from a device.""" - # Just return True, cleanup is done on when handling device registry events + + connections = device_entry.connections + macs = [c[1] for c in connections if c[0] == CONNECTION_NETWORK_MAC] + tasmota_discovery = hass.data[discovery.TASMOTA_DISCOVERY_INSTANCE] + for mac in macs: + await tasmota_discovery.clear_discovery_topic( + mac, config_entry.data[CONF_DISCOVERY_PREFIX] + ) + return True diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a1f52517690..2f3a1b66fea 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.3.1"], + "requirements": ["hatasmota==0.4.0"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 000e5915840..a3f45f959b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -776,7 +776,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.3.1 +hatasmota==0.4.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e17eb48025d..7c846b837d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -535,7 +535,7 @@ hangups==0.4.17 hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.3.1 +hatasmota==0.4.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 6c296a78006..4744d6c2ccf 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -18,7 +18,7 @@ from hatasmota.utils import ( get_topic_tele_will, ) -from homeassistant.components.tasmota.const import DEFAULT_PREFIX +from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -97,6 +97,31 @@ DEFAULT_CONFIG_9_0_0_3 = { } +DEFAULT_SENSOR_CONFIG = { + "sn": { + "Time": "2020-09-25T12:47:15", + "DHT11": {"Temperature": None}, + "TempUnit": "C", + } +} + + +async def remove_device(hass, ws_client, device_id, config_entry_id=None): + """Remove config entry from a device.""" + if config_entry_id is None: + config_entry_id = hass.config_entries.async_entries(DOMAIN)[0].entry_id + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + assert response["success"] + + async def help_test_availability_when_connection_lost( hass, mqtt_client_mock, diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 24467ed5359..b5a59863bed 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -14,7 +14,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component -from .test_common import DEFAULT_CONFIG +from .test_common import DEFAULT_CONFIG, remove_device from tests.common import ( assert_lists_same, @@ -755,9 +755,10 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async def test_not_fires_on_mqtt_message_after_remove_from_registry( - hass, device_reg, calls, mqtt_mock, setup_tasmota + hass, hass_ws_client, device_reg, calls, mqtt_mock, setup_tasmota ): """Test triggers not firing after removal.""" + assert await async_setup_component(hass, "config", {}) # Discover a device with device trigger config = copy.deepcopy(DEFAULT_CONFIG) config["swc"][0] = 0 @@ -803,7 +804,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 # Remove the device - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() async_fire_mqtt_message( @@ -1037,9 +1038,10 @@ async def test_attach_remove_unknown1(hass, device_reg, mqtt_mock, setup_tasmota async def test_attach_unknown_remove_device_from_registry( - hass, device_reg, mqtt_mock, setup_tasmota + hass, hass_ws_client, device_reg, mqtt_mock, setup_tasmota ): """Test attach and removal of device with unknown trigger.""" + assert await async_setup_component(hass, "config", {}) # Discover a device without device triggers config1 = copy.deepcopy(DEFAULT_CONFIG) config1["swc"][0] = -1 @@ -1080,7 +1082,7 @@ async def test_attach_unknown_remove_device_from_registry( ) # Remove the device - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 90ca5d918fd..0b7e3726482 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -6,9 +6,10 @@ from unittest.mock import patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component from .conftest import setup_tasmota_helper -from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3 +from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3, remove_device from tests.common import MockConfigEntry, async_fire_mqtt_message @@ -366,8 +367,11 @@ async def test_device_remove_multiple_config_entries_2( mqtt_mock.async_publish.assert_not_called() -async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_tasmota): +async def test_device_remove_stale( + hass, hass_ws_client, mqtt_mock, caplog, device_reg, setup_tasmota +): """Test removing a stale (undiscovered) device does not throw.""" + assert await async_setup_component(hass, "config", {}) mac = "00000049A3BC" config_entry = hass.config_entries.async_entries("tasmota")[0] @@ -385,7 +389,7 @@ async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_ta assert device_entry is not None # Remove the device - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) # Verify device entry is removed device_entry = device_reg.async_get_device( diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 7cbb12c49fc..6ad69592ae7 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -7,19 +7,29 @@ from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -from .test_common import DEFAULT_CONFIG +from .test_common import DEFAULT_CONFIG, DEFAULT_SENSOR_CONFIG, remove_device -from tests.common import MockConfigEntry, async_fire_mqtt_message +from tests.common import ( + MockConfigEntry, + MockModule, + async_fire_mqtt_message, + mock_integration, +) async def test_device_remove( - hass, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota + hass, hass_ws_client, mqtt_mock, caplog, device_reg, entity_reg, setup_tasmota ): """Test removing a discovered device through device registry.""" + assert await async_setup_component(hass, "config", {}) config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(DEFAULT_SENSOR_CONFIG) mac = config["mac"] async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) + async_fire_mqtt_message( + hass, f"{DEFAULT_PREFIX}/{mac}/sensors", json.dumps(sensor_config) + ) await hass.async_block_till_done() # Verify device entry is created @@ -28,7 +38,7 @@ async def test_device_remove( ) assert device_entry is not None - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() # Verify device entry is removed @@ -51,7 +61,19 @@ async def test_device_remove_non_tasmota_device( hass, device_reg, hass_ws_client, mqtt_mock, setup_tasmota ): """Test removing a non Tasmota device through device registry.""" + assert await async_setup_component(hass, "config", {}) + + async def async_remove_config_entry_device(hass, config_entry, device_entry): + return True + + mock_integration( + hass, + MockModule( + "test", async_remove_config_entry_device=async_remove_config_entry_device + ), + ) config_entry = MockConfigEntry(domain="test") + config_entry.supports_remove_device = True config_entry.add_to_hass(hass) mac = "12:34:56:AB:CD:EF" @@ -61,7 +83,9 @@ async def test_device_remove_non_tasmota_device( ) assert device_entry is not None - device_reg.async_remove_device(device_entry.id) + await remove_device( + hass, await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) await hass.async_block_till_done() # Verify device entry is removed @@ -78,6 +102,7 @@ async def test_device_remove_stale_tasmota_device( hass, device_reg, hass_ws_client, mqtt_mock, setup_tasmota ): """Test removing a stale (undiscovered) Tasmota device through device registry.""" + assert await async_setup_component(hass, "config", {}) config_entry = hass.config_entries.async_entries("tasmota")[0] mac = "12:34:56:AB:CD:EF" @@ -87,7 +112,7 @@ async def test_device_remove_stale_tasmota_device( ) assert device_entry is not None - device_reg.async_remove_device(device_entry.id) + await remove_device(hass, await hass_ws_client(hass), device_entry.id) await hass.async_block_till_done() # Verify device entry is removed @@ -96,15 +121,8 @@ async def test_device_remove_stale_tasmota_device( ) assert device_entry is None - # Verify retained discovery topic has been cleared - mac = mac.replace(":", "") - mqtt_mock.async_publish.assert_has_calls( - [ - call(f"tasmota/discovery/{mac}/config", "", 0, True), - call(f"tasmota/discovery/{mac}/sensors", "", 0, True), - ], - any_order=True, - ) + # Verify retained discovery topic has not been cleared + mqtt_mock.async_publish.assert_not_called() async def test_tasmota_ws_remove_discovered_device( diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index e2f5f1111e1..8e810c82a43 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -22,6 +22,7 @@ from homeassistant.util import dt from .test_common import ( DEFAULT_CONFIG, + DEFAULT_SENSOR_CONFIG, help_test_availability, help_test_availability_discovery_update, help_test_availability_poll_state, @@ -35,14 +36,6 @@ from .test_common import ( from tests.common import async_fire_mqtt_message, async_fire_time_changed -DEFAULT_SENSOR_CONFIG = { - "sn": { - "Time": "2020-09-25T12:47:15", - "DHT11": {"Temperature": None}, - "TempUnit": "C", - } -} - BAD_INDEXED_SENSOR_CONFIG_3 = { "sn": { "Time": "2020-09-25T12:47:15", From 741b010f8f052785fef0c253ccc8b2425ab11cfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:56:48 +0100 Subject: [PATCH 0147/1054] Bump docker/login-action from 1.13.0 to 1.14.0 (#67416) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 84a7df27c87..731d96b36cd 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -122,13 +122,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -187,13 +187,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -252,13 +252,13 @@ jobs: uses: actions/checkout@v2.4.0 - name: Login to DockerHub - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.13.0 + uses: docker/login-action@v1.14.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From 0515a8bd2d0af232aa2284424a17669951aa7b7d Mon Sep 17 00:00:00 2001 From: Igor Pakhomov Date: Tue, 1 Mar 2022 15:15:23 +0200 Subject: [PATCH 0148/1054] Add additional switch for dmaker.airfresh.a1/t2017 to xiaomi_miio (#67033) * Add additional switch for dmaker.airfresh.a1/t2017 to xiaomi_miio - Auxiliary heat switch - Display switch * Auxiliary heat => Auxiliary Heat Co-authored-by: Maciej Bieniek Co-authored-by: Maciej Bieniek --- homeassistant/components/xiaomi_miio/const.py | 10 +++- .../components/xiaomi_miio/switch.py | 54 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index cc607b3f419..04a3772b29e 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -315,6 +315,8 @@ FEATURE_SET_DELAY_OFF_COUNTDOWN = 65536 FEATURE_SET_LED_BRIGHTNESS_LEVEL = 131072 FEATURE_SET_FAVORITE_RPM = 262144 FEATURE_SET_IONIZER = 524288 +FEATURE_SET_DISPLAY = 1048576 +FEATURE_SET_PTC = 2097152 FEATURE_FLAGS_AIRPURIFIER_MIIO = ( FEATURE_SET_BUZZER @@ -387,7 +389,9 @@ FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = ( | FEATURE_SET_CLEAN ) -FEATURE_FLAGS_AIRFRESH_A1 = FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK +FEATURE_FLAGS_AIRFRESH_A1 = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_DISPLAY | FEATURE_SET_PTC +) FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER @@ -398,7 +402,9 @@ FEATURE_FLAGS_AIRFRESH = ( | FEATURE_SET_EXTRA_FEATURES ) -FEATURE_FLAGS_AIRFRESH_T2017 = FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK +FEATURE_FLAGS_AIRFRESH_T2017 = ( + FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK | FEATURE_SET_DISPLAY | FEATURE_SET_PTC +) FEATURE_FLAGS_FAN_P5 = ( FEATURE_SET_BUZZER diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index ce05a891c0a..1fbb1222578 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -59,10 +59,12 @@ from .const import ( FEATURE_SET_BUZZER, FEATURE_SET_CHILD_LOCK, FEATURE_SET_CLEAN, + FEATURE_SET_DISPLAY, FEATURE_SET_DRY, FEATURE_SET_IONIZER, FEATURE_SET_LEARN_MODE, FEATURE_SET_LED, + FEATURE_SET_PTC, KEY_COORDINATOR, KEY_DEVICE, MODEL_AIRFRESH_A1, @@ -121,6 +123,7 @@ ATTR_AUTO_DETECT = "auto_detect" ATTR_BUZZER = "buzzer" ATTR_CHILD_LOCK = "child_lock" ATTR_CLEAN = "clean_mode" +ATTR_DISPLAY = "display" ATTR_DRY = "dry" ATTR_LEARN_MODE = "learn_mode" ATTR_LED = "led" @@ -131,6 +134,7 @@ ATTR_POWER = "power" ATTR_POWER_MODE = "power_mode" ATTR_POWER_PRICE = "power_price" ATTR_PRICE = "price" +ATTR_PTC = "ptc" ATTR_WIFI_LED = "wifi_led" FEATURE_SET_POWER_MODE = 1 @@ -225,6 +229,15 @@ SWITCH_TYPES = ( method_off="async_set_child_lock_off", entity_category=EntityCategory.CONFIG, ), + XiaomiMiioSwitchDescription( + key=ATTR_DISPLAY, + feature=FEATURE_SET_DISPLAY, + name="Display", + icon="mdi:led-outline", + method_on="async_set_display_on", + method_off="async_set_display_off", + entity_category=EntityCategory.CONFIG, + ), XiaomiMiioSwitchDescription( key=ATTR_DRY, feature=FEATURE_SET_DRY, @@ -279,6 +292,15 @@ SWITCH_TYPES = ( method_off="async_set_ionizer_off", entity_category=EntityCategory.CONFIG, ), + XiaomiMiioSwitchDescription( + key=ATTR_PTC, + feature=FEATURE_SET_PTC, + name="Auxiliary Heat", + icon="mdi:radiator", + method_on="async_set_ptc_on", + method_off="async_set_ptc_off", + entity_category=EntityCategory.CONFIG, + ), ) @@ -533,6 +555,22 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): False, ) + async def async_set_display_on(self) -> bool: + """Turn the display on.""" + return await self._try_command( + "Turning the display of the miio device on failed.", + self._device.set_display, + True, + ) + + async def async_set_display_off(self) -> bool: + """Turn the display off.""" + return await self._try_command( + "Turning the display of the miio device off failed.", + self._device.set_display, + False, + ) + async def async_set_dry_on(self) -> bool: """Turn the dry mode on.""" return await self._try_command( @@ -629,6 +667,22 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): False, ) + async def async_set_ptc_on(self) -> bool: + """Turn ionizer on.""" + return await self._try_command( + "Turning ionizer of the miio device on failed.", + self._device.set_ptc, + True, + ) + + async def async_set_ptc_off(self) -> bool: + """Turn ionizer off.""" + return await self._try_command( + "Turning ionizer of the miio device off failed.", + self._device.set_ptc, + False, + ) + class XiaomiGatewaySwitch(XiaomiGatewayDevice, SwitchEntity): """Representation of a XiaomiGatewaySwitch.""" From 7bbde822d2867f5e53fd087bb1fed64ed1f01473 Mon Sep 17 00:00:00 2001 From: Igor Pakhomov Date: Tue, 1 Mar 2022 15:57:14 +0200 Subject: [PATCH 0149/1054] Add "Auxiliary Heat Status" binary_sensor for dmaker.airfresh.a1/t2017 to xiaomi_miio (#67040) --- .../components/xiaomi_miio/binary_sensor.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 83bea4be9b1..ca421486a35 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -23,6 +23,8 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, MODEL_FAN_ZA5, MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, @@ -36,6 +38,7 @@ from .device import XiaomiCoordinatedMiioEntity _LOGGER = logging.getLogger(__name__) ATTR_NO_WATER = "no_water" +ATTR_PTC_STATUS = "ptc_status" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_WATER_TANK_DETACHED = "water_tank_detached" ATTR_MOP_ATTACHED = "is_water_box_carriage_attached" @@ -66,6 +69,12 @@ BINARY_SENSOR_TYPES = ( value=lambda value: not value, entity_category=EntityCategory.DIAGNOSTIC, ), + XiaomiMiioBinarySensorDescription( + key=ATTR_PTC_STATUS, + name="Auxiliary Heat Status", + device_class=BinarySensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, name="Power Supply", @@ -74,6 +83,7 @@ BINARY_SENSOR_TYPES = ( ), ) +AIRFRESH_A1_BINARY_SENSORS = (ATTR_PTC_STATUS,) FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { @@ -171,7 +181,9 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: model = config_entry.data[CONF_MODEL] sensors = [] - if model in MODEL_FAN_ZA5: + if model in MODEL_AIRFRESH_A1 or model in MODEL_AIRFRESH_T2017: + sensors = AIRFRESH_A1_BINARY_SENSORS + elif model in MODEL_FAN_ZA5: sensors = FAN_ZA5_BINARY_SENSORS elif model in MODELS_HUMIDIFIER_MIIO: sensors = HUMIDIFIER_MIIO_BINARY_SENSORS From 32adeb8356b3ccc966ea510d2f2e844bd77d45cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Mar 2022 15:16:18 +0100 Subject: [PATCH 0150/1054] Remove class attributes for backup manager (#67431) * Remove class attributes for backup manager * remove patches --- homeassistant/components/backup/manager.py | 25 +++++++++--------- tests/components/backup/test_manager.py | 30 +++++++++++++--------- tests/components/backup/test_websocket.py | 14 +--------- 3 files changed, 31 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 5861db1ad27..80498450453 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -36,14 +36,13 @@ class Backup: class BackupManager: """Backup manager for the Backup integration.""" - _backups: dict[str, Backup] = {} - _loaded = False - def __init__(self, hass: HomeAssistant) -> None: """Initialize the backup manager.""" self.hass = hass self.backup_dir = Path(hass.config.path("backups")) self.backing_up = False + self.backups: dict[str, Backup] = {} + self.loaded = False async def load_backups(self) -> None: """Load data of stored backup files.""" @@ -68,22 +67,22 @@ class BackupManager: await self.hass.async_add_executor_job(_read_backups) LOGGER.debug("Loaded %s backups", len(backups)) - self._backups = backups - self._loaded = True + self.backups = backups + self.loaded = True async def get_backups(self) -> dict[str, Backup]: """Return backups.""" - if not self._loaded: + if not self.loaded: await self.load_backups() - return self._backups + return self.backups async def get_backup(self, slug: str) -> Backup | None: """Return a backup.""" - if not self._loaded: + if not self.loaded: await self.load_backups() - if not (backup := self._backups.get(slug)): + if not (backup := self.backups.get(slug)): return None if not backup.path.exists(): @@ -92,7 +91,7 @@ class BackupManager: backup.slug, backup.path, ) - self._backups.pop(slug) + self.backups.pop(slug) return None return backup @@ -104,7 +103,7 @@ class BackupManager: await self.hass.async_add_executor_job(backup.path.unlink, True) LOGGER.debug("Removed backup located at %s", backup.path) - self._backups.pop(slug) + self.backups.pop(slug) async def generate_backup(self) -> Backup: """Generate a backup.""" @@ -160,8 +159,8 @@ class BackupManager: path=tar_file_path, size=round(tar_file_path.stat().st_size / 1_048_576, 2), ) - if self._loaded: - self._backups[slug] = backup + if self.loaded: + self.backups[slug] = backup LOGGER.debug("Generated new backup with slug %s", slug) return backup finally: diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 0c4b21746f0..b2e8923263d 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -52,6 +52,20 @@ async def test_load_backups_with_exception( assert backups == {} +async def test_removing_backup( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test removing backup.""" + manager = BackupManager(hass) + manager.backups = {TEST_BACKUP.slug: TEST_BACKUP} + manager.loaded = True + + with patch("pathlib.Path.exists", return_value=True): + await manager.remove_backup(TEST_BACKUP.slug) + assert "Removed backup located at" in caplog.text + + async def test_removing_non_existing_backup( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, @@ -69,16 +83,10 @@ async def test_getting_backup_that_does_not_exist( ): """Test getting backup that does not exist.""" manager = BackupManager(hass) + manager.backups = {TEST_BACKUP.slug: TEST_BACKUP} + manager.loaded = True - with patch( - "homeassistant.components.backup.websocket.BackupManager._backups", - {TEST_BACKUP.slug: TEST_BACKUP}, - ), patch( - "homeassistant.components.backup.websocket.BackupManager._loaded", - True, - ), patch( - "pathlib.Path.exists", return_value=False - ): + with patch("pathlib.Path.exists", return_value=False): backup = await manager.get_backup(TEST_BACKUP.slug) assert backup is None @@ -102,6 +110,7 @@ async def test_generate_backup( ) -> None: """Test generate backup.""" manager = BackupManager(hass) + manager.loaded = True def _mock_iterdir(path: Path) -> list[Path]: if not path.name.endswith("testing_config"): @@ -133,9 +142,6 @@ async def test_generate_backup( ) as mocked_json_util, patch( "homeassistant.components.backup.manager.HAVERSION", "2025.1.0", - ), patch( - "homeassistant.components.backup.websocket.BackupManager._loaded", - True, ): await manager.generate_backup() diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index 7c2f9a8c752..dbdeb33c927 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -40,22 +40,13 @@ async def test_remove( await hass.async_block_till_done() with patch( - "homeassistant.components.backup.websocket.BackupManager._backups", - {TEST_BACKUP.slug: TEST_BACKUP}, - ), patch( - "homeassistant.components.backup.websocket.BackupManager._loaded", - True, - ), patch( - "pathlib.Path.unlink" - ), patch( - "pathlib.Path.exists", return_value=True + "homeassistant.components.backup.websocket.BackupManager.remove_backup", ): await client.send_json({"id": 1, "type": "backup/remove", "slug": "abc123"}) msg = await client.receive_json() assert msg["id"] == 1 assert msg["success"] - assert f"Removed backup located at {TEST_BACKUP.path}" in caplog.text async def test_generate( @@ -69,9 +60,6 @@ async def test_generate( await hass.async_block_till_done() with patch( - "homeassistant.components.backup.websocket.BackupManager._backups", - {TEST_BACKUP.slug: TEST_BACKUP}, - ), patch( "homeassistant.components.backup.websocket.BackupManager.generate_backup", return_value=TEST_BACKUP, ): From 2e7de9570ab2a3d6b0a0458d725a799d36abc67f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 1 Mar 2022 16:40:00 +0100 Subject: [PATCH 0151/1054] CONF_SLAVE do not have default 0 in a validator (#67418) --- homeassistant/components/modbus/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 35fc9cf63a9..315e138e130 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -196,7 +196,7 @@ def duplicate_entity_validator(config: dict) -> dict: addr += "_" + str(entry[CONF_COMMAND_ON]) if CONF_COMMAND_OFF in entry: addr += "_" + str(entry[CONF_COMMAND_OFF]) - addr += "_" + str(entry[CONF_SLAVE]) + addr += "_" + str(entry.get(CONF_SLAVE, 0)) if addr in addresses: err = f"Modbus {component}/{name} address {addr} is duplicate, second entry not loaded!" _LOGGER.warning(err) From 6dacf75aaf0078038ccdf1893881973ed6306986 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 1 Mar 2022 18:19:34 +0100 Subject: [PATCH 0152/1054] Bump python-songpal to 0.14.1 (#67435) Changelog https://github.com/rytilahti/python-songpal/releases/tag/0.14.1 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 80a26a56b22..8825de877e5 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Sony Songpal", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": ["python-songpal==0.14"], + "requirements": ["python-songpal==0.14.1"], "codeowners": ["@rytilahti", "@shenxn"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a3f45f959b7..2b74cde8352 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1927,7 +1927,7 @@ python-smarttub==0.0.29 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.14 +python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c846b837d6..87c94748973 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1230,7 +1230,7 @@ python-picnic-api==1.1.0 python-smarttub==0.0.29 # homeassistant.components.songpal -python-songpal==0.14 +python-songpal==0.14.1 # homeassistant.components.tado python-tado==0.12.0 From 418808d873912d84d102909f0ff4e164fac45e27 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 1 Mar 2022 18:21:40 +0100 Subject: [PATCH 0153/1054] Cleanup search for mac in samsungtv (#67374) * Add logging on config_entry update * Add search for mac * Use info Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index c9ca282fdd1..69289dca274 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -156,6 +156,7 @@ async def _async_create_bridge_with_updated_data( info: dict[str, Any] | None = None if not port or not method: + LOGGER.debug("Attempting to get port or method for %s", host) if method == METHOD_LEGACY: port = LEGACY_PORT else: @@ -167,6 +168,7 @@ async def _async_create_bridge_with_updated_data( "Failed to determine connection method, make sure the device is on." ) + LOGGER.info("Updated port to %s and method to %s for %s", port, method, host) updated_data[CONF_PORT] = port updated_data[CONF_METHOD] = method @@ -174,17 +176,22 @@ async def _async_create_bridge_with_updated_data( mac: str | None = entry.data.get(CONF_MAC) if not mac: + LOGGER.debug("Attempting to get mac for %s", host) if info: mac = mac_from_device_info(info) else: mac = await bridge.async_mac_from_device() - if not mac: - mac = await hass.async_add_executor_job( - partial(getmac.get_mac_address, ip=host) - ) - if mac: - updated_data[CONF_MAC] = mac + if not mac: + mac = await hass.async_add_executor_job( + partial(getmac.get_mac_address, ip=host) + ) + + if mac: + LOGGER.info("Updated mac to %s for %s", mac, host) + updated_data[CONF_MAC] = mac + else: + LOGGER.info("Failed to get mac for %s", host) if updated_data: data = {**entry.data, **updated_data} From b613008eda436ceadef66a474b1b7d137154d8a4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 1 Mar 2022 20:05:51 +0100 Subject: [PATCH 0154/1054] Cleanup samsungtv bridge (#67424) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 55fd44fd45c..6ad2d6eb8bf 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -378,9 +378,6 @@ class SamsungTVWSBridge(SamsungTVBridge): async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" - if not self.port: - return None - if self._rest_api is None: self._rest_api = SamsungTVAsyncRest( host=self.host, From a81fa31314a9a57af0c089c9003e8f01474574a0 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 2 Mar 2022 03:17:25 +0800 Subject: [PATCH 0155/1054] Bump httpx and httpcore (#67438) --- homeassistant/package_constraints.txt | 4 ++-- requirements.txt | 2 +- script/gen_requirements_all.py | 2 +- setup.cfg | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98dd11bbe73..c0fd916c9e2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 home-assistant-frontend==20220226.0 -httpx==0.21.3 +httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 paho-mqtt==1.6.1 @@ -77,7 +77,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.5.0 h11==0.12.0 -httpcore==0.14.5 +httpcore==0.14.7 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/requirements.txt b/requirements.txt index 4885e717b30..923075bb5e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ awesomeversion==22.2.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -httpx==0.21.3 +httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 PyJWT==2.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index de6e2229d1d..c95d470fdeb 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -97,7 +97,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==3.5.0 h11==0.12.0 -httpcore==0.14.5 +httpcore==0.14.7 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/setup.cfg b/setup.cfg index 8787432ba7f..1fbd8265e46 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ install_requires = ciso8601==2.2.0 # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - httpx==0.21.3 + httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 PyJWT==2.1.0 From 94130a6060df836a020aab1af3d1c25ccf56ad37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Mar 2022 09:18:09 -1000 Subject: [PATCH 0156/1054] Avoid creating wiring select for Magic Home if its not supported (#67417) --- homeassistant/components/flux_led/select.py | 2 +- tests/components/flux_led/test_select.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flux_led/select.py b/homeassistant/components/flux_led/select.py index 4067110e336..63929740020 100644 --- a/homeassistant/components/flux_led/select.py +++ b/homeassistant/components/flux_led/select.py @@ -64,7 +64,7 @@ async def async_setup_entry( coordinator, base_unique_id, f"{name} Operating Mode", "operating_mode" ) ) - if device.wirings: + if device.wirings and device.wiring is not None: entities.append( FluxWiringsSelect(coordinator, base_unique_id, f"{name} Wiring", "wiring") ) diff --git a/tests/components/flux_led/test_select.py b/tests/components/flux_led/test_select.py index b2a88b00fe0..91be62e5ab7 100644 --- a/tests/components/flux_led/test_select.py +++ b/tests/components/flux_led/test_select.py @@ -299,3 +299,23 @@ async def test_select_white_channel_type(hass: HomeAssistant) -> None: == WhiteChannelType.NATURAL.name.lower() ) assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_select_device_no_wiring(hass: HomeAssistant) -> None: + """Test select is not created if the device does not support wiring.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.wiring = None + bulb.wirings = ["RGB", "GRB"] + bulb.raw_state = bulb.raw_state._replace(model_num=0x25) + with _patch_discovery(), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + wiring_entity_id = "select.bulb_rgbcw_ddeeff_wiring" + assert hass.states.get(wiring_entity_id) is None From e58ce7ab6e05dd24955f20b2de7fead4afd0c6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 1 Mar 2022 21:37:51 +0100 Subject: [PATCH 0157/1054] Fix returned value from backup/info WS command (#67439) --- homeassistant/components/backup/manager.py | 3 ++- homeassistant/components/backup/websocket.py | 2 +- tests/components/backup/test_websocket.py | 11 ++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 80498450453..bc037287c09 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -5,6 +5,7 @@ from dataclasses import asdict, dataclass import hashlib import json from pathlib import Path +import tarfile from tarfile import TarError from tempfile import TemporaryDirectory @@ -51,7 +52,7 @@ class BackupManager: def _read_backups() -> None: for backup_path in self.backup_dir.glob("*.tar"): try: - with SecureTarFile(backup_path, "r", gzip=False) as backup_file: + with tarfile.open(backup_path, "r:") as backup_file: if data_file := backup_file.extractfile("./backup.json"): data = json.loads(data_file.read()) backup = Backup( diff --git a/homeassistant/components/backup/websocket.py b/homeassistant/components/backup/websocket.py index 5e2d5a99d31..5c12a764941 100644 --- a/homeassistant/components/backup/websocket.py +++ b/homeassistant/components/backup/websocket.py @@ -30,7 +30,7 @@ async def handle_info( connection.send_result( msg["id"], { - "backups": list(backups), + "backups": list(backups.values()), "backing_up": manager.backing_up, }, ) diff --git a/tests/components/backup/test_websocket.py b/tests/components/backup/test_websocket.py index dbdeb33c927..4179eb026c5 100644 --- a/tests/components/backup/test_websocket.py +++ b/tests/components/backup/test_websocket.py @@ -20,12 +20,17 @@ async def test_info( client = await hass_ws_client(hass) await hass.async_block_till_done() - await client.send_json({"id": 1, "type": "backup/info"}) - msg = await client.receive_json() + with patch( + "homeassistant.components.backup.websocket.BackupManager.get_backups", + return_value={TEST_BACKUP.slug: TEST_BACKUP}, + ): + + await client.send_json({"id": 1, "type": "backup/info"}) + msg = await client.receive_json() assert msg["id"] == 1 assert msg["success"] - assert msg["result"] == {"backing_up": False, "backups": []} + assert msg["result"] == {"backing_up": False, "backups": [TEST_BACKUP.as_dict()]} async def test_remove( From 555d0445d8c424818422adf8d0ed03fd08db3d94 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:14:26 +0100 Subject: [PATCH 0158/1054] Pip setup-python to 3.0.0 (#67448) --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 12 ++++++------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 731d96b36cd..ccb76121792 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -104,7 +104,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2e996ea8716..a5b4f37cdf5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -154,7 +154,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -234,7 +234,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -284,7 +284,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -335,7 +335,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -377,7 +377,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -511,7 +511,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 17fd1325704..e778f3cd6b9 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v2.4.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v3.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From 57705bc13d86e004f51f1d4d368754febbfa1018 Mon Sep 17 00:00:00 2001 From: JeroenTuinstra <47460053+JeroenTuinstra@users.noreply.github.com> Date: Wed, 2 Mar 2022 00:12:54 +0100 Subject: [PATCH 0159/1054] Correct selector for remote integration line 50 (#67432) --- homeassistant/components/remote/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 3130484d10b..bdeef15971e 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -47,7 +47,7 @@ send_command: required: true example: "Play" selector: - text: + object: num_repeats: name: Repeats description: The number of times you want to repeat the command(s). From d68ada74ccebaa0c1b6986b3be9cf4d73eca7cae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 15:14:14 -0800 Subject: [PATCH 0160/1054] Restore children media class (#67409) --- homeassistant/components/dlna_dms/dms.py | 6 ++++-- .../components/media_player/browse_media.py | 16 +++++++--------- tests/components/cast/test_media_player.py | 5 +++++ tests/components/dlna_dmr/test_media_player.py | 2 ++ tests/components/motioneye/test_media_source.py | 9 +++++++++ 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 74774a84821..94ac67b0f1a 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -580,7 +580,8 @@ class DmsDeviceSource: children=children, ) - media_source.calculate_children_class() + if media_source.children: + media_source.calculate_children_class() return media_source @@ -650,7 +651,8 @@ class DmsDeviceSource: thumbnail=self._didl_thumbnail_url(item), ) - media_source.calculate_children_class() + if media_source.children: + media_source.calculate_children_class() return media_source diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 26494e4c8a7..fa825042817 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from urllib.parse import quote import yarl @@ -74,11 +75,15 @@ class BrowseMedia: def as_dict(self, *, parent: bool = True) -> dict: """Convert Media class to browse media dictionary.""" - response = { + if self.children_media_class is None and self.children: + self.calculate_children_class() + + response: dict[str, Any] = { "title": self.title, "media_class": self.media_class, "media_content_type": self.media_content_type, "media_content_id": self.media_content_id, + "children_media_class": self.children_media_class, "can_play": self.can_play, "can_expand": self.can_expand, "thumbnail": self.thumbnail, @@ -87,11 +92,7 @@ class BrowseMedia: if not parent: return response - if self.children_media_class is None: - self.calculate_children_class() - response["not_shown"] = self.not_shown - response["children_media_class"] = self.children_media_class if self.children: response["children"] = [ @@ -104,11 +105,8 @@ class BrowseMedia: def calculate_children_class(self) -> None: """Count the children media classes and calculate the correct class.""" - if self.children is None or len(self.children) == 0: - return - self.children_media_class = MEDIA_CLASS_DIRECTORY - + assert self.children is not None proposed_class = self.children[0].media_class if all(child.media_class == proposed_class for child in self.children): self.children_media_class = proposed_class diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index fa70ed17eb9..4cf96f1a965 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -872,6 +872,7 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_1 in response["result"]["children"] @@ -883,6 +884,7 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_2 in response["result"]["children"] @@ -926,6 +928,7 @@ async def test_entity_browse_media_audio_only( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_1 not in response["result"]["children"] @@ -937,6 +940,7 @@ async def test_entity_browse_media_audio_only( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_2 in response["result"]["children"] @@ -1873,6 +1877,7 @@ async def test_cast_platform_browse_media(hass: HomeAssistant, hass_ws_client): "can_play": False, "can_expand": True, "thumbnail": "https://brands.home-assistant.io/_/spotify/logo.png", + "children_media_class": None, } assert expected_child in response["result"]["children"] diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 896968557c1..a9ac5946f30 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -961,6 +961,7 @@ async def test_browse_media( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_video in response["result"]["children"] @@ -972,6 +973,7 @@ async def test_browse_media( "can_play": True, "can_expand": False, "thumbnail": None, + "children_media_class": None, } assert expected_child_audio in response["result"]["children"] diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 6c0e46b34c6..6979d5c645d 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -111,6 +111,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "directory", } ], "not_shown": 0, @@ -143,6 +144,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "directory", } ], "not_shown": 0, @@ -174,6 +176,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "video", }, { "title": "Images", @@ -186,6 +189,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "image", }, ], "not_shown": 0, @@ -220,6 +224,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": True, "thumbnail": None, + "children_media_class": "directory", } ], "not_shown": 0, @@ -255,6 +260,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": True, "can_expand": False, "thumbnail": "http://movie", + "children_media_class": None, }, { "title": "00-36-49.mp4", @@ -268,6 +274,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": True, "can_expand": False, "thumbnail": "http://movie", + "children_media_class": None, }, { "title": "00-02-27.mp4", @@ -281,6 +288,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: "can_play": True, "can_expand": False, "thumbnail": "http://movie", + "children_media_class": None, }, ], "not_shown": 0, @@ -331,6 +339,7 @@ async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: "can_play": False, "can_expand": False, "thumbnail": "http://image", + "children_media_class": None, } ], "not_shown": 0, From 78a49eb9e8148630f82eb6e235d2ad41a0679352 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Mar 2022 14:00:48 -1000 Subject: [PATCH 0161/1054] Partially revert powerwall abs change from #67245 (#67300) --- homeassistant/components/powerwall/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 9efa5f380dd..b0282983629 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -137,7 +137,7 @@ class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity): class PowerWallEnergyDirectionSensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Direction Energy sensor.""" - _attr_state_class = SensorStateClass.TOTAL_INCREASING + _attr_state_class = SensorStateClass.TOTAL _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_device_class = SensorDeviceClass.ENERGY @@ -183,7 +183,7 @@ class PowerWallExportSensor(PowerWallEnergyDirectionSensor): @property def native_value(self) -> float: """Get the current value in kWh.""" - return abs(self.meter.get_energy_exported()) + return self.meter.get_energy_exported() class PowerWallImportSensor(PowerWallEnergyDirectionSensor): @@ -200,4 +200,4 @@ class PowerWallImportSensor(PowerWallEnergyDirectionSensor): @property def native_value(self) -> float: """Get the current value in kWh.""" - return abs(self.meter.get_energy_imported()) + return self.meter.get_energy_imported() From dc10a4f0bbff7809b972a72851bf1a636e0208ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 16:05:23 -0800 Subject: [PATCH 0162/1054] Bump frontend to 20220301.0 (#67457) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4b28744d0e3..cc118e23dc9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220226.0" + "home-assistant-frontend==20220301.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c0fd916c9e2..33fa40b1766 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220226.0 +home-assistant-frontend==20220301.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2b74cde8352..88a7bd0c77d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -803,7 +803,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220226.0 +home-assistant-frontend==20220301.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87c94748973..81565c25a51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220226.0 +home-assistant-frontend==20220301.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 133add6100d11c8d0f3e102703663099bc955d6e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Mar 2022 01:06:36 +0100 Subject: [PATCH 0163/1054] Fix CO2Signal having unknown data (#67453) --- homeassistant/components/co2signal/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 8d691b0e5c9..b10cd054ff9 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -92,14 +92,15 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE def available(self) -> bool: """Return True if entity is available.""" return ( - super().available - and self.coordinator.data["data"].get(self._description.key) is not None + super().available and self._description.key in self.coordinator.data["data"] ) @property def native_value(self) -> StateType: """Return sensor state.""" - return round(self.coordinator.data["data"][self._description.key], 2) # type: ignore[misc] + if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[misc] + return None + return round(value, 2) @property def native_unit_of_measurement(self) -> str | None: From 7889aace5ff7ffc88eff7e357968408fcc190932 Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 2 Mar 2022 01:21:47 +0100 Subject: [PATCH 0164/1054] Add flipr API error detection and catch it correctly. (#67405) --- homeassistant/components/flipr/__init__.py | 14 +++++++-- homeassistant/components/flipr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flipr/test_sensor.py | 30 ++++++++++++++++++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 8379845982a..1a9f3dc0314 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from flipr_api import FliprAPIRestClient +from flipr_api.exceptions import FliprError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform @@ -11,6 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from .const import ATTRIBUTION, CONF_FLIPR_ID, DOMAIN, MANUFACTURER, NAME @@ -68,9 +70,15 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Fetch data from API endpoint.""" - return await self.hass.async_add_executor_job( - self.client.get_pool_measure_latest, self.flipr_id - ) + try: + data = await self.hass.async_add_executor_job( + self.client.get_pool_measure_latest, self.flipr_id + ) + except (FliprError) as error: + _LOGGER.error(error) + raise UpdateFailed from error + + return data class FliprEntity(CoordinatorEntity): diff --git a/homeassistant/components/flipr/manifest.json b/homeassistant/components/flipr/manifest.json index 357b5aeb160..77388393d3f 100644 --- a/homeassistant/components/flipr/manifest.json +++ b/homeassistant/components/flipr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flipr", "requirements": [ - "flipr-api==1.4.1"], + "flipr-api==1.4.2"], "codeowners": [ "@cnico" ], diff --git a/requirements_all.txt b/requirements_all.txt index 88a7bd0c77d..3fff1c2694b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ fixerio==1.0.0a0 fjaraskupan==1.0.2 # homeassistant.components.flipr -flipr-api==1.4.1 +flipr-api==1.4.2 # homeassistant.components.flux_led flux_led==0.28.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81565c25a51..7d0db0fa26c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -431,7 +431,7 @@ fivem-api==0.1.2 fjaraskupan==1.0.2 # homeassistant.components.flipr -flipr-api==1.4.1 +flipr-api==1.4.2 # homeassistant.components.flux_led flux_led==0.28.27 diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 7fd04fbc992..45816801472 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import patch +from flipr_api.exceptions import FliprError + from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import ( ATTR_ICON, @@ -84,3 +86,31 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" assert state.state == "0.23654886" + + +async def test_error_flipr_api_sensors(hass: HomeAssistant) -> None: + """Test the Flipr sensors error.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="test_entry_unique_id", + data={ + CONF_EMAIL: "toto@toto.com", + CONF_PASSWORD: "myPassword", + CONF_FLIPR_ID: "myfliprid", + }, + ) + + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + with patch( + "flipr_api.FliprAPIRestClient.get_pool_measure_latest", + side_effect=FliprError("Error during flipr data retrieval..."), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Check entity is not generated because of the FliprError raised. + entity = registry.async_get("sensor.flipr_myfliprid_red_ox") + assert entity is None From de2ddfe7b788ab9da8a4a0bf64b4f49d0791c072 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 2 Mar 2022 01:22:15 +0100 Subject: [PATCH 0165/1054] Add missing temperature sensor for Shelly Motion2 (#67458) --- homeassistant/components/shelly/sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index ce9c57f5889..21a7447e2b2 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -215,6 +215,15 @@ SENSORS: Final = { icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, ), + ("sensor", "temp"): BlockSensorDescription( + key="sensor|temp", + name="Temperature", + unit_fn=temperature_unit, + value=lambda value: round(value, 1), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ("sensor", "extTemp"): BlockSensorDescription( key="sensor|extTemp", name="Temperature", From 65eefcacfc373a1a0dc30ede9eb294c4c31e8082 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Mar 2022 16:56:05 -0800 Subject: [PATCH 0166/1054] Add support for detecting hostname based addresses as internal (#67407) --- homeassistant/helpers/network.py | 4 +- tests/components/roku/test_media_player.py | 13 ++++-- tests/helpers/test_network.py | 51 ++++++++++++++++------ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 9d7780ab900..a8c4b3cf458 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -25,7 +25,9 @@ class NoURLAvailableError(HomeAssistantError): def is_internal_request(hass: HomeAssistant) -> bool: """Test if the current request is internal.""" try: - _get_internal_url(hass, require_current_request=True) + get_url( + hass, allow_external=False, allow_cloud=False, require_current_request=True + ) return True except NoURLAvailableError: return False diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 79b996530e3..050814e3817 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -759,7 +759,10 @@ async def test_media_browse( assert msg["result"]["children"][0]["title"] == "Roku Channel Store" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "11" - assert "/browse_media/app/11" in msg["result"]["children"][0]["thumbnail"] + assert ( + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/11" + ) assert msg["result"]["children"][0]["can_play"] # test invalid media type @@ -1016,14 +1019,18 @@ async def test_tv_media_browse( assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" assert ( - "/browse_media/app/tvinput.hdmi2" in msg["result"]["children"][0]["thumbnail"] + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/tvinput.hdmi2" ) assert msg["result"]["children"][0]["can_play"] assert msg["result"]["children"][3]["title"] == "Roku Channel Store" assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP assert msg["result"]["children"][3]["media_content_id"] == "11" - assert "/browse_media/app/11" in msg["result"]["children"][3]["thumbnail"] + assert ( + msg["result"]["children"][3]["thumbnail"] + == "http://192.168.1.160:8060/query/icon/11" + ) assert msg["result"]["children"][3]["can_play"] # test channels diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 15a9b8d1ff8..0838375fd1f 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -20,6 +20,17 @@ from homeassistant.helpers.network import ( from tests.common import mock_component +@pytest.fixture(name="mock_current_request") +def mock_current_request_mock(): + """Mock the current request.""" + mock_current_request = Mock(name="mock_request") + with patch( + "homeassistant.helpers.network.http.current_request", + Mock(get=mock_current_request), + ): + yield mock_current_request + + async def test_get_url_internal(hass: HomeAssistant): """Test getting an instance URL when the user has set an internal URL.""" assert hass.config.internal_url is None @@ -611,7 +622,7 @@ async def test_get_current_request_url_with_known_host( get_url(hass, require_current_request=True) -async def test_is_internal_request(hass: HomeAssistant): +async def test_is_internal_request(hass: HomeAssistant, mock_current_request): """Test if accessing an instance on its internal URL.""" # Test with internal URL: http://example.local:8123 await async_process_ha_core_config( @@ -620,18 +631,16 @@ async def test_is_internal_request(hass: HomeAssistant): ) assert hass.config.internal_url == "http://example.local:8123" + + # No request active + mock_current_request.return_value = None assert not is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="example.local" - ): - assert is_internal_request(hass) + mock_current_request.return_value = Mock(url="http://example.local:8123") + assert is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", - return_value="no_match.example.local", - ): - assert not is_internal_request(hass) + mock_current_request.return_value = Mock(url="http://no_match.example.local:8123") + assert not is_internal_request(hass) # Test with internal URL: http://192.168.0.1:8123 await async_process_ha_core_config( @@ -642,10 +651,26 @@ async def test_is_internal_request(hass: HomeAssistant): assert hass.config.internal_url == "http://192.168.0.1:8123" assert not is_internal_request(hass) - with patch( - "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1" + mock_current_request.return_value = Mock(url="http://192.168.0.1:8123") + assert is_internal_request(hass) + + # Test for matching against local IP + hass.config.api = Mock(use_ssl=False, local_ip="192.168.123.123", port=8123) + for allowed in ("127.0.0.1", "192.168.123.123"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url + + # Test for matching against HassOS hostname + with patch.object( + hass.components.hassio, "is_hassio", return_value=True + ), patch.object( + hass.components.hassio, + "get_host_info", + return_value={"hostname": "hellohost"}, ): - assert is_internal_request(hass) + for allowed in ("hellohost", "hellohost.local"): + mock_current_request.return_value = Mock(url=f"http://{allowed}:8123") + assert is_internal_request(hass), mock_current_request.return_value.url async def test_is_hass_url(hass): From 4ea6ca7f9190606ee98d0bbbc5f3335f52e4831a Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 2 Mar 2022 00:56:20 -0500 Subject: [PATCH 0167/1054] Prefer internal docker URL for VLC telnet when possible (#67090) --- .../components/media_player/browse_media.py | 21 ++++++++-- .../components/vlc_telnet/media_player.py | 7 +++- homeassistant/helpers/network.py | 29 ++++++++++++++ .../media_player/test_browse_media.py | 38 +++++++++++++++++- tests/helpers/test_network.py | 39 +++++++++++++++++++ 5 files changed, 127 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index fa825042817..d85dcc0a3e3 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -10,14 +10,22 @@ import yarl from homeassistant.components.http.auth import async_sign_path from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import get_url, is_hass_url +from homeassistant.helpers.network import ( + get_supervisor_network_url, + get_url, + is_hass_url, +) from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY @callback def async_process_play_media_url( - hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False + hass: HomeAssistant, + media_content_id: str, + *, + allow_relative_url: bool = False, + for_supervisor_network: bool = False, ) -> str: """Update a media URL with authentication if it points at Home Assistant.""" if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): @@ -39,7 +47,14 @@ def async_process_play_media_url( # convert relative URL to absolute URL if media_content_id[0] == "/" and not allow_relative_url: - media_content_id = f"{get_url(hass)}{media_content_id}" + base_url = None + if for_supervisor_network: + base_url = get_supervisor_network_url(hass) + + if not base_url: + base_url = get_url(hass) + + media_content_id = f"{base_url}{media_content_id}" return media_content_id diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 140c2b2c253..72beede3f2f 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -31,7 +31,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -125,6 +125,7 @@ class VlcDevice(MediaPlayerEntity): manufacturer="VideoLAN", name=name, ) + self._using_addon = config_entry.source == SOURCE_HASSIO @catch_vlc_errors async def async_update(self) -> None: @@ -316,7 +317,9 @@ class VlcDevice(MediaPlayerEntity): ) # If media ID is a relative URL, we serve it from HA. - media_id = async_process_play_media_url(self.hass, media_id) + media_id = async_process_play_media_url( + self.hass, media_id, for_supervisor_network=self._using_addon + ) await self._vlc.add(media_id) self._state = STATE_PLAYING diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index a8c4b3cf458..76c51fa29d2 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,7 @@ """Network helpers.""" from __future__ import annotations +from collections.abc import Callable from contextlib import suppress from ipaddress import ip_address from typing import cast @@ -15,6 +16,7 @@ from homeassistant.util.network import is_ip_address, is_loopback, normalize_url TYPE_URL_INTERNAL = "internal_url" TYPE_URL_EXTERNAL = "external_url" +SUPERVISOR_NETWORK_HOST = "homeassistant" class NoURLAvailableError(HomeAssistantError): @@ -33,6 +35,31 @@ def is_internal_request(hass: HomeAssistant) -> bool: return False +@bind_hass +def get_supervisor_network_url( + hass: HomeAssistant, *, allow_ssl: bool = False +) -> str | None: + """Get URL for home assistant within supervisor network.""" + if hass.config.api is None or not hass.components.hassio.is_hassio(): + return None + + scheme = "http" + if hass.config.api.use_ssl: + # Certificate won't be valid for hostname so this URL usually won't work + if not allow_ssl: + return None + + scheme = "https" + + return str( + yarl.URL.build( + scheme=scheme, + host=SUPERVISOR_NETWORK_HOST, + port=hass.config.api.port, + ) + ) + + def is_hass_url(hass: HomeAssistant, url: str) -> bool: """Return if the URL points at this Home Assistant instance.""" parsed = yarl.URL(normalize_url(url)) @@ -53,11 +80,13 @@ def is_hass_url(hass: HomeAssistant, url: str) -> bool: except NoURLAvailableError: return None + potential_base_factory: Callable[[], str | None] for potential_base_factory in ( lambda: hass.config.internal_url, lambda: hass.config.external_url, cloud_url, host_ip, + lambda: get_supervisor_network_url(hass, allow_ssl=True), ): potential_base = potential_base_factory() diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index ba7a93fc3a3..5e4bac2c635 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -8,9 +8,11 @@ from homeassistant.components.media_player.browse_media import ( ) from homeassistant.config import async_process_ha_core_config +from tests.common import mock_component -@pytest.fixture -def mock_sign_path(): + +@pytest.fixture(name="mock_sign_path") +def fixture_mock_sign_path(): """Mock sign path.""" with patch( "homeassistant.components.media_player.browse_media.async_sign_path", @@ -58,3 +60,35 @@ async def test_process_play_media_url(hass, mock_sign_path): ) == "http://192.168.123.123:8123/path?hello=world" ) + + +async def test_process_play_media_url_for_addon(hass, mock_sign_path): + """Test it uses the hostname for an addon if available.""" + await async_process_ha_core_config( + hass, + { + "internal_url": "http://example.local:8123", + "external_url": "https://example.com", + }, + ) + + # Not hassio or hassio not loaded yet, don't use supervisor network url + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + != "http://homeassistant:8123/path?authSig=bla" + ) + + # Is hassio and not SSL, use an supervisor network url + mock_component(hass, "hassio") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + == "http://homeassistant:8123/path?authSig=bla" + ) + + # Hassio loaded but using SSL, don't use an supervisor network url + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + != "https://homeassistant:8123/path?authSig=bla" + ) diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 0838375fd1f..0c9e8361104 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -12,6 +12,7 @@ from homeassistant.helpers.network import ( _get_external_url, _get_internal_url, _get_request_host, + get_supervisor_network_url, get_url, is_hass_url, is_internal_request, @@ -715,3 +716,41 @@ async def test_is_hass_url(hass): assert is_hass_url(hass, "https://example.nabu.casa") is True assert is_hass_url(hass, "http://example.nabu.casa:443") is False assert is_hass_url(hass, "http://example.nabu.casa") is False + + +async def test_is_hass_url_addon_url(hass): + """Test is_hass_url with a supervisor network URL.""" + assert is_hass_url(hass, "http://homeassistant:8123") is False + + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + assert is_hass_url(hass, "http://homeassistant:8123") is False + + mock_component(hass, "hassio") + assert is_hass_url(hass, "http://homeassistant:8123") + assert not is_hass_url(hass, "https://homeassistant:8123") + + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert not is_hass_url(hass, "http://homeassistant:8123") + assert is_hass_url(hass, "https://homeassistant:8123") + + +async def test_get_supervisor_network_url(hass): + """Test get_supervisor_network_url.""" + assert get_supervisor_network_url(hass) is None + + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + await async_process_ha_core_config(hass, {}) + assert get_supervisor_network_url(hass) is None + + mock_component(hass, "hassio") + assert get_supervisor_network_url(hass) == "http://homeassistant:8123" + + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert get_supervisor_network_url(hass) is None + assert ( + get_supervisor_network_url(hass, allow_ssl=True) == "https://homeassistant:8123" + ) From e9496869da79ffca49a651b39404c462f675fafc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 2 Mar 2022 07:23:36 +0100 Subject: [PATCH 0168/1054] Rework mysensors test fixtures (#67331) * Reworks mysensors test fixtures * Fix missing autospecced methods --- tests/components/mysensors/conftest.py | 66 ++++++++++++++++------- tests/components/mysensors/test_init.py | 8 +-- tests/components/mysensors/test_sensor.py | 14 +++-- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index fe98b3f7e0a..7e38d42f011 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import AsyncGenerator, Callable, Generator import json from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from mysensors import BaseSyncGateway from mysensors.persistence import MySensorsJSONDecoder @@ -63,33 +63,44 @@ async def serial_transport_fixture( """Mock a serial transport.""" with patch( "mysensors.gateway_serial.AsyncTransport", autospec=True - ) as transport_class, patch("mysensors.AsyncTasks", autospec=True) as tasks_class: - tasks = tasks_class.return_value - tasks.persistence = MagicMock + ) as transport_class, patch("mysensors.task.OTAFirmware", autospec=True), patch( + "mysensors.task.load_fw", autospec=True + ), patch( + "mysensors.task.Persistence", autospec=True + ) as persistence_class: + persistence = persistence_class.return_value - mock_gateway_features(tasks, transport_class, gateway_nodes) + mock_gateway_features(persistence, transport_class, gateway_nodes) yield transport_class def mock_gateway_features( - tasks: MagicMock, transport_class: MagicMock, nodes: dict[int, Sensor] + persistence: MagicMock, transport_class: MagicMock, nodes: dict[int, Sensor] ) -> None: """Mock the gateway features.""" - async def mock_start_persistence() -> None: + async def mock_schedule_save_sensors() -> None: """Load nodes from via persistence.""" gateway = transport_class.call_args[0][0] gateway.sensors.update(nodes) - tasks.start_persistence.side_effect = mock_start_persistence + persistence.schedule_save_sensors = AsyncMock( + side_effect=mock_schedule_save_sensors + ) + # For some reason autospeccing does not recognize these methods. + persistence.safe_load_sensors = MagicMock() + persistence.save_sensors = MagicMock() - async def mock_start() -> None: + async def mock_connect() -> None: """Mock the start method.""" + transport.connect_task = MagicMock() gateway = transport_class.call_args[0][0] gateway.on_conn_made(gateway) - tasks.start.side_effect = mock_start + transport = transport_class.return_value + transport.connect_task = None + transport.connect.side_effect = mock_connect @pytest.fixture(name="transport") @@ -98,6 +109,12 @@ def transport_fixture(serial_transport: MagicMock) -> MagicMock: return serial_transport +@pytest.fixture +def transport_write(transport: MagicMock) -> MagicMock: + """Return the transport mock that accepts string messages.""" + return transport.return_value.send + + @pytest.fixture(name="serial_entry") async def serial_entry_fixture(hass: HomeAssistant) -> MockConfigEntry: """Create a config entry for a serial gateway.""" @@ -119,16 +136,28 @@ def config_entry_fixture(serial_entry: MockConfigEntry) -> MockConfigEntry: return serial_entry -@pytest.fixture -async def integration( +@pytest.fixture(name="integration") +async def integration_fixture( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry -) -> AsyncGenerator[tuple[MockConfigEntry, Callable[[str], None]], None]: +) -> AsyncGenerator[MockConfigEntry, None]: """Set up the mysensors integration with a config entry.""" device = config_entry.data[CONF_DEVICE] config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} config_entry.add_to_hass(hass) - def receive_message(message_string: str) -> None: + with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + yield config_entry + + +@pytest.fixture +def receive_message( + transport: MagicMock, integration: MockConfigEntry +) -> Callable[[str], None]: + """Receive a message for the gateway.""" + + def receive_message_callback(message_string: str) -> None: """Receive a message with the transport. The message_string parameter is a string in the MySensors message format. @@ -137,14 +166,13 @@ async def integration( # node_id;child_id;command;ack;type;payload\n gateway.logic(message_string) - with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): - await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - yield config_entry, receive_message + return receive_message_callback @pytest.fixture(name="gateway") -def gateway_fixture(transport, integration) -> BaseSyncGateway: +def gateway_fixture( + transport: MagicMock, integration: MockConfigEntry +) -> BaseSyncGateway: """Return a setup gateway.""" return transport.call_args[0][0] diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 6f97c312ec0..bb5d77dc7e3 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -1,8 +1,8 @@ """Test function in __init__.py.""" from __future__ import annotations -from collections.abc import Callable -from typing import Any, Awaitable +from collections.abc import Awaitable, Callable +from typing import Any from unittest.mock import patch from aiohttp import ClientWebSocketResponse @@ -359,14 +359,14 @@ async def test_import( async def test_remove_config_entry_device( hass: HomeAssistant, gps_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, gateway: BaseSyncGateway, hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], ) -> None: """Test that a device can be removed ok.""" entity_id = "sensor.gps_sensor_1_1" node_id = 1 - config_entry, _ = integration + config_entry = integration assert await async_setup_component(hass, "config", {}) await hass.async_block_till_done() diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 0774d480c98..45fe98b98c7 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -29,11 +29,10 @@ from tests.common import MockConfigEntry async def test_gps_sensor( hass: HomeAssistant, gps_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + receive_message: Callable[[str], None], ) -> None: """Test a gps sensor.""" entity_id = "sensor.gps_sensor_1_1" - _, receive_message = integration state = hass.states.get(entity_id) @@ -59,7 +58,7 @@ async def test_gps_sensor( async def test_power_sensor( hass: HomeAssistant, power_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test a power sensor.""" entity_id = "sensor.power_sensor_1_1" @@ -76,7 +75,7 @@ async def test_power_sensor( async def test_energy_sensor( hass: HomeAssistant, energy_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test an energy sensor.""" entity_id = "sensor.energy_sensor_1_1" @@ -93,7 +92,7 @@ async def test_energy_sensor( async def test_sound_sensor( hass: HomeAssistant, sound_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test a sound sensor.""" entity_id = "sensor.sound_sensor_1_1" @@ -109,7 +108,7 @@ async def test_sound_sensor( async def test_distance_sensor( hass: HomeAssistant, distance_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + integration: MockConfigEntry, ) -> None: """Test a distance sensor.""" entity_id = "sensor.distance_sensor_1_1" @@ -129,14 +128,13 @@ async def test_distance_sensor( async def test_temperature_sensor( hass: HomeAssistant, temperature_sensor: Sensor, - integration: tuple[MockConfigEntry, Callable[[str], None]], + receive_message: Callable[[str], None], unit_system: UnitSystem, unit: str, ) -> None: """Test a temperature sensor.""" entity_id = "sensor.temperature_sensor_1_1" hass.config.units = unit_system - _, receive_message = integration temperature = "22.0" message_string = f"1;1;1;0;0;{temperature}\n" From e9a7d4ddb20a3287c5c1634ed6965a2b66977713 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 08:48:40 +0100 Subject: [PATCH 0169/1054] Bump docker/login-action from 1.14.0 to 1.14.1 (#67462) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index ccb76121792..c4bfe2fcb20 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -122,13 +122,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -187,13 +187,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -252,13 +252,13 @@ jobs: uses: actions/checkout@v2.4.0 - name: Login to DockerHub - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.0 + uses: docker/login-action@v1.14.1 with: registry: ghcr.io username: ${{ github.repository_owner }} From d7d41e2a4d3ae521edad19bdcbd7567f65fa5c08 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 2 Mar 2022 09:32:02 +0100 Subject: [PATCH 0170/1054] Bump actions/checkout from 2.4.0 to 3.0.0 (#67456) --- .github/workflows/builder.yml | 12 ++++++------ .github/workflows/ci.yaml | 28 ++++++++++++++-------------- .github/workflows/translations.yaml | 4 ++-- .github/workflows/wheels.yml | 6 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index c4bfe2fcb20..65debe2f82f 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -24,7 +24,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 with: fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 @@ -100,7 +100,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' @@ -173,7 +173,7 @@ jobs: - tinker steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set build additional args run: | @@ -216,7 +216,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -249,7 +249,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Login to DockerHub uses: docker/login-action@v1.14.1 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a5b4f37cdf5..cf96d3254ff 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Filter for core changes uses: dorny/paths-filter@v2.10.2 id: core @@ -151,7 +151,7 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v3.0.0 @@ -232,7 +232,7 @@ jobs: - prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 id: python @@ -282,7 +282,7 @@ jobs: - prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 id: python @@ -333,7 +333,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 id: python @@ -375,7 +375,7 @@ jobs: - prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 id: python @@ -485,7 +485,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -509,7 +509,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 id: python @@ -544,7 +544,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Generate partial Python venv restore key id: generate-python-key run: >- @@ -612,7 +612,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -654,7 +654,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -698,7 +698,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -741,7 +741,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.7 @@ -845,7 +845,7 @@ jobs: - pytest steps: - name: Check out code from GitHub - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Download all coverage artifacts uses: actions/download-artifact@v2 - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index e778f3cd6b9..55273226b89 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v3.0.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0b73ee9fa70..448389fb5bb 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,7 +22,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Get information id: info @@ -74,7 +74,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Download env_file uses: actions/download-artifact@v2 @@ -115,7 +115,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3.0.0 - name: Download env_file uses: actions/download-artifact@v2 From 4ac1746e2891c1dc95d287ac3deedb0b008037de Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Wed, 2 Mar 2022 04:08:03 -0500 Subject: [PATCH 0171/1054] Update Pyoctoprint to 1.8 (#67459) --- homeassistant/components/octoprint/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 385ab88428a..4086f9fbe20 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -3,7 +3,7 @@ "name": "OctoPrint", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/octoprint", - "requirements": ["pyoctoprintapi==0.1.7"], + "requirements": ["pyoctoprintapi==0.1.8"], "codeowners": ["@rfleming71"], "zeroconf": ["_octoprint._tcp.local."], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3fff1c2694b..58e005bddba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1675,7 +1675,7 @@ pynzbgetapi==0.2.0 pyobihai==1.3.1 # homeassistant.components.octoprint -pyoctoprintapi==0.1.7 +pyoctoprintapi==0.1.8 # homeassistant.components.ombi pyombi==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d0db0fa26c..d4167c024ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1101,7 +1101,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.octoprint -pyoctoprintapi==0.1.7 +pyoctoprintapi==0.1.8 # homeassistant.components.openuv pyopenuv==2021.11.0 From 3c3a27584a088c55143c696450dae323f44de51d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 2 Mar 2022 10:36:23 +0100 Subject: [PATCH 0172/1054] Rfxtrx correct overzealous type checking (#67437) --- homeassistant/components/rfxtrx/config_flow.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 754eaeca9ff..ea0f7478740 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -36,7 +36,13 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from . import DOMAIN, DeviceTuple, get_device_id, get_rfx_object +from . import ( + DOMAIN, + DeviceTuple, + get_device_id, + get_device_tuple_from_identifiers, + get_rfx_object, +) from .binary_sensor import supported as binary_supported from .const import ( CONF_AUTOMATIC_ADD, @@ -64,7 +70,7 @@ RECV_MODES = sorted(itertools.chain(*rfxtrxmod.lowlevel.Status.RECMODES)) class DeviceData(TypedDict): """Dict data representing a device entry.""" - event_code: str + event_code: str | None device_id: DeviceTuple @@ -398,15 +404,15 @@ class OptionsFlow(config_entries.OptionsFlow): def _get_device_data(self, entry_id) -> DeviceData: """Get event code based on device identifier.""" - event_code: str + event_code: str | None = None entry = self._device_registry.async_get(entry_id) assert entry - device_id = cast(DeviceTuple, next(iter(entry.identifiers))[1:]) + device_id = get_device_tuple_from_identifiers(entry.identifiers) + assert device_id 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 = cast(str, packet_id) break - assert event_code return DeviceData(event_code=event_code, device_id=device_id) @callback From beb4756eb841cd5dbdb6c82c433af99249a82565 Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 2 Mar 2022 13:00:33 +0100 Subject: [PATCH 0173/1054] Address late review of flipr (#67477) --- homeassistant/components/flipr/__init__.py | 3 +-- tests/components/flipr/test_binary_sensor.py | 3 ++- tests/components/flipr/test_sensor.py | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 1a9f3dc0314..3281410ec2d 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -75,8 +75,7 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator): self.client.get_pool_measure_latest, self.flipr_id ) except (FliprError) as error: - _LOGGER.error(error) - raise UpdateFailed from error + raise UpdateFailed(error) from error return data diff --git a/tests/components/flipr/test_binary_sensor.py b/tests/components/flipr/test_binary_sensor.py index 48f9361723c..fc24ddee340 100644 --- a/tests/components/flipr/test_binary_sensor.py +++ b/tests/components/flipr/test_binary_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as entity_reg from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -36,7 +37,7 @@ async def test_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 45816801472..c5ab3dc1541 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as entity_reg from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -44,7 +45,7 @@ async def test_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", @@ -102,7 +103,7 @@ async def test_error_flipr_api_sensors(hass: HomeAssistant) -> None: entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = entity_reg.async_get(hass) with patch( "flipr_api.FliprAPIRestClient.get_pool_measure_latest", From aac4036c0c8d47b2342e76124c10a32a86d3d636 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 2 Mar 2022 14:30:54 +0200 Subject: [PATCH 0174/1054] Bump aioshelly to 1.0.11 (#67476) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1e9f34608eb..1d4d47748be 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.10"], + "requirements": ["aioshelly==1.0.11"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 58e005bddba..6b9f93a0ab5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.10 +aioshelly==1.0.11 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4167c024ea..5c54f0a6e83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aioridwell==2021.12.2 aiosenseme==0.6.1 # homeassistant.components.shelly -aioshelly==1.0.10 +aioshelly==1.0.11 # homeassistant.components.steamist aiosteamist==0.3.1 From 063872a0c0650e5c53c10201e8f6ed34e8917744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 2 Mar 2022 13:55:05 +0100 Subject: [PATCH 0175/1054] Split meta image creation (#67480) --- .github/workflows/builder.yml | 104 ++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 65debe2f82f..b7b00c3ef59 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -243,21 +243,28 @@ jobs: channel: beta publish_container: - name: Publish meta container + name: Publish meta container for ${{ matrix.registry }} if: github.repository_owner == 'home-assistant' needs: ["init", "build_base"] runs-on: ubuntu-latest + strategy: + matrix: + registry: + - "ghcr.io/home-assistant" + - "homeassistant" steps: - name: Checkout the repository uses: actions/checkout@v3.0.0 - name: Login to DockerHub + if: matrix.registry == 'homeassistant' uses: docker/login-action@v1.14.1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry + if: matrix.registry == 'ghcr.io/home-assistant' uses: docker/login-action@v1.14.1 with: registry: ghcr.io @@ -273,38 +280,37 @@ jobs: export DOCKER_CLI_EXPERIMENTAL=enabled function create_manifest() { - local docker_reg=${1} - local tag_l=${2} - local tag_r=${3} + local tag_l=${1} + local tag_r=${2} - docker manifest create "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/amd64-homeassistant:${tag_r}" \ - "${docker_reg}/i386-homeassistant:${tag_r}" \ - "${docker_reg}/armhf-homeassistant:${tag_r}" \ - "${docker_reg}/armv7-homeassistant:${tag_r}" \ - "${docker_reg}/aarch64-homeassistant:${tag_r}" + docker manifest create "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/i386-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \ + "${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/amd64-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/amd64-homeassistant:${tag_r}" \ --os linux --arch amd64 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/i386-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/i386-homeassistant:${tag_r}" \ --os linux --arch 386 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/armhf-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/armhf-homeassistant:${tag_r}" \ --os linux --arch arm --variant=v6 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/armv7-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/armv7-homeassistant:${tag_r}" \ --os linux --arch arm --variant=v7 - docker manifest annotate "${docker_reg}/home-assistant:${tag_l}" \ - "${docker_reg}/aarch64-homeassistant:${tag_r}" \ + docker manifest annotate "${{ matrix.registry }}/home-assistant:${tag_l}" \ + "${{ matrix.registry }}/aarch64-homeassistant:${tag_r}" \ --os linux --arch arm64 --variant=v8 - docker manifest push --purge "${docker_reg}/home-assistant:${tag_l}" + docker manifest push --purge "${{ matrix.registry }}/home-assistant:${tag_l}" } function validate_image() { @@ -315,36 +321,34 @@ jobs: fi } - for docker_reg in "homeassistant" "ghcr.io/home-assistant"; do - docker pull "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" - docker pull "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}" + docker pull "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/amd64-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/i386-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/armhf-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/armv7-homeassistant:${{ needs.init.outputs.version }}" - validate_image "${docker_reg}/aarch64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/amd64-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/i386-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/armhf-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/armv7-homeassistant:${{ needs.init.outputs.version }}" + validate_image "${{ matrix.registry }}/aarch64-homeassistant:${{ needs.init.outputs.version }}" - # Create version tag - create_manifest "${docker_reg}" "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" + # Create version tag + create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}" - # Create general tags - if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then - create_manifest "${docker_reg}" "dev" "${{ needs.init.outputs.version }}" - elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then - create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" - else - create_manifest "${docker_reg}" "stable" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "latest" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "beta" "${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "rc" "${{ needs.init.outputs.version }}" + # Create general tags + if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then + create_manifest"dev" "${{ needs.init.outputs.version }}" + elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then + create_manifest "beta" "${{ needs.init.outputs.version }}" + create_manifest "rc" "${{ needs.init.outputs.version }}" + else + create_manifest "stable" "${{ needs.init.outputs.version }}" + create_manifest "latest" "${{ needs.init.outputs.version }}" + create_manifest "beta" "${{ needs.init.outputs.version }}" + create_manifest "rc" "${{ needs.init.outputs.version }}" - # Create series version tag (e.g. 2021.6) - v="${{ needs.init.outputs.version }}" - create_manifest "${docker_reg}" "${v%.*}" "${{ needs.init.outputs.version }}" - fi - done + # Create series version tag (e.g. 2021.6) + v="${{ needs.init.outputs.version }}" + create_manifest "${v%.*}" "${{ needs.init.outputs.version }}" + fi From 2ff1101a15b099e006f8a04df9e57d9b5c21ec01 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 2 Mar 2022 12:58:39 +0000 Subject: [PATCH 0176/1054] Bump to aiohomekit 0.7.15 (#67470) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 44de321db4a..dfd45991b3f 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.14"], + "requirements": ["aiohomekit==0.7.15"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 6b9f93a0ab5..7cfd3f3242a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.14 +aiohomekit==0.7.15 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c54f0a6e83..f46e0adf646 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.14 +aiohomekit==0.7.15 # homeassistant.components.emulated_hue # homeassistant.components.http From a5806fb8688930a9e37eb389f9986cdd3240ab8d Mon Sep 17 00:00:00 2001 From: Igor Pakhomov Date: Wed, 2 Mar 2022 15:08:19 +0200 Subject: [PATCH 0177/1054] Add buttons for dmaker.airfresh.a1/t2017 to xiaomi_miio (#67065) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/xiaomi_miio/__init__.py | 1 + .../components/xiaomi_miio/button.py | 120 ++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 homeassistant/components/xiaomi_miio/button.py diff --git a/.coveragerc b/.coveragerc index 1e9d2ba98a0..11db4118ebd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1408,6 +1408,7 @@ omit = homeassistant/components/xiaomi_miio/air_quality.py homeassistant/components/xiaomi_miio/alarm_control_panel.py homeassistant/components/xiaomi_miio/binary_sensor.py + homeassistant/components/xiaomi_miio/button.py homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py homeassistant/components/xiaomi_miio/fan.py diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 2849b249762..e1b9a044201 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -91,6 +91,7 @@ GATEWAY_PLATFORMS = [ SWITCH_PLATFORMS = [Platform.SWITCH] FAN_PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.FAN, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py new file mode 100644 index 00000000000..279c72ca8af --- /dev/null +++ b/homeassistant/components/xiaomi_miio/button.py @@ -0,0 +1,120 @@ +"""Support for Xiaomi buttons.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + CONF_MODEL, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, + MODEL_AIRFRESH_A1, + MODEL_AIRFRESH_T2017, +) +from .device import XiaomiCoordinatedMiioEntity + +ATTR_RESET_DUST_FILTER = "reset_dust_filter" +ATTR_RESET_UPPER_FILTER = "reset_upper_filter" + + +@dataclass +class XiaomiMiioButtonDescription(ButtonEntityDescription): + """A class that describes button entities.""" + + method_press: str = "" + method_press_error_message: str = "" + + +BUTTON_TYPES = ( + XiaomiMiioButtonDescription( + key=ATTR_RESET_DUST_FILTER, + name="Reset Dust Filter", + icon="mdi:air-filter", + method_press="reset_dust_filter", + method_press_error_message="Resetting the dust filter lifetime failed", + entity_category=EntityCategory.CONFIG, + ), + XiaomiMiioButtonDescription( + key=ATTR_RESET_UPPER_FILTER, + name="Reset Upper Filter", + icon="mdi:air-filter", + method_press="reset_upper_filter", + method_press_error_message="Resetting the upper filter lifetime failed.", + entity_category=EntityCategory.CONFIG, + ), +) + +MODEL_TO_BUTTON_MAP: dict[str, tuple[str, ...]] = { + MODEL_AIRFRESH_A1: (ATTR_RESET_DUST_FILTER,), + MODEL_AIRFRESH_T2017: ( + ATTR_RESET_DUST_FILTER, + ATTR_RESET_UPPER_FILTER, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the button from a config entry.""" + model = config_entry.data[CONF_MODEL] + + if model not in MODEL_TO_BUTTON_MAP: + return + + entities = [] + buttons = MODEL_TO_BUTTON_MAP[model] + unique_id = config_entry.unique_id + device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + + for description in BUTTON_TYPES: + if description.key not in buttons: + continue + + entities.append( + XiaomiGenericCoordinatedButton( + f"{config_entry.title} {description.name}", + device, + config_entry, + f"{description.key}_{unique_id}", + coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): + """A button implementation for Xiaomi.""" + + entity_description: XiaomiMiioButtonDescription + + _attr_device_class = ButtonDeviceClass.RESTART + + def __init__(self, name, device, entry, unique_id, coordinator, description): + """Initialize the plug switch.""" + super().__init__(name, device, entry, unique_id, coordinator) + self.entity_description = description + + async def async_press(self, **kwargs: Any) -> None: + """Press the button.""" + method = getattr(self._device, self.entity_description.method_press) + await self._try_command( + self.entity_description.method_press_error_message, + method, + ) From 0974abf9e290d5a545f349f17c0f52f2fcd7a571 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 2 Mar 2022 14:00:48 +0000 Subject: [PATCH 0178/1054] Remove Ecobee homekit vendor extensions that just don't work (#67474) --- .../components/homekit_controller/number.py | 36 --------- .../specific_devices/test_ecobee3.py | 80 ------------------- 2 files changed, 116 deletions(-) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 5fcb5027640..b994bc80f4a 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -48,42 +48,6 @@ NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), - CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_COOL: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_COOL, - name="Home Cool Target", - icon="mdi:thermometer-minus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_HEAT: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_HOME_TARGET_HEAT, - name="Home Heat Target", - icon="mdi:thermometer-plus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_COOL: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_COOL, - name="Sleep Cool Target", - icon="mdi:thermometer-minus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_HEAT: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_SLEEP_TARGET_HEAT, - name="Sleep Heat Target", - icon="mdi:thermometer-plus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_COOL: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_COOL, - name="Away Cool Target", - icon="mdi:thermometer-minus", - entity_category=EntityCategory.CONFIG, - ), - CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_HEAT: NumberEntityDescription( - key=CharacteristicsTypes.VENDOR_ECOBEE_AWAY_TARGET_HEAT, - name="Away Heat Target", - icon="mdi:thermometer-plus", - entity_category=EntityCategory.CONFIG, - ), } diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 83378650b97..3c47195b442 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -14,12 +14,10 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.number import NumberMode from homeassistant.components.sensor import SensorStateClass from homeassistant.config_entries import ConfigEntryState from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import EntityCategory from tests.components.homekit_controller.common import ( HUB_TEST_ACCESSORY_ID, @@ -123,84 +121,6 @@ async def test_ecobee3_setup(hass): }, state="heat", ), - EntityTestInfo( - entity_id="number.homew_home_cool_target", - friendly_name="HomeW Home Cool Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:35", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 33.3, - "min": 18.3, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="24.4", - ), - EntityTestInfo( - entity_id="number.homew_home_heat_target", - friendly_name="HomeW Home Heat Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:34", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 26.1, - "min": 7.2, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="22.2", - ), - EntityTestInfo( - entity_id="number.homew_sleep_cool_target", - friendly_name="HomeW Sleep Cool Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:37", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 33.3, - "min": 18.3, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="27.8", - ), - EntityTestInfo( - entity_id="number.homew_sleep_heat_target", - friendly_name="HomeW Sleep Heat Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:36", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 26.1, - "min": 7.2, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="17.8", - ), - EntityTestInfo( - entity_id="number.homew_away_cool_target", - friendly_name="HomeW Away Cool Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:39", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 33.3, - "min": 18.3, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="26.7", - ), - EntityTestInfo( - entity_id="number.homew_away_heat_target", - friendly_name="HomeW Away Heat Target", - unique_id="homekit-123456789012-aid:1-sid:16-cid:38", - entity_category=EntityCategory.CONFIG, - capabilities={ - "max": 26.1, - "min": 7.2, - "mode": NumberMode.AUTO, - "step": 0.1, - }, - state="18.9", - ), EntityTestInfo( entity_id="sensor.homew_current_temperature", friendly_name="HomeW Current Temperature", From 5b8cf379a33a101acd07e4e4d901c808bd24b21e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Mar 2022 16:49:48 +0100 Subject: [PATCH 0179/1054] Improve mobile_app key handling (#67429) --- homeassistant/components/mobile_app/const.py | 1 + .../components/mobile_app/helpers.py | 74 ++++-- .../components/mobile_app/webhook.py | 25 +- tests/components/mobile_app/test_http_api.py | 44 ++++ tests/components/mobile_app/test_webhook.py | 225 +++++++++++++++++- 5 files changed, 344 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index a2a4e15ee72..ba81a0484cf 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -28,6 +28,7 @@ ATTR_CONFIG_ENTRY_ID = "entry_id" ATTR_DEVICE_NAME = "device_name" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" +ATTR_NO_LEGACY_ENCRYPTION = "no_legacy_encryption" ATTR_OS_NAME = "os_name" ATTR_OS_VERSION = "os_version" ATTR_PUSH_WEBSOCKET_CHANNEL = "push_websocket_channel" diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index b7d38357a78..545c3511fc9 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -7,7 +7,7 @@ import json import logging from aiohttp.web import Response, json_response -from nacl.encoding import Base64Encoder +from nacl.encoding import Base64Encoder, HexEncoder, RawEncoder from nacl.secret import SecretBox from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON @@ -23,6 +23,7 @@ from .const import ( ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_SECRET, @@ -34,7 +35,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def setup_decrypt() -> tuple[int, Callable]: +def setup_decrypt(key_encoder) -> tuple[int, Callable]: """Return decryption function and length of key. Async friendly. @@ -42,12 +43,14 @@ def setup_decrypt() -> tuple[int, Callable]: def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return SecretBox(key, encoder=key_encoder).decrypt( + ciphertext, encoder=Base64Encoder + ) return (SecretBox.KEY_SIZE, decrypt) -def setup_encrypt() -> tuple[int, Callable]: +def setup_encrypt(key_encoder) -> tuple[int, Callable]: """Return encryption function and length of key. Async friendly. @@ -55,15 +58,22 @@ def setup_encrypt() -> tuple[int, Callable]: def encrypt(ciphertext, key): """Encrypt ciphertext using key.""" - return SecretBox(key).encrypt(ciphertext, encoder=Base64Encoder) + return SecretBox(key, encoder=key_encoder).encrypt( + ciphertext, encoder=Base64Encoder + ) return (SecretBox.KEY_SIZE, encrypt) -def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: +def _decrypt_payload_helper( + key: str | None, + ciphertext: str, + get_key_bytes: Callable[[str, int], str | bytes], + key_encoder, +) -> dict[str, str] | None: """Decrypt encrypted payload.""" try: - keylen, decrypt = setup_decrypt() + keylen, decrypt = setup_decrypt(key_encoder) except OSError: _LOGGER.warning("Ignoring encrypted payload because libsodium not installed") return None @@ -72,18 +82,33 @@ def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: _LOGGER.warning("Ignoring encrypted payload because no decryption key known") return None - key_bytes = key.encode("utf-8") - key_bytes = key_bytes[:keylen] - key_bytes = key_bytes.ljust(keylen, b"\0") + key_bytes = get_key_bytes(key, keylen) - try: - msg_bytes = decrypt(ciphertext, key_bytes) - message = json.loads(msg_bytes.decode("utf-8")) - _LOGGER.debug("Successfully decrypted mobile_app payload") - return message - except ValueError: - _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") - return None + msg_bytes = decrypt(ciphertext, key_bytes) + message = json.loads(msg_bytes.decode("utf-8")) + _LOGGER.debug("Successfully decrypted mobile_app payload") + return message + + +def _decrypt_payload(key: str | None, ciphertext: str) -> dict[str, str] | None: + """Decrypt encrypted payload.""" + + def get_key_bytes(key: str, keylen: int) -> str: + return key + + return _decrypt_payload_helper(key, ciphertext, get_key_bytes, HexEncoder) + + +def _decrypt_payload_legacy(key: str | None, ciphertext: str) -> dict[str, str] | None: + """Decrypt encrypted payload.""" + + def get_key_bytes(key: str, keylen: int) -> bytes: + key_bytes = key.encode("utf-8") + key_bytes = key_bytes[:keylen] + key_bytes = key_bytes.ljust(keylen, b"\0") + return key_bytes + + return _decrypt_payload_helper(key, ciphertext, get_key_bytes, RawEncoder) def registration_context(registration: dict) -> Context: @@ -158,11 +183,16 @@ def webhook_response( data = json.dumps(data, cls=JSONEncoder) if registration[ATTR_SUPPORTS_ENCRYPTION]: - keylen, encrypt = setup_encrypt() + keylen, encrypt = setup_encrypt( + HexEncoder if ATTR_NO_LEGACY_ENCRYPTION in registration else RawEncoder + ) - key = registration[CONF_SECRET].encode("utf-8") - key = key[:keylen] - key = key.ljust(keylen, b"\0") + if ATTR_NO_LEGACY_ENCRYPTION in registration: + key = registration[CONF_SECRET] + else: + key = registration[CONF_SECRET].encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b"\0") enc_data = encrypt(data.encode("utf-8"), key).decode("utf-8") data = json.dumps({"encrypted": True, "encrypted_data": enc_data}) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 221c4eef733..860b8ef7b53 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -7,6 +7,7 @@ import logging import secrets from aiohttp.web import HTTPBadRequest, Request, Response, json_response +from nacl.exceptions import CryptoError from nacl.secret import SecretBox import voluptuous as vol @@ -58,6 +59,7 @@ from .const import ( ATTR_EVENT_TYPE, ATTR_MANUFACTURER, ATTR_MODEL, + ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, @@ -97,6 +99,7 @@ from .const import ( ) from .helpers import ( _decrypt_payload, + _decrypt_payload_legacy, empty_okay_response, error_response, registration_context, @@ -191,7 +194,27 @@ async def handle_webhook( if req_data[ATTR_WEBHOOK_ENCRYPTED]: enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] - webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) + try: + webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) + if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data: + data = {**config_entry.data, ATTR_NO_LEGACY_ENCRYPTION: True} + hass.config_entries.async_update_entry(config_entry, data=data) + except CryptoError: + if ATTR_NO_LEGACY_ENCRYPTION not in config_entry.data: + try: + webhook_payload = _decrypt_payload_legacy( + config_entry.data[CONF_SECRET], enc_data + ) + except CryptoError: + _LOGGER.warning( + "Ignoring encrypted payload because unable to decrypt" + ) + except ValueError: + _LOGGER.warning("Ignoring invalid encrypted payload") + else: + _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") + except ValueError: + _LOGGER.warning("Ignoring invalid encrypted payload") if webhook_type not in WEBHOOK_COMMANDS: _LOGGER.error( diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 5d92418bba2..4c4e9b54ccf 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -1,4 +1,5 @@ """Tests for the mobile_app HTTP API.""" +from binascii import unhexlify from http import HTTPStatus import json from unittest.mock import patch @@ -75,6 +76,49 @@ async def test_registration_encryption(hass, hass_client): assert resp.status == HTTPStatus.CREATED register_json = await resp.json() + key = unhexlify(register_json[CONF_SECRET]) + + payload = json.dumps(RENDER_TEMPLATE["data"]).encode("utf-8") + + data = SecretBox(key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await api_client.post( + f"/api/webhook/{register_json[CONF_WEBHOOK_ID]}", json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = SecretBox(key).decrypt( + webhook_json["encrypted_data"], encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + assert json.loads(decrypted_data) == {"one": "Hello world"} + + +async def test_registration_encryption_legacy(hass, hass_client): + """Test that registrations happen.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + api_client = await hass_client() + + resp = await api_client.post("/api/mobile_app/registrations", json=REGISTER) + + assert resp.status == HTTPStatus.CREATED + register_json = await resp.json() + keylen = SecretBox.KEY_SIZE key = register_json[CONF_SECRET].encode("utf-8") key = key[:keylen] diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 48b61988de2..5f220cf0ebe 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,5 @@ """Webhook tests for mobile_app.""" +from binascii import unhexlify from http import HTTPStatus from unittest.mock import patch @@ -22,7 +23,29 @@ from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE from tests.common import async_mock_service -def encrypt_payload(secret_key, payload): +def encrypt_payload(secret_key, payload, encode_json=True): + """Return a encrypted payload given a key and dictionary of data.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + prepped_key = unhexlify(secret_key) + + if encode_json: + payload = json.dumps(payload) + payload = payload.encode("utf-8") + + return ( + SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") + ) + + +def encrypt_payload_legacy(secret_key, payload, encode_json=True): """Return a encrypted payload given a key and dictionary of data.""" try: from nacl.encoding import Base64Encoder @@ -38,7 +61,9 @@ def encrypt_payload(secret_key, payload): prepped_key = prepped_key[:keylen] prepped_key = prepped_key.ljust(keylen, b"\0") - payload = json.dumps(payload).encode("utf-8") + if encode_json: + payload = json.dumps(payload) + payload = payload.encode("utf-8") return ( SecretBox(prepped_key).encrypt(payload, encoder=Base64Encoder).decode("utf-8") @@ -56,6 +81,27 @@ def decrypt_payload(secret_key, encrypted_data): import json + prepped_key = unhexlify(secret_key) + + decrypted_data = SecretBox(prepped_key).decrypt( + encrypted_data, encoder=Base64Encoder + ) + decrypted_data = decrypted_data.decode("utf-8") + + return json.loads(decrypted_data) + + +def decrypt_payload_legacy(secret_key, encrypted_data): + """Return a decrypted payload given a key and a string of encrypted data.""" + try: + from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + keylen = SecretBox.KEY_SIZE prepped_key = secret_key.encode("utf-8") prepped_key = prepped_key[:keylen] @@ -273,6 +319,181 @@ async def test_webhook_handle_decryption(webhook_client, create_registrations): assert decrypted_data == {"one": "Hello world"} +async def test_webhook_handle_decryption_legacy(webhook_client, create_registrations): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + +async def test_webhook_handle_decryption_fail( + webhook_client, create_registrations, caplog +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send valid data + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + assert decrypted_data == {"one": "Hello world"} + caplog.clear() + + # Send invalid JSON data + data = encrypt_payload(key, "{not_valid", encode_json=False) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring invalid encrypted payload" in caplog.text + caplog.clear() + + # Break the key, and send JSON data + data = encrypt_payload(key[::-1], RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring encrypted payload because unable to decrypt" in caplog.text + + +async def test_webhook_handle_decryption_legacy_fail( + webhook_client, create_registrations, caplog +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send valid data using legacy method + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + assert decrypted_data == {"one": "Hello world"} + caplog.clear() + + # Send invalid JSON data + data = encrypt_payload_legacy(key, "{not_valid", encode_json=False) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload_legacy(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring invalid encrypted payload" in caplog.text + caplog.clear() + + # Break the key, and send JSON data + data = encrypt_payload_legacy(key[::-1], RENDER_TEMPLATE["data"]) + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + webhook_json = await resp.json() + assert decrypt_payload_legacy(key, webhook_json["encrypted_data"]) == {} + assert "Ignoring encrypted payload because unable to decrypt" in caplog.text + + +async def test_webhook_handle_decryption_legacy_upgrade( + webhook_client, create_registrations +): + """Test that we can encrypt/decrypt properly.""" + key = create_registrations[0]["secret"] + + # Send using legacy method + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + # Send using new method + data = encrypt_payload(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {"one": "Hello world"} + + # Send using legacy method - no longer possible + data = encrypt_payload_legacy(key, RENDER_TEMPLATE["data"]) + + container = {"type": "render_template", "encrypted": True, "encrypted_data": data} + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container + ) + + assert resp.status == HTTPStatus.OK + + webhook_json = await resp.json() + assert "encrypted_data" in webhook_json + + # The response should be empty, encrypted with the new method + with pytest.raises(Exception): + decrypt_payload_legacy(key, webhook_json["encrypted_data"]) + decrypted_data = decrypt_payload(key, webhook_json["encrypted_data"]) + + assert decrypted_data == {} + + async def test_webhook_requires_encryption(webhook_client, create_registrations): """Test that encrypted registrations only accept encrypted data.""" resp = await webhook_client.post( From c5dd5e18c0443b332dddfbbf4a8ff03374c5c070 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Mar 2022 16:53:13 +0100 Subject: [PATCH 0180/1054] Improve binary sensor group when member is unknown or unavailable (#67468) --- .../components/group/binary_sensor.py | 14 +++++++++-- tests/components/group/test_binary_sensor.py | 24 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 9c0301d97e6..de0c3d393ca 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -80,7 +81,6 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} self._attr_unique_id = unique_id self._device_class = device_class - self._state: str | None = None self.mode = any if mode: self.mode = all @@ -106,13 +106,23 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): def async_update_group_state(self) -> None: """Query all members and determine the binary sensor group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] + + # filtered_states are members currently in the state machine filtered_states: list[str] = [x.state for x in all_states if x is not None] + + # Set group as unavailable if all members are unavailable self._attr_available = any( state != STATE_UNAVAILABLE for state in filtered_states ) - if STATE_UNAVAILABLE in filtered_states: + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + ) + if not valid_state: + # Set as unknown if any / all member is not unknown or unavailable self._attr_is_on = None else: + # Set as ON if any / all member is ON states = list(map(lambda x: x == STATE_ON, filtered_states)) state = self.mode(states) self._attr_is_on = state diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index 0a85c793aaa..a0872b11f16 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -95,6 +95,16 @@ async def test_state_reporting_all(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + async def test_state_reporting_any(hass): """Test the state reporting.""" @@ -116,11 +126,10 @@ async def test_state_reporting_any(hass): await hass.async_start() await hass.async_block_till_done() - # binary sensors have state off if unavailable hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_OFF) @@ -137,7 +146,6 @@ async def test_state_reporting_any(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - # binary sensors have state off if unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -149,3 +157,13 @@ async def test_state_reporting_any(hass): entry = entity_registry.async_get("binary_sensor.binary_sensor_group") assert entry assert entry.unique_id == "unique_identifier" + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN From 797a9c3de5aae5bcdff5b635b7efa4d018fcd8f9 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Thu, 3 Mar 2022 02:54:47 +1100 Subject: [PATCH 0181/1054] Sort DMS results using only criteria supported by the device (#67475) --- homeassistant/components/dlna_dms/dms.py | 23 +++++- .../dlna_dms/test_dms_device_source.py | 77 ++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 94ac67b0f1a..0fa4d77d005 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -547,7 +547,7 @@ class DmsDeviceSource: children = await self._device.async_browse_direct_children( object_id, metadata_filter=DLNA_BROWSE_FILTER, - sort_criteria=DLNA_SORT_CRITERIA, + sort_criteria=self._sort_criteria, ) return self._didl_to_media_source(base_object, children) @@ -680,6 +680,27 @@ class DmsDeviceSource: """Make an identifier for BrowseMediaSource.""" return f"{self.source_id}/{action}{object_id}" + @property # type: ignore + @functools.cache + def _sort_criteria(self) -> list[str]: + """Return criteria to be used for sorting results. + + The device must be connected before reading this property. + """ + assert self._device + + if self._device.sort_capabilities == ["*"]: + return DLNA_SORT_CRITERIA + + # Filter criteria based on what the device supports. Strings in + # DLNA_SORT_CRITERIA are prefixed with a sign, while those in + # the device's sort_capabilities are not. + return [ + criterion + for criterion in DLNA_SORT_CRITERIA + if criterion[1:] in self._device.sort_capabilities + ] + class Action(StrEnum): """Actions that can be specified in a DMS media-source identifier.""" diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 4ee9cce91ba..d6fcdb267d6 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -8,7 +8,7 @@ from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite import pytest -from homeassistant.components.dlna_dms.const import DOMAIN +from homeassistant.components.dlna_dms.const import DLNA_SORT_CRITERIA, DOMAIN from homeassistant.components.dlna_dms.dms import ( ActionError, DeviceConnectionError, @@ -686,6 +686,81 @@ async def test_browse_media_object( assert not child.children +async def test_browse_object_sort_anything( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sort criteria for children where device allows anything.""" + dms_device_mock.sort_capabilities = ["*"] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be dlna_dms's default + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=DLNA_SORT_CRITERIA + ) + + +async def test_browse_object_sort_superset( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sorting where device allows superset of integration's criteria.""" + dms_device_mock.sort_capabilities = [ + "dc:title", + "upnp:originalTrackNumber", + "upnp:class", + "upnp:artist", + "dc:creator", + "upnp:genre", + ] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be dlna_dms's default + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=DLNA_SORT_CRITERIA + ) + + +async def test_browse_object_sort_subset( + device_source_mock: DmsDeviceSource, dms_device_mock: Mock +) -> None: + """Test sorting where device allows subset of integration's criteria.""" + dms_device_mock.sort_capabilities = [ + "dc:title", + "upnp:class", + ] + + object_id = "0" + dms_device_mock.async_browse_metadata.return_value = didl_lite.Container( + id="0", restricted="false", title="root" + ) + dms_device_mock.async_browse_direct_children.return_value = DmsDevice.BrowseResult( + [], 0, 0, 0 + ) + await device_source_mock.async_browse_object("0") + + # Sort criteria should be reduced to only those allowed, + # and in the order specified by DLNA_SORT_CRITERIA + expected_criteria = ["+upnp:class", "+dc:title"] + dms_device_mock.async_browse_direct_children.assert_awaited_once_with( + object_id, metadata_filter=ANY, sort_criteria=expected_criteria + ) + + async def test_browse_media_path( device_source_mock: DmsDeviceSource, dms_device_mock: Mock ) -> None: From 9f51fd7c6f003e4511f76be3d8347f738914364a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 2 Mar 2022 16:55:33 +0100 Subject: [PATCH 0182/1054] Set fail-fast to false for meta container (#67484) --- .github/workflows/builder.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index b7b00c3ef59..853a5ca0c65 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -248,6 +248,7 @@ jobs: needs: ["init", "build_base"] runs-on: ubuntu-latest strategy: + fail-fast: false matrix: registry: - "ghcr.io/home-assistant" From a97fe7aae041eba15d385c39812f3178f33d2e7c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 2 Mar 2022 17:01:47 +0100 Subject: [PATCH 0183/1054] Mqtt fix issue with displaying non UTF-8 payload (#67471) * Mqtt fix issue with displaying non UTF-8 payload * None or binary * Casting and additional test * casting --- homeassistant/components/mqtt/__init__.py | 13 +++++++++++-- tests/components/mqtt/test_init.py | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b98fe990fe8..090d9cdfa73 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1206,20 +1206,29 @@ async def websocket_subscribe(hass, connection, msg): async def forward_messages(mqttmsg: ReceiveMessage): """Forward events to websocket.""" + try: + payload = cast(bytes, mqttmsg.payload).decode( + DEFAULT_ENCODING + ) # not str because encoding is set to None + except (AttributeError, UnicodeDecodeError): + # Convert non UTF-8 payload to a string presentation + payload = str(mqttmsg.payload) + connection.send_message( websocket_api.event_message( msg["id"], { "topic": mqttmsg.topic, - "payload": mqttmsg.payload, + "payload": payload, "qos": mqttmsg.qos, "retain": mqttmsg.retain, }, ) ) + # Perform UTF-8 decoding directly in callback routine connection.subscriptions[msg["id"]] = await async_subscribe( - hass, msg["topic"], forward_messages + hass, msg["topic"], forward_messages, encoding=None ) connection.send_message(websocket_api.result_message(msg["id"])) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b770122d39a..44803f2a5f6 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1647,6 +1647,7 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): async_fire_mqtt_message(hass, "test-topic", "test1") async_fire_mqtt_message(hass, "test-topic", "test2") + async_fire_mqtt_message(hass, "test-topic", b"\xDE\xAD\xBE\xEF") response = await client.receive_json() assert response["event"]["topic"] == "test-topic" @@ -1656,6 +1657,10 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): assert response["event"]["topic"] == "test-topic" assert response["event"]["payload"] == "test2" + response = await client.receive_json() + assert response["event"]["topic"] == "test-topic" + assert response["event"]["payload"] == "b'\\xde\\xad\\xbe\\xef'" + # Unsubscribe await client.send_json({"id": 8, "type": "unsubscribe_events", "subscription": 5}) response = await client.receive_json() From dc24e6505ec6250922b205f5940d449272f42046 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Mar 2022 08:22:34 -0800 Subject: [PATCH 0184/1054] Add guard radio browser media source (#67486) --- .../components/radio_browser/media_source.py | 74 +++++++++++-------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index 8240691b247..6ba1b7b2b9a 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, ) from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -35,9 +36,8 @@ async def async_get_media_source(hass: HomeAssistant) -> RadioMediaSource: """Set up Radio Browser media source.""" # Radio browser support only a single config entry entry = hass.config_entries.async_entries(DOMAIN)[0] - radios = hass.data[DOMAIN] - return RadioMediaSource(hass, radios, entry) + return RadioMediaSource(hass, entry) class RadioMediaSource(MediaSource): @@ -45,26 +45,33 @@ class RadioMediaSource(MediaSource): name = "Radio Browser" - def __init__( - self, hass: HomeAssistant, radios: RadioBrowser, entry: ConfigEntry - ) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize CameraMediaSource.""" super().__init__(DOMAIN) self.hass = hass self.entry = entry - self.radios = radios + + @property + def radios(self) -> RadioBrowser | None: + """Return the radio browser.""" + return self.hass.data.get(DOMAIN) async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve selected Radio station to a streaming URL.""" - station = await self.radios.station(uuid=item.identifier) + radios = self.radios + + if radios is None: + raise Unresolvable("Radio Browser not initialized") + + station = await radios.station(uuid=item.identifier) if not station: - raise BrowseError("Radio station is no longer available") + raise Unresolvable("Radio station is no longer available") if not (mime_type := self._async_get_station_mime_type(station)): - raise BrowseError("Could not determine stream type of radio station") + raise Unresolvable("Could not determine stream type of radio station") # Register "click" with Radio Browser - await self.radios.station_click(uuid=station.uuid) + await radios.station_click(uuid=station.uuid) return PlayMedia(station.url, mime_type) @@ -73,6 +80,11 @@ class RadioMediaSource(MediaSource): item: MediaSourceItem, ) -> BrowseMediaSource: """Return media.""" + radios = self.radios + + if radios is None: + raise BrowseError("Radio Browser not initialized") + return BrowseMediaSource( domain=DOMAIN, identifier=None, @@ -83,10 +95,10 @@ class RadioMediaSource(MediaSource): can_expand=True, children_media_class=MEDIA_CLASS_DIRECTORY, children=[ - *await self._async_build_popular(item), - *await self._async_build_by_tag(item), - *await self._async_build_by_language(item), - *await self._async_build_by_country(item), + *await self._async_build_popular(radios, item), + *await self._async_build_by_tag(radios, item), + *await self._async_build_by_language(radios, item), + *await self._async_build_by_country(radios, item), ], ) @@ -100,7 +112,9 @@ class RadioMediaSource(MediaSource): return mime_type @callback - def _async_build_stations(self, stations: list[Station]) -> list[BrowseMediaSource]: + def _async_build_stations( + self, radios: RadioBrowser, stations: list[Station] + ) -> list[BrowseMediaSource]: """Build list of media sources from radio stations.""" items: list[BrowseMediaSource] = [] @@ -126,23 +140,23 @@ class RadioMediaSource(MediaSource): return items async def _async_build_by_country( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing radio stations by country.""" category, _, country_code = (item.identifier or "").partition("/") if country_code: - stations = await self.radios.stations( + stations = await radios.stations( filter_by=FilterBy.COUNTRY_CODE_EXACT, filter_term=country_code, hide_broken=True, order=Order.NAME, reverse=False, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) # We show country in the root additionally, when there is no item if not item.identifier or category == "country": - countries = await self.radios.countries(order=Order.NAME) + countries = await radios.countries(order=Order.NAME) return [ BrowseMediaSource( domain=DOMAIN, @@ -160,22 +174,22 @@ class RadioMediaSource(MediaSource): return [] async def _async_build_by_language( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing radio stations by language.""" category, _, language = (item.identifier or "").partition("/") if category == "language" and language: - stations = await self.radios.stations( + stations = await radios.stations( filter_by=FilterBy.LANGUAGE_EXACT, filter_term=language, hide_broken=True, order=Order.NAME, reverse=False, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) if category == "language": - languages = await self.radios.languages(order=Order.NAME, hide_broken=True) + languages = await radios.languages(order=Order.NAME, hide_broken=True) return [ BrowseMediaSource( domain=DOMAIN, @@ -206,17 +220,17 @@ class RadioMediaSource(MediaSource): return [] async def _async_build_popular( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing popular radio stations.""" if item.identifier == "popular": - stations = await self.radios.stations( + stations = await radios.stations( hide_broken=True, limit=250, order=Order.CLICK_COUNT, reverse=True, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) if not item.identifier: return [ @@ -234,22 +248,22 @@ class RadioMediaSource(MediaSource): return [] async def _async_build_by_tag( - self, item: MediaSourceItem + self, radios: RadioBrowser, item: MediaSourceItem ) -> list[BrowseMediaSource]: """Handle browsing radio stations by tags.""" category, _, tag = (item.identifier or "").partition("/") if category == "tag" and tag: - stations = await self.radios.stations( + stations = await radios.stations( filter_by=FilterBy.TAG_EXACT, filter_term=tag, hide_broken=True, order=Order.NAME, reverse=False, ) - return self._async_build_stations(stations) + return self._async_build_stations(radios, stations) if category == "tag": - tags = await self.radios.tags( + tags = await radios.tags( hide_broken=True, limit=100, order=Order.STATION_COUNT, From a4915eb70407c855d2f2751ade87215e968e1350 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 2 Mar 2022 18:49:57 +0100 Subject: [PATCH 0185/1054] Handle exception in modbus slave sensor (#67472) --- homeassistant/components/modbus/sensor.py | 3 +++ tests/components/modbus/test_sensor.py | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 8363de3adf1..d4f3d1f28b6 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -101,6 +101,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): return self._lazy_errors = self._lazy_error_count self._attr_available = False + self._attr_native_value = None + if self._coordinator: + self._coordinator.async_set_updated_data(None) self.async_write_ha_state() return diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index aa513d1c473..4e4e2e284cf 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -33,6 +33,7 @@ from homeassistant.const import ( CONF_SLAVE, CONF_STRUCTURE, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import State @@ -565,13 +566,14 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ], ) @pytest.mark.parametrize( - "config_addon,register_words,expected", + "config_addon,register_words,do_exception,expected", [ ( { CONF_SLAVE_COUNT: 0, }, [0x0102, 0x0304], + False, ["16909060"], ), ( @@ -579,6 +581,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): CONF_SLAVE_COUNT: 1, }, [0x0102, 0x0304, 0x0403, 0x0201], + False, ["16909060", "67305985"], ), ( @@ -595,6 +598,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): 0x0D0E, 0x0F00, ], + False, [ "16909060", "84281096", @@ -602,6 +606,22 @@ async def test_all_sensor(hass, mock_do_cycle, expected): "219025152", ], ), + ( + { + CONF_SLAVE_COUNT: 1, + }, + [0x0102, 0x0304, 0x0403, 0x0201], + True, + [STATE_UNAVAILABLE, STATE_UNKNOWN], + ), + ( + { + CONF_SLAVE_COUNT: 1, + }, + [], + False, + [STATE_UNAVAILABLE, STATE_UNKNOWN], + ), ], ) async def test_slave_sensor(hass, mock_do_cycle, expected): From b245ba6d57e155f105a0645d5e3321c3b7c9fdaf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Mar 2022 19:09:06 +0100 Subject: [PATCH 0186/1054] Bump samsungtvws to v2.1.0 (#67483) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 16 ++++++++++------ homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 6ad2d6eb8bf..39f6a5e4f36 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from asyncio.exceptions import TimeoutError as AsyncioTimeoutError import contextlib -from typing import Any +from typing import Any, cast from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse @@ -313,10 +313,12 @@ class SamsungTVWSBridge(SamsungTVBridge): """Get installed app list.""" if self._app_list is None: if remote := self._get_remote(): - raw_app_list: list[dict[str, str]] = remote.app_list() + raw_app_list = remote.app_list() self._app_list = { app["name"]: app["appId"] - for app in sorted(raw_app_list, key=lambda app: app["name"]) + for app in sorted( + raw_app_list or [], key=lambda app: cast(str, app["name"]) + ) } return self._app_list @@ -355,8 +357,8 @@ class SamsungTVWSBridge(SamsungTVBridge): host=self.host, port=self.port, token=self.token, - timeout=config[CONF_TIMEOUT], - name=config[CONF_NAME], + timeout=TIMEOUT_REQUEST, + name=VALUE_CONF_NAME, ) as remote: remote.open() self.token = remote.token @@ -379,6 +381,7 @@ class SamsungTVWSBridge(SamsungTVBridge): async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" if self._rest_api is None: + assert self.port self._rest_api = SamsungTVAsyncRest( host=self.host, session=async_get_clientsession(self.hass), @@ -423,7 +426,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # Different reasons, e.g. hostname not resolveable pass - def _get_remote(self) -> SamsungTVWS: + def _get_remote(self) -> SamsungTVWS | None: """Create or return a remote control instance.""" if self._remote is None: # We need to create a new instance to reconnect. @@ -431,6 +434,7 @@ class SamsungTVWSBridge(SamsungTVBridge): LOGGER.debug( "Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host ) + assert self.port self._remote = SamsungTVWS( host=self.host, port=self.port, diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index dd014fab97d..a536169de90 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws[async]==2.0.0", + "samsungtvws[async]==2.1.0", "wakeonlan==2.0.1" ], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7cfd3f3242a..7b208fc5666 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.0.0 +samsungtvws[async]==2.1.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f46e0adf646..a44c5c3772f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.0.0 +samsungtvws[async]==2.1.0 # homeassistant.components.dhcp scapy==2.4.5 From b8861578fffba5c0326643b05855278081a3f939 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:30:33 +0100 Subject: [PATCH 0187/1054] Implement async websocket in samsungtv (#67127) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 6 +- homeassistant/components/samsungtv/bridge.py | 82 +++++++++---------- tests/components/samsungtv/conftest.py | 12 +-- .../components/samsungtv/test_config_flow.py | 44 +++++----- tests/components/samsungtv/test_init.py | 2 +- .../components/samsungtv/test_media_player.py | 68 +++++++++------ 6 files changed, 111 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 69289dca274..664ce1fa439 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -125,11 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={**entry.data, CONF_TOKEN: bridge.token} ) - def new_token_callback() -> None: - """Update config entry with the new token.""" - hass.add_job(_update_token) - - bridge.register_new_token_callback(new_token_callback) + bridge.register_new_token_callback(_update_token) async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 39f6a5e4f36..3a06a9ff906 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -2,16 +2,18 @@ from __future__ import annotations from abc import ABC, abstractmethod +import asyncio from asyncio.exceptions import TimeoutError as AsyncioTimeoutError import contextlib from typing import Any, cast from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse -from samsungtvws import SamsungTVWS +from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.exceptions import ConnectionFailure, HttpApiError -from websocket import WebSocketException +from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey +from websockets.exceptions import WebSocketException from homeassistant.const import ( CONF_HOST, @@ -298,7 +300,8 @@ class SamsungTVWSBridge(SamsungTVBridge): self.token = token self._rest_api: SamsungTVAsyncRest | None = None self._app_list: dict[str, str] | None = None - self._remote: SamsungTVWS | None = None + self._remote: SamsungTVWSAsyncRemote | None = None + self._remote_lock = asyncio.Lock() async def async_mac_from_device(self) -> str | None: """Try to fetch the mac address of the TV.""" @@ -306,39 +309,27 @@ class SamsungTVWSBridge(SamsungTVBridge): return mac_from_device_info(info) if info else None async def async_get_app_list(self) -> dict[str, str] | None: - """Get installed app list.""" - return await self.hass.async_add_executor_job(self._get_app_list) - - def _get_app_list(self) -> dict[str, str] | None: """Get installed app list.""" if self._app_list is None: - if remote := self._get_remote(): - raw_app_list = remote.app_list() + if remote := await self._async_get_remote(): + raw_app_list = await remote.app_list() self._app_list = { app["name"]: app["appId"] for app in sorted( - raw_app_list or [], key=lambda app: cast(str, app["name"]) + raw_app_list or [], + key=lambda app: cast(str, app["name"]), ) } - + LOGGER.debug("Generated app list: %s", self._app_list) return self._app_list async def async_is_on(self) -> bool: """Tells if the TV is on.""" - return await self.hass.async_add_executor_job(self._is_on) - - def _is_on(self) -> bool: - """Tells if the TV is on.""" - if self._remote is not None: - self._close_remote() - - return self._get_remote() is not None + if remote := await self._async_get_remote(): + return remote.is_alive() + return False async def async_try_connect(self) -> str: - """Try to connect to the Websocket TV.""" - return await self.hass.async_add_executor_job(self._try_connect) - - def _try_connect(self) -> str: """Try to connect to the Websocket TV.""" for self.port in WEBSOCKET_PORTS: config = { @@ -353,14 +344,14 @@ class SamsungTVWSBridge(SamsungTVBridge): result = None try: LOGGER.debug("Try config: %s", config) - with SamsungTVWS( + async with SamsungTVWSAsyncRemote( host=self.host, port=self.port, token=self.token, timeout=TIMEOUT_REQUEST, name=VALUE_CONF_NAME, ) as remote: - remote.open() + await remote.open() self.token = remote.token LOGGER.debug("Working config: %s", config) return RESULT_SUCCESS @@ -369,7 +360,7 @@ class SamsungTVWSBridge(SamsungTVBridge): "Working but unsupported config: %s, error: %s", config, err ) result = RESULT_NOT_SUPPORTED - except (OSError, ConnectionFailure) as err: + except (OSError, AsyncioTimeoutError, ConnectionFailure) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) # pylint: disable=useless-else-on-loop else: @@ -397,10 +388,6 @@ class SamsungTVWSBridge(SamsungTVBridge): return None async def async_send_key(self, key: str, key_type: str | None = None) -> None: - """Send the key using websocket protocol.""" - await self.hass.async_add_executor_job(self._send_key, key, key_type) - - def _send_key(self, key: str, key_type: str | None = None) -> None: """Send the key using websocket protocol.""" if key == "KEY_POWEROFF": key = "KEY_POWER" @@ -409,11 +396,13 @@ class SamsungTVWSBridge(SamsungTVBridge): retry_count = 1 for _ in range(retry_count + 1): try: - if remote := self._get_remote(): + if remote := await self._async_get_remote(): if key_type == "run_app": - remote.run_app(key) + await remote.send_command( + ChannelEmitCommand.launch_app(key) + ) else: - remote.send_key(key) + await remote.send_command(SendRemoteKey.click(key)) break except ( BrokenPipeError, @@ -426,29 +415,40 @@ class SamsungTVWSBridge(SamsungTVBridge): # Different reasons, e.g. hostname not resolveable pass - def _get_remote(self) -> SamsungTVWS | None: + async def _async_get_remote(self) -> SamsungTVWSAsyncRemote | None: """Create or return a remote control instance.""" - if self._remote is None: + if (remote := self._remote) and remote.is_alive(): + # If we have one then try to use it + return remote + + async with self._remote_lock: + # If we don't have one make sure we do it under the lock + # so we don't make two do due a race to get the remote + return await self._async_get_remote_under_lock() + + async def _async_get_remote_under_lock(self) -> SamsungTVWSAsyncRemote | None: + """Create or return a remote control instance.""" + if self._remote is None or not self._remote.is_alive(): # We need to create a new instance to reconnect. try: LOGGER.debug( "Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host ) assert self.port - self._remote = SamsungTVWS( + self._remote = SamsungTVWSAsyncRemote( host=self.host, port=self.port, token=self.token, timeout=TIMEOUT_WEBSOCKET, name=VALUE_CONF_NAME, ) - self._remote.open() + await self._remote.start_listening() # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure as err: LOGGER.debug("ConnectionFailure %s", err.__repr__()) self._notify_reauth_callback() - except (WebSocketException, OSError) as err: + except (WebSocketException, AsyncioTimeoutError, OSError) as err: LOGGER.debug("WebSocketException, OSError %s", err.__repr__()) self._remote = None else: @@ -465,15 +465,11 @@ class SamsungTVWSBridge(SamsungTVBridge): return self._remote async def async_close_remote(self) -> None: - """Close remote object.""" - await self.hass.async_add_executor_job(self._close_remote) - - def _close_remote(self) -> None: """Close remote object.""" try: if self._remote is not None: # Close the current remote connection - self._remote.close() + await self._remote.close() self._remote = None except OSError: LOGGER.debug("Could not establish connection") diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index e92caac9fc4..7975c291fe3 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -1,10 +1,10 @@ """Fixtures for Samsung TV.""" from datetime import datetime -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest from samsungctl import Remote -from samsungtvws import SamsungTVWS +from samsungtvws.async_remote import SamsungTVWSAsyncRemote import homeassistant.util.dt as dt_util @@ -56,11 +56,11 @@ def rest_api_fixture() -> Mock: def remotews_fixture() -> Mock: """Patch the samsungtvws SamsungTVWS.""" with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", ) as remotews_class: - remotews = Mock(SamsungTVWS) - remotews.__enter__ = Mock(return_value=remotews) - remotews.__exit__ = Mock() + remotews = Mock(SamsungTVWSAsyncRemote) + remotews.__aenter__ = AsyncMock(return_value=remotews) + remotews.__aexit__ = AsyncMock() remotews.app_list.return_value = SAMPLE_APP_LIST remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 11ca69b91ef..94056a71a50 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -4,9 +4,9 @@ from unittest.mock import ANY, AsyncMock, Mock, call, patch import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse -from samsungtvws import SamsungTVWS +from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.exceptions import ConnectionFailure, HttpApiError -from websocket import WebSocketException, WebSocketProtocolException +from websockets.exceptions import WebSocketException, WebSocketProtocolError from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf @@ -272,8 +272,8 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", - side_effect=WebSocketProtocolException("Boom"), + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=WebSocketProtocolError("Boom"), ): # websocket device not supported result = await hass.config_entries.flow.async_init( @@ -289,7 +289,7 @@ async def test_user_not_successful(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=OSError("Boom"), ): result = await hass.config_entries.flow.async_init( @@ -305,7 +305,7 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=ConnectionFailure("Boom"), ): result = await hass.config_entries.flow.async_init( @@ -464,9 +464,9 @@ async def test_ssdp_websocket_not_supported( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", ) as remotews, patch.object( - remotews, "open", side_effect=WebSocketProtocolException("Boom") + remotews, "open", side_effect=WebSocketProtocolError("Boom") ): # device not supported result = await hass.config_entries.flow.async_init( @@ -497,7 +497,7 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", @@ -526,7 +526,7 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=ConnectionFailure("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", @@ -830,13 +830,13 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" ) as remotews, patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", ) as rest_api_class: - remote = Mock(SamsungTVWS) - remote.__enter__ = Mock(return_value=remote) - remote.__exit__ = Mock(return_value=False) + remote = Mock(SamsungTVWSAsyncRemote) + remote.__aenter__ = AsyncMock(return_value=remote) + remote.__aexit__ = AsyncMock(return_value=False) remote.app_list.return_value = SAMPLE_APP_LIST rest_api_class.return_value.rest_device_info = AsyncMock( return_value={ @@ -876,15 +876,15 @@ async def test_websocket_no_mac(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" ) as remotews, patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", ) as rest_api_class, patch( "getmac.get_mac_address", return_value="gg:hh:ii:ll:mm:nn" ): - remote = Mock(SamsungTVWS) - remote.__enter__ = Mock(return_value=remote) - remote.__exit__ = Mock(return_value=False) + remote = Mock(SamsungTVWSAsyncRemote) + remote.__aenter__ = AsyncMock(return_value=remote) + remote.__aexit__ = AsyncMock(return_value=False) remote.app_list.return_value = SAMPLE_APP_LIST rest_api_class.return_value.rest_device_info = AsyncMock( return_value={ @@ -964,15 +964,15 @@ async def test_autodetect_legacy(hass: HomeAssistant) -> None: async def test_autodetect_none(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" mock_remotews = Mock() - mock_remotews.__enter__ = Mock(return_value=mock_remotews) - mock_remotews.__exit__ = Mock() + mock_remotews.__aenter__ = AsyncMock(return_value=mock_remotews) + mock_remotews.__aexit__ = AsyncMock() mock_remotews.open = Mock(side_effect=OSError("Boom")) with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ) as remote, patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", return_value=mock_remotews, ) as remotews: result = await hass.config_entries.flow.async_init( @@ -1314,7 +1314,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=WebSocketException, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 6b6aa429243..6beac1b65bc 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -80,7 +80,7 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant) with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS.open", + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=OSError, ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index ddc28097de4..4c324abe4d8 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,13 +2,14 @@ import asyncio from datetime import datetime, timedelta import logging -from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, call, patch +from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch import pytest from samsungctl import exceptions -from samsungtvws import SamsungTVWS +from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.exceptions import ConnectionFailure -from websocket import WebSocketException +from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey +from websockets.exceptions import WebSocketException from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( @@ -159,13 +160,13 @@ async def test_setup_without_turnon(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews") async def test_setup_websocket(hass: HomeAssistant) -> None: """Test setup of platform.""" - - with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: - remote = Mock(SamsungTVWS) - remote.__enter__ = Mock(return_value=remote) - remote.__exit__ = Mock() + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" + ) as remote_class: + remote = Mock(SamsungTVWSAsyncRemote) + remote.__aenter__ = AsyncMock(return_value=remote) + remote.__aexit__ = AsyncMock() remote.app_list.return_value = SAMPLE_APP_LIST - remote.token = "123456789" remote_class.return_value = remote @@ -209,10 +210,12 @@ async def test_setup_websocket_2( "networkType": "wireless", }, } - with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: - remote = Mock(SamsungTVWS) - remote.__enter__ = Mock(return_value=remote) - remote.__exit__ = Mock() + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" + ) as remote_class: + remote = Mock(SamsungTVWSAsyncRemote) + remote.__aenter__ = AsyncMock(return_value=remote) + remote.__aexit__ = AsyncMock() remote.app_list.return_value = SAMPLE_APP_LIST remote.token = "987654321" remote_class.return_value = remote @@ -228,8 +231,7 @@ async def test_setup_websocket_2( state = hass.states.get(entity_id) assert state - assert remote_class.call_count == 2 - assert remote_class.call_args_list[0] == call(**MOCK_CALLS_WS) + remote_class.assert_called_once_with(**MOCK_CALLS_WS) @pytest.mark.usefixtures("remote") @@ -274,7 +276,8 @@ async def test_update_off_ws( state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON - remotews.open = Mock(side_effect=WebSocketException("Boom")) + remotews.start_listening = Mock(side_effect=WebSocketException("Boom")) + remotews.is_alive.return_value = False next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): @@ -322,7 +325,9 @@ async def test_update_connection_failure( ): await setup_samsungtv(hass, MOCK_CONFIGWS) - with patch.object(remotews, "open", side_effect=ConnectionFailure("Boom")): + with patch.object( + remotews, "start_listening", side_effect=ConnectionFailure("Boom") + ), patch.object(remotews, "is_alive", return_value=False): next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -454,7 +459,7 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_key = Mock(side_effect=WebSocketException("Boom")) + remotews.send_command = Mock(side_effect=WebSocketException("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -465,7 +470,7 @@ async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_key = Mock(side_effect=OSError("Boom")) + remotews.send_command = Mock(side_effect=OSError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -572,18 +577,22 @@ async def test_turn_off_websocket(hass: HomeAssistant, remotews: Mock) -> None: side_effect=[OSError("Boom"), DEFAULT_MOCK], ): await setup_samsungtv(hass, MOCK_CONFIGWS) + remotews.send_command.reset_mock() + assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remotews.send_key.call_count == 1 - assert remotews.send_key.call_args_list == [call("KEY_POWER")] + assert remotews.send_command.call_count == 1 + command = remotews.send_command.call_args_list[0].args[0] + assert isinstance(command, SendRemoteKey) + assert command.params["DataOfCmd"] == "KEY_POWER" assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key not called - assert remotews.send_key.call_count == 1 + assert remotews.send_command.call_count == 1 async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: @@ -911,6 +920,7 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for play_media.""" await setup_samsungtv(hass, MOCK_CONFIGWS) + remotews.send_command.reset_mock() assert await hass.services.async_call( DOMAIN, @@ -922,18 +932,24 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: }, True, ) - assert remotews.run_app.call_count == 1 - assert remotews.run_app.call_args_list == [call("3201608010191")] + assert remotews.send_command.call_count == 1 + command = remotews.send_command.call_args_list[0].args[0] + assert isinstance(command, ChannelEmitCommand) + assert command.params["data"]["appId"] == "3201608010191" async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for select_source.""" await setup_samsungtv(hass, MOCK_CONFIGWS) + remotews.send_command.reset_mock() + assert await hass.services.async_call( DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, ) - assert remotews.run_app.call_count == 1 - assert remotews.run_app.call_args_list == [call("3201608010191")] + assert remotews.send_command.call_count == 1 + command = remotews.send_command.call_args_list[0].args[0] + assert isinstance(command, ChannelEmitCommand) + assert command.params["data"]["appId"] == "3201608010191" From 26f2388fa13bba4819b8dc718344fca0e1e623ac Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Mar 2022 20:52:11 +0100 Subject: [PATCH 0188/1054] Adjust error handling scope in samsungtv (#66692) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 3a06a9ff906..342c4f7f429 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -430,18 +430,16 @@ class SamsungTVWSBridge(SamsungTVBridge): """Create or return a remote control instance.""" if self._remote is None or not self._remote.is_alive(): # We need to create a new instance to reconnect. + LOGGER.debug("Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host) + assert self.port + self._remote = SamsungTVWSAsyncRemote( + host=self.host, + port=self.port, + token=self.token, + timeout=TIMEOUT_WEBSOCKET, + name=VALUE_CONF_NAME, + ) try: - LOGGER.debug( - "Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host - ) - assert self.port - self._remote = SamsungTVWSAsyncRemote( - host=self.host, - port=self.port, - token=self.token, - timeout=TIMEOUT_WEBSOCKET, - name=VALUE_CONF_NAME, - ) await self._remote.start_listening() # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket From 8ee3be33e9f0ccd6563defb2ec94d084dfbca1c8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 2 Mar 2022 23:20:10 +0100 Subject: [PATCH 0189/1054] Bump pysensibo to v1.0.8 (#67506) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 35273bb3d6f..f0132833d70 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.7"], + "requirements": ["pysensibo==1.0.8"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 7b208fc5666..8bae64aa22c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1767,7 +1767,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.7 +pysensibo==1.0.8 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a44c5c3772f..3ce0b0e74ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1157,7 +1157,7 @@ pyrituals==0.0.6 pyruckus==0.12 # homeassistant.components.sensibo -pysensibo==1.0.7 +pysensibo==1.0.8 # homeassistant.components.serial # homeassistant.components.zha From a3a66451d7210dc86ee0a10663e8066068c449ce Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 2 Mar 2022 23:22:14 +0100 Subject: [PATCH 0190/1054] Implement reauth for Sensibo (#67446) --- .../components/sensibo/config_flow.py | 51 +++++ .../components/sensibo/coordinator.py | 5 +- homeassistant/components/sensibo/strings.json | 14 +- .../components/sensibo/translations/en.json | 14 +- tests/components/sensibo/test_config_flow.py | 182 ++++++++++++++++++ 5 files changed, 257 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index dca5ea62fe6..b8cca2e6fb8 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -1,6 +1,8 @@ """Adds config flow for Sensibo integration.""" from __future__ import annotations +from typing import Any + from pysensibo.exceptions import AuthenticationError import voluptuous as vol @@ -24,6 +26,55 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Sensibo.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Sensibo.""" + errors: dict[str, str] = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + try: + username = await async_validate_api(self.hass, api_key) + except AuthenticationError: + errors["base"] = "invalid_auth" + except ConnectionError: + errors["base"] = "cannot_connect" + except NoDevicesError: + errors["base"] = "no_devices" + except NoUsernameError: + errors["base"] = "no_username" + else: + assert self.entry is not None + + if username == self.entry.unique_id: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: api_key, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + errors["base"] = "incorrect_api_key" + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + errors=errors, + ) + async def async_step_import(self, config: dict) -> FlowResult: """Import a configuration from config.yaml.""" diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index ef0475640b5..926232ef159 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -10,6 +10,7 @@ from pysensibo.exceptions import AuthenticationError, SensiboError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -41,7 +42,9 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): data = await self.client.async_get_devices() for dev in data["result"]: devices.append(dev) - except (AuthenticationError, SensiboError) as error: + except AuthenticationError as error: + raise ConfigEntryAuthFailed from error + except SensiboError as error: raise UpdateFailed from error device_data: dict[str, dict[str, Any]] = {} diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 9b035bc7f05..2b7b351e27f 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -1,19 +1,25 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "no_devices": "No devices discovered", - "no_username": "Could not get username" + "no_username": "Could not get username", + "incorrect_api_key": "Invalid API key for selected account" }, "step": { "user": { "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "name": "[%key:common::config_flow::data::name%]" + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" } } } diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 102ffa35879..4acdf4862aa 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -1,19 +1,25 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "no_devices": "No devices discovered", - "no_username": "Could not get username" + "no_username": "Could not get username", + "incorrect_api_key": "Invalid API key for selected account" }, "step": { "user": { "data": { - "api_key": "API Key", - "name": "Name" + "api_key": "API Key" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API Key" } } } diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 9cc96c1c04b..cb5c2b239df 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from unittest.mock import patch import aiohttp @@ -233,3 +234,184 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: ) assert result2["errors"] == {"base": "no_username"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + version=2, + domain=DOMAIN, + unique_id="username", + data={"api_key": "1234567890"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ) as mock_sensibo, patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == {"api_key": "1234567891"} + + assert len(mock_sensibo.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "sideeffect,p_error", + [ + (aiohttp.ClientConnectionError, "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (AuthenticationError, "invalid_auth"), + (SensiboError, "cannot_connect"), + ], +) +async def test_reauth_flow_error( + hass: HomeAssistant, sideeffect: Exception, p_error: str +) -> None: + """Test a reauthentication flow with error.""" + entry = MockConfigEntry( + version=2, + domain=DOMAIN, + unique_id="username", + data={"api_key": "1234567890"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + side_effect=sideeffect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": p_error} + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == {"api_key": "1234567891"} + + +@pytest.mark.parametrize( + "get_devices,get_me,p_error", + [ + ( + {"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + {"result": {}}, + "no_username", + ), + ( + {"result": []}, + {"result": {"username": "username"}}, + "no_devices", + ), + ( + {"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + {"result": {"username": "username2"}}, + "incorrect_api_key", + ), + ], +) +async def test_flow_reauth_no_username_or_device( + hass: HomeAssistant, + get_devices: dict[str, Any], + get_me: dict[str, Any], + p_error: str, +) -> None: + """Test config flow get no username from api.""" + entry = MockConfigEntry( + version=2, + domain=DOMAIN, + unique_id="username", + data={"api_key": "1234567890"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value=get_devices, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value=get_me, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": p_error} From e7e4c7e3da1f6b07acb26e053cbc9b27ae7a1c47 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Mar 2022 16:41:03 -0600 Subject: [PATCH 0191/1054] Bump soco to 0.26.4 (#67498) --- 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 4bb8623acb2..6f482e92dc9 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": ["soco==0.26.3"], + "requirements": ["soco==0.26.4"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 8bae64aa22c..847dfa203ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2166,7 +2166,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.3 +soco==0.26.4 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ce0b0e74ee..acdb89f5aa0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.3 +soco==0.26.4 # homeassistant.components.solaredge solaredge==0.0.2 From 4b298863c7f4d033409420533e77e5ed62538049 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Mar 2022 00:38:26 +0100 Subject: [PATCH 0192/1054] Fix nightly manifest (#67489) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 853a5ca0c65..1f8cd2f8e9d 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -339,7 +339,7 @@ jobs: # Create general tags if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then - create_manifest"dev" "${{ needs.init.outputs.version }}" + create_manifest "dev" "${{ needs.init.outputs.version }}" elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then create_manifest "beta" "${{ needs.init.outputs.version }}" create_manifest "rc" "${{ needs.init.outputs.version }}" From 11175b39f5b6f59dd2338cedd95d5c6205b4ed88 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 3 Mar 2022 00:20:45 +0000 Subject: [PATCH 0193/1054] [ci skip] Translation update --- .../components/knx/translations/de.json | 22 +++++++++---------- .../components/ps4/translations/it.json | 2 +- .../components/sensibo/translations/bg.json | 3 ++- .../components/sensibo/translations/en.json | 11 +++++----- .../components/sensibo/translations/hu.json | 5 ++++- .../components/sensibo/translations/id.json | 5 ++++- .../components/sensibo/translations/it.json | 5 ++++- .../components/sensibo/translations/nl.json | 5 ++++- .../components/sleepiq/translations/bg.json | 9 +++++++- .../components/sleepiq/translations/es.json | 5 +++++ .../components/sleepiq/translations/hu.json | 10 ++++++++- .../components/sleepiq/translations/id.json | 10 ++++++++- .../components/sleepiq/translations/it.json | 10 ++++++++- .../components/sleepiq/translations/nl.json | 10 ++++++++- .../sleepiq/translations/pt-BR.json | 2 +- .../components/tasmota/translations/it.json | 2 +- .../tuya/translations/select.es.json | 4 ++++ .../components/wled/translations/es.json | 3 ++- 18 files changed, 93 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 6716b5c8a37..2f7795153dc 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -11,8 +11,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Individuelle Adresse f\u00fcr die Verbindung", - "local_ip": "Lokale IP des Home Assistant (f\u00fcr automatische Erkennung leer lassen)", + "individual_address": "Physikalische Adresse f\u00fcr die Verbindung", + "local_ip": "Lokale IP von Home Assistant (f\u00fcr automatische Erkennung leer lassen)", "port": "Port", "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" @@ -21,8 +21,8 @@ }, "routing": { "data": { - "individual_address": "Individuelle Adresse f\u00fcr die Routingverbindung", - "local_ip": "Lokale IP des Home Assistant (f\u00fcr automatische Erkennung leer lassen)", + "individual_address": "Physikalische Adresse f\u00fcr die Routingverbindung", + "local_ip": "Lokale IP von Home Assistant (f\u00fcr automatische Erkennung leer lassen)", "multicast_group": "Die f\u00fcr das Routing verwendete Multicast-Gruppe", "multicast_port": "Der f\u00fcr das Routing verwendete Multicast-Port" }, @@ -47,18 +47,18 @@ "init": { "data": { "connection_type": "KNX-Verbindungstyp", - "individual_address": "Individuelle Standardadresse", - "local_ip": "Lokale IP des Home Assistant (verwende 0.0.0.0 f\u00fcr automatische Erkennung)", - "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery verwenden", - "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery verwenden", - "rate_limit": "Maximal ausgehende Telegrams pro Sekunde", - "state_updater": "Lesen von Zust\u00e4nden aus dem KNX Bus global freigeben" + "individual_address": "Standard physikalische Adresse", + "local_ip": "Lokale IP von Home Assistant (verwende 0.0.0.0 f\u00fcr automatische Erkennung)", + "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery", + "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery", + "rate_limit": "Maximal ausgehende Telegramme pro Sekunde", + "state_updater": "Lesen von Zust\u00e4nden von dem KNX Bus global freigeben" } }, "tunnel": { "data": { "host": "Host", - "local_ip": "Lokale IP (leer lassen, wenn unsicher)", + "local_ip": "Lokale IP (im Zweifel leer lassen)", "port": "Port", "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index 0a7db1888f6..8df24f24afb 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -33,7 +33,7 @@ "ip_address": "Indirizzo IP (Lascia vuoto se stai usando il rilevamento automatico).", "mode": "Modalit\u00e0 di configurazione" }, - "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno automaticamente individuati.", + "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno rilevati automaticamente.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index ef66d0b9368..ab5250b55f8 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -4,7 +4,8 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 4acdf4862aa..1b5d7cd9214 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -6,20 +6,21 @@ }, "error": { "cannot_connect": "Failed to connect", + "incorrect_api_key": "Invalid API key for selected account", "invalid_auth": "Invalid authentication", "no_devices": "No devices discovered", - "no_username": "Could not get username", - "incorrect_api_key": "Invalid API key for selected account" + "no_username": "Could not get username" }, "step": { - "user": { + "reauth_confirm": { "data": { "api_key": "API Key" } }, - "reauth_confirm": { + "user": { "data": { - "api_key": "API Key" + "api_key": "API Key", + "name": "Name" } } } diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 09aca99c669..55fd361aacc 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -4,7 +4,10 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_devices": "Nincs \u00e9szlelt eszk\u00f6z", + "no_username": "Nem siker\u00fclt beolvasni a felhaszn\u00e1l\u00f3nevet" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index d4ea1c29254..157bb18a674 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -4,7 +4,10 @@ "already_configured": "Akun sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "no_devices": "Tidak ada perangkat yang ditemukan", + "no_username": "Tidak bisa mendapatkan nama pengguna" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index 48c5b477073..d67ac8a9ad3 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -4,7 +4,10 @@ "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "no_devices": "Nessun dispositivo rilevato", + "no_username": "Impossibile ottenere il nome utente" }, "step": { "user": { diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index c1aa1c36340..8a3f935b983 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -4,7 +4,10 @@ "already_configured": "Account is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "no_devices": "Geen apparaten gevonden", + "no_username": "Kan gebruikersnaam niet ophalen" }, "step": { "user": { diff --git a/homeassistant/components/sleepiq/translations/bg.json b/homeassistant/components/sleepiq/translations/bg.json index b8fb3b61a77..e1494bd66a1 100644 --- a/homeassistant/components/sleepiq/translations/bg.json +++ b/homeassistant/components/sleepiq/translations/bg.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index 8715dc6b7aa..ae6059aa227 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -8,6 +8,11 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + } + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/sleepiq/translations/hu.json b/homeassistant/components/sleepiq/translations/hu.json index c4adcb1bd9e..45caff592a8 100644 --- a/homeassistant/components/sleepiq/translations/hu.json +++ b/homeassistant/components/sleepiq/translations/hu.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A SleepIQ-integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie fi\u00f3kj\u00e1t: {username}.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/sleepiq/translations/id.json b/homeassistant/components/sleepiq/translations/id.json index a974f44967e..175f7a37794 100644 --- a/homeassistant/components/sleepiq/translations/id.json +++ b/homeassistant/components/sleepiq/translations/id.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Integrasi SlepIQ perlu mengautentikasi ulang akun Anda {username}.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/sleepiq/translations/it.json b/homeassistant/components/sleepiq/translations/it.json index 7ae1601843e..e0e0d75cb2a 100644 --- a/homeassistant/components/sleepiq/translations/it.json +++ b/homeassistant/components/sleepiq/translations/it.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "L'integrazione SleepIQ deve autenticare nuovamente il tuo account {username}.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/sleepiq/translations/nl.json b/homeassistant/components/sleepiq/translations/nl.json index 3271c6bce45..09029be118e 100644 --- a/homeassistant/components/sleepiq/translations/nl.json +++ b/homeassistant/components/sleepiq/translations/nl.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "De SleepIQ-integratie moet uw account {username} opnieuw verifi\u00ebren.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/sleepiq/translations/pt-BR.json b/homeassistant/components/sleepiq/translations/pt-BR.json index 99ed7e0dd88..2556c273b95 100644 --- a/homeassistant/components/sleepiq/translations/pt-BR.json +++ b/homeassistant/components/sleepiq/translations/pt-BR.json @@ -13,7 +13,7 @@ "data": { "password": "Senha" }, - "description": "A integra\u00e7\u00e3o do SleepIQ precisa autenticar novamente sua conta {username} .", + "description": "A integra\u00e7\u00e3o do SleepIQ precisa autenticar novamente sua conta {username}.", "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { diff --git a/homeassistant/components/tasmota/translations/it.json b/homeassistant/components/tasmota/translations/it.json index 9d1f143ebad..6cb52a616b0 100644 --- a/homeassistant/components/tasmota/translations/it.json +++ b/homeassistant/components/tasmota/translations/it.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "invalid_discovery_topic": "Prefisso dell'argomento di individuazione non valido." + "invalid_discovery_topic": "Prefisso dell'argomento di rilevamento non valido." }, "step": { "config": { diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index 02201f09dad..7dc4cf1067b 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -19,6 +19,10 @@ "6h": "6 horas", "cancel": "Cancelar" }, + "tuya__curtain_mode": { + "morning": "Ma\u00f1ana", + "night": "Noche" + }, "tuya__decibel_sensitivity": { "0": "Sensibilidad baja", "1": "Sensibilidad alta" diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index c1c50986b61..d883ba6ff23 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" From ad87d06d1f974e2f91ef27380d5b67e63de9b9ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Mar 2022 20:59:31 -1000 Subject: [PATCH 0194/1054] Enable strict typing for usb (#67466) * Enable strict typing for usb * Enable strict typing for usb * Enable strict typing for usb * adjust * coverage * Update tests/components/usb/test_init.py --- .strict-typing | 1 + homeassistant/components/usb/__init__.py | 15 ++++-- mypy.ini | 11 +++++ tests/components/usb/test_init.py | 62 ++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/.strict-typing b/.strict-typing index 652d5fe29e1..73975e2d1f7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -206,6 +206,7 @@ homeassistant.components.unifiprotect.* homeassistant.components.upcloud.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* +homeassistant.components.usb.* homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.velbus.* diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 38377aadd9f..5ac390ad168 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,7 +6,7 @@ import fnmatch import logging import os import sys -from typing import Any +from typing import TYPE_CHECKING, Any from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -28,6 +28,9 @@ from .const import DOMAIN from .models import USBDevice from .utils import usb_device_from_port +if TYPE_CHECKING: + from pyudev import Device + _LOGGER = logging.getLogger(__name__) REQUEST_SCAN_COOLDOWN = 60 # 1 minute cooldown @@ -163,12 +166,14 @@ class USBDiscovery: monitor, callback=self._device_discovered, name="usb-observer" ) observer.start() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, lambda event: observer.stop() - ) + + def _stop_observer(event: Event) -> None: + observer.stop() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_observer) self.observer_active = True - def _device_discovered(self, device): + def _device_discovered(self, device: Device) -> None: """Call when the observer discovers a new usb tty device.""" if device.action != "add": return diff --git a/mypy.ini b/mypy.ini index 2d4005afae6..e35fb90f0ea 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2067,6 +2067,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.usb.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.vacuum.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index ce86965f093..f1cf67bfb78 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -1,12 +1,12 @@ """Tests for the USB Discovery integration.""" import os import sys -from unittest.mock import MagicMock, patch, sentinel +from unittest.mock import MagicMock, call, patch, sentinel import pytest from homeassistant.components import usb -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component from . import conbee_device, slae_sh_device @@ -52,6 +52,60 @@ def mock_venv(): yield +@pytest.mark.skipif( + not sys.platform.startswith("linux"), + reason="Only works on linux", +) +async def test_observer_discovery(hass, hass_ws_client, venv): + """Test that observer can discover a device without raising an exception.""" + new_usb = [{"domain": "test1", "vid": "3039"}] + + mock_comports = [ + MagicMock( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + ] + mock_observer = None + + async def _mock_monitor_observer_callback(callback): + await hass.async_add_executor_job( + callback, MagicMock(action="create", device_path="/dev/new") + ) + + def _create_mock_monitor_observer(monitor, callback, name): + nonlocal mock_observer + hass.async_create_task(_mock_monitor_observer_callback(callback)) + mock_observer = MagicMock() + return mock_observer + + with patch("pyudev.Context"), patch( + "pyudev.MonitorObserver", new=_create_mock_monitor_observer + ), patch("pyudev.Monitor.filter_by"), patch( + "homeassistant.components.usb.async_get_usb", return_value=new_usb + ), patch( + "homeassistant.components.usb.comports", return_value=mock_comports + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow: + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "test1" + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert mock_observer.mock_calls == [call.start(), call.stop()] + + @pytest.mark.skipif( not sys.platform.startswith("linux"), reason="Only works on linux", @@ -66,7 +120,6 @@ async def test_removal_by_observer_before_started(hass, operating_system): def _create_mock_monitor_observer(monitor, callback, name): hass.async_create_task(_mock_monitor_observer_callback(callback)) - return MagicMock() new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] @@ -99,6 +152,9 @@ async def test_removal_by_observer_before_started(hass, operating_system): assert len(mock_config_flow.mock_calls) == 0 + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + async def test_discovered_by_websocket_scan(hass, hass_ws_client): """Test a device is discovered from websocket scan.""" From 85c863a76605a0aa5781a4268722dc07fffd9f12 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 3 Mar 2022 08:46:58 +0100 Subject: [PATCH 0195/1054] Allow reload of modbus (#67390) * Allow reload of modbus. Co-authored-by: Paulus Schoutsen --- homeassistant/components/modbus/modbus.py | 14 ++++++++++++++ homeassistant/components/modbus/services.yaml | 3 +++ 2 files changed, 17 insertions(+) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index c980eab2b34..3fa4a74a932 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -35,6 +35,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType from .const import ( @@ -129,6 +130,11 @@ async def async_modbus_setup( ) -> bool: """Set up Modbus component.""" + platform_names = [] + for entry in PLATFORMS: + platform_names.append(entry[1]) + await async_setup_reload_service(hass, DOMAIN, platform_names) + hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: my_hub = ModbusHub(hass, conf_hub) @@ -239,6 +245,14 @@ async def async_modbus_setup( return True +async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: + """Release modbus resources.""" + _LOGGER.info("Modbus reloading") + for hub in hass.data[DOMAIN]: + await hub.async_close() + del hass.data[DOMAIN] + + class ModbusHub: """Thread safe wrapper class for pymodbus.""" diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 373954e25df..07acf0a72df 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,3 +1,6 @@ +reload: + name: Reload + description: Reload all modbus entities. write_coil: name: Write coil description: Write to a modbus coil. From 58c00da8a046e65ec0744426b3e2aa613249e9d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 08:48:02 +0100 Subject: [PATCH 0196/1054] Bump home-assistant/builder from 2022.01.0 to 2022.03.1 (#67525) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 1f8cd2f8e9d..f365987b0d6 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -135,7 +135,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.01.0 + uses: home-assistant/builder@2022.03.1 with: args: | $BUILD_ARGS \ @@ -200,7 +200,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.01.0 + uses: home-assistant/builder@2022.03.1 with: args: | $BUILD_ARGS \ From 15580281a34385dfc8bd9fe61f3c380bdd0de5de Mon Sep 17 00:00:00 2001 From: "Richard T. Schaefer" Date: Thu, 3 Mar 2022 03:35:06 -0600 Subject: [PATCH 0197/1054] Allow area, device, and entity selectors to optionally support multiple selections like target selector (#63138) * Allow area, device, and entity selectors to optionally support multiple selections like target selector * Update according to code review comments * Adjust tests * Update according to review comments * Tweak error message for multiple entities Co-authored-by: Erik --- homeassistant/helpers/config_validation.py | 7 +- homeassistant/helpers/selector.py | 86 +++++++++++++-------- tests/components/blueprint/test_importer.py | 14 +++- tests/helpers/test_selector.py | 27 +++++-- 4 files changed, 90 insertions(+), 44 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7f33ec1f1ec..78dae80ebfb 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -729,8 +729,11 @@ _FAKE_UUID_4_HEX = re.compile(r"^[0-9a-f]{32}$") def fake_uuid4_hex(value: Any) -> str: """Validate a fake v4 UUID generated by random_uuid_hex.""" - if not _FAKE_UUID_4_HEX.match(value): - raise vol.Invalid("Invalid UUID") + try: + if not _FAKE_UUID_4_HEX.match(value): + raise vol.Invalid("Invalid UUID") + except TypeError as exc: + raise vol.Invalid("Invalid UUID") from exc return cast(str, value) # Pattern.match throws if input is not a string diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index f280feb83b2..861b2143cb9 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -8,7 +8,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT -from homeassistant.core import split_entity_id +from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator from . import config_validation as cv @@ -74,44 +74,54 @@ class Selector: return {"selector": {self.selector_type: self.config}} +SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + # Integration that provided the entity + vol.Optional("integration"): str, + # Domain the entity belongs to + vol.Optional("domain"): str, + # Device class of the entity + vol.Optional("device_class"): str, + } +) + + @SELECTORS.register("entity") class EntitySelector(Selector): - """Selector of a single entity.""" + """Selector of a single or list of entities.""" selector_type = "entity" - CONFIG_SCHEMA = vol.Schema( - { - # Integration that provided the entity - vol.Optional("integration"): str, - # Domain the entity belongs to - vol.Optional("domain"): str, - # Device class of the entity - vol.Optional("device_class"): str, - } + CONFIG_SCHEMA = SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA.extend( + {vol.Optional("multiple", default=False): cv.boolean} ) - def __call__(self, data: Any) -> str: + def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" - try: - entity_id = cv.entity_id(data) - domain = split_entity_id(entity_id)[0] - except vol.Invalid: - # Not a valid entity_id, maybe it's an entity entry id - return cv.entity_id_or_uuid(cv.string(data)) - else: - if "domain" in self.config and domain != self.config["domain"]: - raise vol.Invalid( - f"Entity {entity_id} belongs to domain {domain}, " - f"expected {self.config['domain']}" - ) - return entity_id + def validate(e_or_u: str) -> str: + e_or_u = cv.entity_id_or_uuid(e_or_u) + if not valid_entity_id(e_or_u): + return e_or_u + if allowed_domain := self.config.get("domain"): + domain = split_entity_id(e_or_u)[0] + if domain != allowed_domain: + raise vol.Invalid( + f"Entity {e_or_u} belongs to domain {domain}, " + f"expected {allowed_domain}" + ) + return e_or_u + + if not self.config["multiple"]: + return validate(data) + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return cast(list, vol.Schema([validate])(data)) # Output is a list @SELECTORS.register("device") class DeviceSelector(Selector): - """Selector of a single device.""" + """Selector of a single or list of devices.""" selector_type = "device" @@ -124,31 +134,41 @@ class DeviceSelector(Selector): # Model of device vol.Optional("model"): str, # Device has to contain entities matching this selector - vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, } ) - def __call__(self, data: Any) -> str: + def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" - return cv.string(data) + if not self.config["multiple"]: + return cv.string(data) + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [cv.string(val) for val in data] @SELECTORS.register("area") class AreaSelector(Selector): - """Selector of a single area.""" + """Selector of a single or list of areas.""" selector_type = "area" CONFIG_SCHEMA = vol.Schema( { - vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, } ) - def __call__(self, data: Any) -> str: + def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" - return cv.string(data) + if not self.config["multiple"]: + return cv.string(data) + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [cv.string(val) for val in data] @SELECTORS.register("number") diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 382363aa560..623d1e9ebbf 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -25,13 +25,14 @@ COMMUNITY_POST_INPUTS = { "integration": "zha", "manufacturer": "IKEA of Sweden", "model": "TRADFRI remote control", + "multiple": False, } }, }, "light": { "name": "Light(s)", "description": "The light(s) to control", - "selector": {"target": {"entity": {"domain": "light"}}}, + "selector": {"target": {"entity": {"domain": "light", "multiple": False}}}, }, "force_brightness": { "name": "Force turn on brightness", @@ -218,10 +219,17 @@ async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock): "motion_entity": { "name": "Motion Sensor", "selector": { - "entity": {"domain": "binary_sensor", "device_class": "motion"} + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "multiple": False, + } }, }, - "light_entity": {"name": "Light", "selector": {"entity": {"domain": "light"}}}, + "light_entity": { + "name": "Light", + "selector": {"entity": {"domain": "light", "multiple": False}}, + }, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index edf856d6843..0af8a050ce8 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -37,12 +37,6 @@ def test_invalid_base_schema(schema): selector.validate_selector(schema) -def test_validate_selector(): - """Test return is the same as input.""" - schema = {"device": {"manufacturer": "mock-manuf", "model": "mock-model"}} - assert schema == selector.validate_selector(schema) - - def _test_selector( selector_type, schema, valid_selections, invalid_selections, converter=None ): @@ -99,6 +93,11 @@ def _test_selector( ("abc123",), (None,), ), + ( + {"multiple": True}, + (["abc123", "def456"],), + ("abc123", None, ["abc123", None]), + ), ), ) def test_device_selector_schema(schema, valid_selections, invalid_selections): @@ -123,6 +122,17 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections): ("binary_sensor.abc123", FAKE_UUID), (None, "sensor.abc123"), ), + ( + {"multiple": True, "domain": "sensor"}, + (["sensor.abc123", "sensor.def456"], ["sensor.abc123", FAKE_UUID]), + ( + "sensor.abc123", + FAKE_UUID, + None, + "abc123", + ["sensor.abc123", "light.def456"], + ), + ), ), ) def test_entity_selector_schema(schema, valid_selections, invalid_selections): @@ -165,6 +175,11 @@ def test_entity_selector_schema(schema, valid_selections, invalid_selections): ("abc123",), (None,), ), + ( + {"multiple": True}, + ((["abc123", "def456"],)), + (None, "abc123", ["abc123", None]), + ), ), ) def test_area_selector_schema(schema, valid_selections, invalid_selections): From 13eeaa4c73928c44f137b4d62e6c0f6ac01180db Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 3 Mar 2022 11:29:58 +0100 Subject: [PATCH 0198/1054] rfxtrx: bump to 0.28 (#67530) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index d7125518329..edbf5c8556c 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.27.1"], + "requirements": ["pyRFXtrx==0.28.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 847dfa203ac..8a017abebab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1305,7 +1305,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.27.1 +pyRFXtrx==0.28.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acdb89f5aa0..ac07e9037ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -854,7 +854,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.27.1 +pyRFXtrx==0.28.0 # homeassistant.components.tibber pyTibber==0.22.1 From 2678d8120a0bb59545a1d63a93574925205a5a79 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 3 Mar 2022 08:16:03 -0800 Subject: [PATCH 0199/1054] Update nest media source to make device play most recent event (#67557) --- homeassistant/components/nest/media_source.py | 15 ++++++++---- tests/components/nest/test_media_source.py | 23 ++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index af8a4af8ba5..7a8ae49bdbc 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -339,15 +339,21 @@ class NestMediaSource(MediaSource): media_id: MediaId | None = parse_media_id(item.identifier) if not media_id: raise Unresolvable("No identifier specified for MediaSourceItem") - if not media_id.event_token: - raise Unresolvable( - "Identifier missing an event_token: %s" % item.identifier - ) devices = await self.devices() if not (device := devices.get(media_id.device_id)): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier ) + if not media_id.event_token: + # The device resolves to the most recent event if available + if not ( + last_event_id := await _async_get_recent_event_id(media_id, device) + ): + raise Unresolvable( + "Unable to resolve recent event for device: %s" % item.identifier + ) + media_id = last_event_id + # Infer content type from the device, since it only supports one # snapshot type (either jpg or mp4 clip) content_type = EventImageType.IMAGE.content_type @@ -384,6 +390,7 @@ class NestMediaSource(MediaSource): device_id=last_event_id.device_id, event_token=last_event_id.event_token, ) + browse_device.can_play = True browse_root.children.append(browse_device) return browse_root diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 2361049ecc1..c733d4d3cfe 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -327,6 +327,7 @@ async def test_camera_event(hass, auth, hass_client): assert browse.children[0].identifier == device.id assert browse.children[0].title == "Front: Recent Events" assert browse.children[0].can_expand + assert browse.children[0].can_play # Expanding the root does not expand the device assert len(browse.children[0].children) == 0 @@ -371,6 +372,13 @@ async def test_camera_event(hass, auth, hass_client): contents = await response.read() assert contents == IMAGE_BYTES_FROM_EVENT + # Resolving the device id points to the most recent event + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + ) + assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" + assert media.mime_type == "image/jpeg" + async def test_event_order(hass, auth): """Test multiple events are in descending timestamp order.""" @@ -787,6 +795,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): browse.children[0].thumbnail == f"/api/nest/event_media/{device.id}/{event_identifier}/thumbnail" ) + assert browse.children[0].can_play # Browse to the device browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" @@ -805,7 +814,6 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 assert browse.children[0].can_play - # No thumbnail support for mp4 clips yet assert ( browse.children[0].thumbnail == f"/api/nest/event_media/{device.id}/{event_identifier}/thumbnail" @@ -973,6 +981,10 @@ async def test_multiple_devices(hass, auth, hass_client): assert device2 # Very no events have been received yet + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert len(browse.children) == 2 + assert not browse.children[0].can_play + assert not browse.children[1].can_play browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" ) @@ -998,6 +1010,10 @@ async def test_multiple_devices(hass, auth, hass_client): ) await hass.async_block_till_done() + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert len(browse.children) == 2 + assert browse.children[0].can_play + assert not browse.children[1].can_play browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" ) @@ -1020,6 +1036,10 @@ async def test_multiple_devices(hass, auth, hass_client): ) await hass.async_block_till_done() + browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") + assert len(browse.children) == 2 + assert browse.children[0].can_play + assert browse.children[1].can_play browse = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device1.id}" ) @@ -1427,6 +1447,7 @@ async def test_camera_image_resize(hass, auth, hass_client): browse.children[0].thumbnail == f"/api/nest/event_media/{device.id}/{event_identifier}/thumbnail" ) + assert browse.children[0].can_play # Browse to device. No thumbnail is needed for the device on the device page browse = await media_source.async_browse_media( From 1a78e18eeb1ebd40fe678dcfef3ab34495db49cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 3 Mar 2022 17:27:09 +0100 Subject: [PATCH 0200/1054] Add update integration (#66552) Co-authored-by: Paulus Schoutsen --- .core_files.yaml | 1 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/default_config/manifest.json | 3 +- homeassistant/components/update/__init__.py | 273 ++++++++++++++ homeassistant/components/update/manifest.json | 10 + homeassistant/components/update/strings.json | 3 + .../components/update/translations/en.json | 3 + mypy.ini | 11 + tests/components/update/__init__.py | 1 + tests/components/update/test_init.py | 347 ++++++++++++++++++ 11 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/update/__init__.py create mode 100644 homeassistant/components/update/manifest.json create mode 100644 homeassistant/components/update/strings.json create mode 100644 homeassistant/components/update/translations/en.json create mode 100644 tests/components/update/__init__.py create mode 100644 tests/components/update/test_init.py diff --git a/.core_files.yaml b/.core_files.yaml index ebc3ff376f8..160a0d80d9a 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -94,6 +94,7 @@ components: &components - homeassistant/components/tag/* - homeassistant/components/template/* - homeassistant/components/timer/* + - homeassistant/components/update/* - homeassistant/components/usb/* - homeassistant/components/webhook/* - homeassistant/components/websocket_api/* diff --git a/.strict-typing b/.strict-typing index 73975e2d1f7..a5a2127eb68 100644 --- a/.strict-typing +++ b/.strict-typing @@ -204,6 +204,7 @@ homeassistant.components.tts.* homeassistant.components.twentemilieu.* homeassistant.components.unifiprotect.* homeassistant.components.upcloud.* +homeassistant.components.update.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* homeassistant.components.usb.* diff --git a/CODEOWNERS b/CODEOWNERS index ce1e69244d0..82930f875f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1053,6 +1053,8 @@ tests/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop tests/components/upcloud/* @scop +homeassistant/components/update/* @home-assistant/core +tests/components/update/* @home-assistant/core homeassistant/components/updater/* @home-assistant/core tests/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman @ehendrix23 diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 1ab827529c6..7059df580d9 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -31,10 +31,11 @@ "tag", "timer", "usb", + "update", "webhook", "zeroconf", "zone" ], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py new file mode 100644 index 00000000000..66f99b83117 --- /dev/null +++ b/homeassistant/components/update/__init__.py @@ -0,0 +1,273 @@ +"""Support for Update.""" +from __future__ import annotations + +import asyncio +import dataclasses +import logging +from typing import Any, Protocol + +import async_timeout +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import integration_platform, storage +from homeassistant.helpers.typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "update" +INFO_CALLBACK_TIMEOUT = 5 +STORAGE_VERSION = 1 + + +class IntegrationUpdateFailed(HomeAssistantError): + """Error to indicate an update has failed.""" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Update integration.""" + hass.data[DOMAIN] = UpdateManager(hass=hass) + websocket_api.async_register_command(hass, handle_info) + websocket_api.async_register_command(hass, handle_update) + websocket_api.async_register_command(hass, handle_skip) + return True + + +@websocket_api.websocket_command({vol.Required("type"): "update/info"}) +@websocket_api.async_response +async def handle_info( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get pending updates from all platforms.""" + manager: UpdateManager = hass.data[DOMAIN] + updates = await manager.gather_updates() + connection.send_result(msg["id"], updates) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "update/skip", + vol.Required("domain"): str, + vol.Required("identifier"): str, + vol.Required("version"): str, + } +) +@websocket_api.async_response +async def handle_skip( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Skip an update.""" + manager: UpdateManager = hass.data[DOMAIN] + + if not await manager.domain_is_valid(msg["domain"]): + connection.send_error( + msg["id"], websocket_api.ERR_NOT_FOUND, "Domain not supported" + ) + return + + manager.skip_update(msg["domain"], msg["identifier"], msg["version"]) + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "update/update", + vol.Required("domain"): str, + vol.Required("identifier"): str, + vol.Required("version"): str, + vol.Optional("backup"): bool, + } +) +@websocket_api.async_response +async def handle_update( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle an update.""" + manager: UpdateManager = hass.data[DOMAIN] + + if not await manager.domain_is_valid(msg["domain"]): + connection.send_error( + msg["id"], + websocket_api.ERR_NOT_FOUND, + f"{msg['domain']} is not a supported domain", + ) + return + + try: + await manager.perform_update( + domain=msg["domain"], + identifier=msg["identifier"], + version=msg["version"], + backup=msg.get("backup"), + ) + except IntegrationUpdateFailed as err: + connection.send_error( + msg["id"], + "update_failed", + str(err), + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Update of %s to version %s failed", + msg["identifier"], + msg["version"], + ) + connection.send_error( + msg["id"], + "update_failed", + "Unknown Error", + ) + else: + connection.send_result(msg["id"]) + + +class UpdatePlatformProtocol(Protocol): + """Define the format that update platforms can have.""" + + async def async_list_updates(self, hass: HomeAssistant) -> list[UpdateDescription]: + """List all updates available in the integration.""" + + async def async_perform_update( + self, + hass: HomeAssistant, + identifier: str, + version: str, + **kwargs: Any, + ) -> None: + """Perform an update.""" + + +@dataclasses.dataclass() +class UpdateDescription: + """Describe an update update.""" + + identifier: str + name: str + current_version: str + available_version: str + changelog_content: str | None = None + changelog_url: str | None = None + icon_url: str | None = None + supports_backup: bool = False + + +class UpdateManager: + """Update manager for the update integration.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the update manager.""" + self._hass = hass + self._store = storage.Store( + hass=hass, + version=STORAGE_VERSION, + key=DOMAIN, + ) + self._skip: set[str] = set() + self._platforms: dict[str, UpdatePlatformProtocol] = {} + self._loaded = False + + async def add_platform( + self, + hass: HomeAssistant, + integration_domain: str, + platform: UpdatePlatformProtocol, + ) -> None: + """Add a platform to the update manager.""" + self._platforms[integration_domain] = platform + + async def _load(self) -> None: + """Load platforms and data from storage.""" + await integration_platform.async_process_integration_platforms( + self._hass, DOMAIN, self.add_platform + ) + from_storage = await self._store.async_load() + if isinstance(from_storage, dict): + self._skip = set(from_storage["skipped"]) + + self._loaded = True + + async def gather_updates(self) -> list[dict[str, Any]]: + """Gather updates.""" + if not self._loaded: + await self._load() + + updates: dict[str, list[UpdateDescription] | None] = {} + + for domain, update_descriptions in zip( + self._platforms, + await asyncio.gather( + *( + self._get_integration_info(integration_domain, registration) + for integration_domain, registration in self._platforms.items() + ) + ), + ): + updates[domain] = update_descriptions + + return [ + { + "domain": integration_domain, + **dataclasses.asdict(description), + } + for integration_domain, update_descriptions in updates.items() + if update_descriptions is not None + for description in update_descriptions + if f"{integration_domain}_{description.identifier}_{description.available_version}" + not in self._skip + ] + + async def domain_is_valid(self, domain: str) -> bool: + """Return if the domain is valid.""" + if not self._loaded: + await self._load() + return domain in self._platforms + + @callback + def _data_to_save(self) -> dict[str, Any]: + """Schedule storing the data.""" + return {"skipped": list(self._skip)} + + async def perform_update( + self, + domain: str, + identifier: str, + version: str, + **kwargs: Any, + ) -> None: + """Perform an update.""" + await self._platforms[domain].async_perform_update( + hass=self._hass, + identifier=identifier, + version=version, + **kwargs, + ) + + @callback + def skip_update(self, domain: str, identifier: str, version: str) -> None: + """Skip an update.""" + self._skip.add(f"{domain}_{identifier}_{version}") + self._store.async_delay_save(self._data_to_save, 60) + + async def _get_integration_info( + self, + integration_domain: str, + platform: UpdatePlatformProtocol, + ) -> list[UpdateDescription] | None: + """Get integration update details.""" + + try: + async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): + return await platform.async_list_updates(hass=self._hass) + except asyncio.TimeoutError: + _LOGGER.warning("Timeout while getting updates from %s", integration_domain) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error fetching info from %s", integration_domain) + return None diff --git a/homeassistant/components/update/manifest.json b/homeassistant/components/update/manifest.json new file mode 100644 index 00000000000..a005381fb5f --- /dev/null +++ b/homeassistant/components/update/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "update", + "name": "Update", + "documentation": "https://www.home-assistant.io/integrations/update", + "codeowners": [ + "@home-assistant/core" + ], + "quality_scale": "internal", + "iot_class": "calculated" +} \ No newline at end of file diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json new file mode 100644 index 00000000000..95b82de3b4d --- /dev/null +++ b/homeassistant/components/update/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/en.json b/homeassistant/components/update/translations/en.json new file mode 100644 index 00000000000..95b82de3b4d --- /dev/null +++ b/homeassistant/components/update/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index e35fb90f0ea..045e9a4eee8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2045,6 +2045,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.update.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.uptime.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/update/__init__.py b/tests/components/update/__init__.py new file mode 100644 index 00000000000..f3e55ca4ed3 --- /dev/null +++ b/tests/components/update/__init__.py @@ -0,0 +1 @@ +"""Tests for the Update integration.""" diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py new file mode 100644 index 00000000000..f91df08bf52 --- /dev/null +++ b/tests/components/update/test_init.py @@ -0,0 +1,347 @@ +"""Tests for the Update integration init.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from typing import Any +from unittest.mock import Mock, patch + +from aiohttp import ClientWebSocketResponse +import pytest + +from homeassistant.components.update import ( + DOMAIN, + IntegrationUpdateFailed, + UpdateDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import mock_platform + + +async def setup_mock_domain( + hass: HomeAssistant, + async_list_updates: Callable[[HomeAssistant], Awaitable[list[UpdateDescription]]] + | None = None, + async_perform_update: Callable[[HomeAssistant, str, str], Awaitable[bool]] + | None = None, +) -> None: + """Set up a mock domain.""" + + async def _mock_async_list_updates(hass: HomeAssistant) -> list[UpdateDescription]: + return [ + UpdateDescription( + identifier="lorem_ipsum", + name="Lorem Ipsum", + current_version="1.0.0", + available_version="1.0.1", + ) + ] + + async def _mock_async_perform_update( + hass: HomeAssistant, + identifier: str, + version: str, + **kwargs: Any, + ) -> bool: + return True + + mock_platform( + hass, + "some_domain.update", + Mock( + async_list_updates=async_list_updates or _mock_async_list_updates, + async_perform_update=async_perform_update or _mock_async_perform_update, + ), + ) + + assert await async_setup_component(hass, "some_domain", {}) + + +async def gather_update_info( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> list[dict]: + """Gather all info.""" + client = await hass_ws_client(hass) + await client.send_json({"id": 1, "type": "update/info"}) + resp = await client.receive_json() + return resp["result"] + + +async def test_update_updates( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test getting updates.""" + await setup_mock_domain(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.components.update.storage.Store.async_load", + return_value={"skipped": []}, + ): + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + data = data[0] == { + "domain": "some_domain", + "identifier": "lorem_ipsum", + "name": "Lorem Ipsum", + "current_version": "1.0.0", + "available_version": "1.0.1", + "changelog_url": None, + "icon_url": None, + } + + +async def test_update_updates_with_timeout_error( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test timeout while getting updates.""" + + async def mock_async_list_updates(hass: HomeAssistant) -> list[UpdateDescription]: + raise asyncio.TimeoutError() + + await setup_mock_domain(hass, async_list_updates=mock_async_list_updates) + + assert await async_setup_component(hass, DOMAIN, {}) + + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 0 + + +async def test_update_updates_with_exception( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test exception while getting updates.""" + + async def mock_async_list_updates(hass: HomeAssistant) -> list[UpdateDescription]: + raise Exception() + + await setup_mock_domain(hass, async_list_updates=mock_async_list_updates) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 0 + + +async def test_update_update( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test performing an update.""" + await setup_mock_domain(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + update = data[0] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/update", + "domain": update["domain"], + "identifier": update["identifier"], + "version": update["available_version"], + } + ) + resp = await client.receive_json() + assert resp["success"] + + +async def test_skip_update( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test skipping updates.""" + await setup_mock_domain(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + update = data[0] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/skip", + "domain": update["domain"], + "identifier": update["identifier"], + "version": update["available_version"], + } + ) + resp = await client.receive_json() + assert resp["success"] + + data = await gather_update_info(hass, hass_ws_client) + assert len(data) == 0 + + +async def test_skip_non_existing_update( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test skipping non-existing updates.""" + await setup_mock_domain(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/skip", + "domain": "non_existing", + "identifier": "non_existing", + "version": "non_existing", + } + ) + resp = await client.receive_json() + assert not resp["success"] + + data = await gather_update_info(hass, hass_ws_client) + assert len(data) == 1 + + +async def test_update_update_non_existing( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that we fail when trying to update something that does not exist.""" + await setup_mock_domain(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/update", + "domain": "does_not_exist", + "identifier": "does_not_exist", + "version": "non_existing", + } + ) + resp = await client.receive_json() + assert not resp["success"] + assert resp["error"]["code"] == "not_found" + + +async def test_update_update_failed( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that we correctly handle failed updates.""" + + async def mock_async_perform_update( + hass: HomeAssistant, + identifier: str, + version: str, + **kwargs, + ) -> bool: + raise IntegrationUpdateFailed("Test update failed") + + await setup_mock_domain(hass, async_perform_update=mock_async_perform_update) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + update = data[0] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/update", + "domain": update["domain"], + "identifier": update["identifier"], + "version": update["available_version"], + } + ) + resp = await client.receive_json() + assert not resp["success"] + assert resp["error"]["code"] == "update_failed" + assert resp["error"]["message"] == "Test update failed" + + +async def test_update_update_failed_generic( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that we correctly handle failed updates.""" + + async def mock_async_perform_update( + hass: HomeAssistant, + identifier: str, + version: str, + **kwargs, + ) -> bool: + raise TypeError("Test update failed") + + await setup_mock_domain(hass, async_perform_update=mock_async_perform_update) + + assert await async_setup_component(hass, DOMAIN, {}) + data = await gather_update_info(hass, hass_ws_client) + + assert len(data) == 1 + update = data[0] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/update", + "domain": update["domain"], + "identifier": update["identifier"], + "version": update["available_version"], + } + ) + resp = await client.receive_json() + assert not resp["success"] + assert resp["error"]["code"] == "update_failed" + assert resp["error"]["message"] == "Unknown Error" + assert "Test update failed" in caplog.text + + +async def test_update_before_info( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test that we fail when trying to update something that does not exist.""" + await setup_mock_domain(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "update/update", + "domain": "does_not_exist", + "identifier": "does_not_exist", + "version": "non_existing", + } + ) + resp = await client.receive_json() + assert not resp["success"] + assert resp["error"]["code"] == "not_found" From 74483d2669a811f7f9069223b52d322f0b63a92f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 3 Mar 2022 19:06:33 +0100 Subject: [PATCH 0201/1054] Fix Samsung TV state when the device is turned off (#67541) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 17 +++++ .../components/samsungtv/test_media_player.py | 75 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 342c4f7f429..8fbe0572379 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -300,6 +300,7 @@ class SamsungTVWSBridge(SamsungTVBridge): self.token = token self._rest_api: SamsungTVAsyncRest | None = None self._app_list: dict[str, str] | None = None + self._device_info: dict[str, Any] | None = None self._remote: SamsungTVWSAsyncRemote | None = None self._remote_lock = asyncio.Lock() @@ -323,8 +324,20 @@ class SamsungTVWSBridge(SamsungTVBridge): LOGGER.debug("Generated app list: %s", self._app_list) return self._app_list + def _get_device_spec(self, key: str) -> Any | None: + """Check if a flag exists in latest device info.""" + if not ((info := self._device_info) and (device := info.get("device"))): + return None + return device.get(key) + async def async_is_on(self) -> bool: """Tells if the TV is on.""" + if self._get_device_spec("PowerState") is not None: + LOGGER.debug("Checking if TV %s is on using device info", self.host) + # Ensure we get an updated value + info = await self.async_device_info() + return info is not None and info["device"]["PowerState"] == "on" + LOGGER.debug("Checking if TV %s is on using websocket", self.host) if remote := await self._async_get_remote(): return remote.is_alive() return False @@ -383,6 +396,7 @@ class SamsungTVWSBridge(SamsungTVBridge): with contextlib.suppress(HttpApiError, AsyncioTimeoutError): device_info: dict[str, Any] = await self._rest_api.rest_device_info() LOGGER.debug("Device info on %s is: %s", self.host, device_info) + self._device_info = device_info return device_info return None @@ -453,6 +467,9 @@ class SamsungTVWSBridge(SamsungTVBridge): LOGGER.debug( "Created SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host ) + if self._device_info is None: + # Initialise device info on first connect + await self.async_device_info() if self.token != self._remote.token: LOGGER.debug( "SamsungTVWSBridge has provided a new token %s", diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 4c324abe4d8..af35c40d074 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,5 +1,6 @@ """Tests for samsungtv component.""" import asyncio +from copy import deepcopy from datetime import datetime, timedelta import logging from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch @@ -7,7 +8,7 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch import pytest from samsungctl import exceptions from samsungtvws.async_remote import SamsungTVWSAsyncRemote -from samsungtvws.exceptions import ConnectionFailure +from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import WebSocketException @@ -267,11 +268,14 @@ async def test_update_off(hass: HomeAssistant, mock_now: datetime) -> None: assert state.state == STATE_OFF -async def test_update_off_ws( - hass: HomeAssistant, remotews: Mock, mock_now: datetime +async def test_update_off_ws_no_power_state( + hass: HomeAssistant, remotews: Mock, rest_api: Mock, mock_now: datetime ) -> None: """Testing update tv off.""" await setup_samsungtv(hass, MOCK_CONFIGWS) + # device_info should only get called once, as part of the setup + rest_api.rest_device_info.assert_called_once() + rest_api.rest_device_info.reset_mock() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -286,6 +290,71 @@ async def test_update_off_ws( state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF + rest_api.rest_device_info.assert_not_called() + + +@pytest.mark.usefixtures("remotews") +async def test_update_off_ws_with_power_state( + hass: HomeAssistant, remotews: Mock, rest_api: Mock, mock_now: datetime +) -> None: + """Testing update tv off.""" + with patch.object( + rest_api, "rest_device_info", side_effect=HttpApiError + ) as mock_device_info, patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ) as mock_start_listening: + await setup_samsungtv(hass, MOCK_CONFIGWS) + + mock_device_info.assert_called_once() + mock_start_listening.assert_called_once() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + # First update uses start_listening once, and initialises device_info + device_info = deepcopy(rest_api.rest_device_info.return_value) + device_info["device"]["PowerState"] = "on" + rest_api.rest_device_info.return_value = device_info + next_update = mock_now + timedelta(minutes=1) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + remotews.start_listening.assert_called_once() + rest_api.rest_device_info.assert_called_once() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + # After initial update, start_listening shouldn't be called + remotews.start_listening.reset_mock() + + # Second update uses device_info(ON) + rest_api.rest_device_info.reset_mock() + next_update = mock_now + timedelta(minutes=2) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + rest_api.rest_device_info.assert_called_once() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + # Third update uses device_info (OFF) + rest_api.rest_device_info.reset_mock() + device_info["device"]["PowerState"] = "off" + next_update = mock_now + timedelta(minutes=3) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + rest_api.rest_device_info.assert_called_once() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + remotews.start_listening.assert_not_called() @pytest.mark.usefixtures("remote") From 8e3b0f655431a18c19a1d36da387dffabd5b6223 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 3 Mar 2022 19:07:08 +0100 Subject: [PATCH 0202/1054] Cleanup mac fixtures in samsungtv tests (#67529) Co-authored-by: epenet --- tests/components/samsungtv/conftest.py | 17 ++---- tests/components/samsungtv/const.py | 11 ++++ .../components/samsungtv/test_config_flow.py | 57 +++++++------------ .../components/samsungtv/test_diagnostics.py | 12 +--- tests/components/samsungtv/test_init.py | 6 +- .../components/samsungtv/test_media_player.py | 21 +------ 6 files changed, 44 insertions(+), 80 deletions(-) diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 7975c291fe3..d8bbfbf4627 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -8,7 +8,7 @@ from samsungtvws.async_remote import SamsungTVWSAsyncRemote import homeassistant.util.dt as dt_util -from .const import SAMPLE_APP_LIST +from .const import SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_WIFI @pytest.fixture(autouse=True) @@ -39,16 +39,9 @@ def rest_api_fixture() -> Mock: "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", autospec=True, ) as rest_api_class: - rest_api_class.return_value.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "wifiMac": "aa:bb:cc:dd:ee:ff", - "name": "[TV] Living Room", - "type": "Samsung SmartTV", - "networkType": "wireless", - }, - } + rest_api_class.return_value.rest_device_info.return_value = ( + SAMPLE_DEVICE_INFO_WIFI + ) yield rest_api_class.return_value @@ -82,7 +75,7 @@ def mock_now() -> datetime: return dt_util.utcnow() -@pytest.fixture(name="no_mac_address") +@pytest.fixture(name="mac_address", autouse=True) def mac_address_fixture() -> Mock: """Patch getmac.get_mac_address.""" with patch("getmac.get_mac_address", return_value=None) as mac: diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index d56e540e64b..a3bbcb2dd7f 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -22,3 +22,14 @@ SAMPLE_APP_LIST = [ "name": "Spotify - Music and Podcasts", }, ] + +SAMPLE_DEVICE_INFO_WIFI = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:ww:ii:ff:ii", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + "networkType": "wireless", + }, +} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 94056a71a50..98eef3d7810 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -99,7 +99,7 @@ MOCK_SSDP_DATA_WRONGMODEL = ssdp.SsdpServiceInfo( }, ) MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( - ip="fake_host", macaddress="aa:bb:cc:dd:ee:ff", hostname="fake_hostname" + ip="fake_host", macaddress="aa:bb:dd:hh:cc:pp", hostname="fake_hostname" ) EXISTING_IP = "192.168.40.221" MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( @@ -109,7 +109,7 @@ MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( name="mock_name", port=1234, properties={ - "deviceid": "aa:bb:cc:dd:ee:ff", + "deviceid": "aa:bb:zz:ee:rr:oo", "manufacturer": "fake_manufacturer", "model": "fake_model", "serialNumber": "fake_serial", @@ -316,10 +316,8 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote") -async def test_ssdp(hass: HomeAssistant, no_mac_address: Mock) -> None: +async def test_ssdp(hass: HomeAssistant) -> None: """Test starting a flow from discovery.""" - - no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, @@ -345,10 +343,8 @@ async def test_ssdp(hass: HomeAssistant, no_mac_address: Mock) -> None: @pytest.mark.usefixtures("remote") -async def test_ssdp_noprefix(hass: HomeAssistant, no_mac_address: Mock) -> None: +async def test_ssdp_noprefix(hass: HomeAssistant) -> None: """Test starting a flow from discovery without prefixes.""" - - no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO_2, @@ -449,7 +445,7 @@ async def test_ssdp_websocket_success_populates_mac_address( assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" - assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "82GXARRS" assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -490,7 +486,6 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("no_mac_address") async def test_ssdp_not_successful(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( @@ -519,7 +514,6 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("no_mac_address") async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( @@ -549,12 +543,8 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote") -async def test_ssdp_already_in_progress( - hass: HomeAssistant, no_mac_address: Mock -) -> None: +async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: """Test starting a flow from discovery twice.""" - - no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, @@ -576,12 +566,8 @@ async def test_ssdp_already_in_progress( @pytest.mark.usefixtures("remote") -async def test_ssdp_already_configured( - hass: HomeAssistant, no_mac_address: Mock -) -> None: +async def test_ssdp_already_configured(hass: HomeAssistant) -> None: """Test starting a flow from discovery when already configured.""" - - no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", return_value=MOCK_DEVICE_INFO, @@ -609,10 +595,8 @@ async def test_ssdp_already_configured( @pytest.mark.usefixtures("remote") -async def test_import_legacy(hass: HomeAssistant, no_mac_address: Mock) -> None: +async def test_import_legacy(hass: HomeAssistant) -> None: """Test importing from yaml with hostname.""" - - no_mac_address.return_value = "aa:bb:cc:dd:ee:ff" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -632,7 +616,7 @@ async def test_import_legacy(hass: HomeAssistant, no_mac_address: Mock) -> None: assert entries[0].data[CONF_PORT] == LEGACY_PORT -@pytest.mark.usefixtures("remote", "remotews", "no_mac_address") +@pytest.mark.usefixtures("remote", "remotews") async def test_import_legacy_without_name(hass: HomeAssistant, rest_api: Mock) -> None: """Test importing from yaml without a name.""" rest_api.rest_device_info.return_value = None @@ -733,7 +717,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" - assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -759,7 +743,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" - assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["data"][CONF_MODEL] == "82GXARRS" assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -870,8 +854,9 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" -async def test_websocket_no_mac(hass: HomeAssistant) -> None: +async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: """Test for send key with autodetection of protocol.""" + mac_address.return_value = "gg:ee:tt:mm:aa:cc" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), @@ -879,9 +864,7 @@ async def test_websocket_no_mac(hass: HomeAssistant) -> None: "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" ) as remotews, patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", - ) as rest_api_class, patch( - "getmac.get_mac_address", return_value="gg:hh:ii:ll:mm:nn" - ): + ) as rest_api_class: remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock(return_value=False) @@ -908,14 +891,14 @@ async def test_websocket_no_mac(hass: HomeAssistant) -> None: assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "websocket" assert result["data"][CONF_TOKEN] == "123456789" - assert result["data"][CONF_MAC] == "gg:hh:ii:ll:mm:nn" + assert result["data"][CONF_MAC] == "gg:ee:tt:mm:aa:cc" remotews.assert_called_once_with(**AUTODETECT_WEBSOCKET_SSL) rest_api_class.assert_called_once_with(**DEVICEINFO_WEBSOCKET_SSL) await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].data[CONF_MAC] == "gg:hh:ii:ll:mm:nn" + assert entries[0].data[CONF_MAC] == "gg:ee:tt:mm:aa:cc" async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: @@ -1058,7 +1041,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.data[CONF_MAC] == "aa:bb:dd:hh:cc:pp" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1086,7 +1069,7 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @@ -1115,7 +1098,7 @@ async def test_update_missing_mac_unique_id_added_from_ssdp( assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" @@ -1147,7 +1130,7 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( assert len(mock_setup_entry.mock_calls) == 1 assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.data[CONF_MAC] == "aa:bb:zz:ee:rr:oo" assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 67bc012ced1..47446e9103c 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -7,6 +7,7 @@ from homeassistant.components.samsungtv import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from .const import SAMPLE_DEVICE_INFO_WIFI from .test_media_player import MOCK_ENTRY_WS_WITH_MAC from tests.common import MockConfigEntry @@ -56,14 +57,5 @@ async def test_entry_diagnostics( "unique_id": "any", "version": 2, }, - "device_info": { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "name": "[TV] Living Room", - "networkType": "wireless", - "type": "Samsung SmartTV", - "wifiMac": "aa:bb:cc:dd:ee:ff", - }, - }, + "device_info": SAMPLE_DEVICE_INFO_WIFI, } diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 6beac1b65bc..bc815b838ed 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -55,7 +55,7 @@ REMOTE_CALL = { } -@pytest.mark.usefixtures("remotews", "no_mac_address") +@pytest.mark.usefixtures("remotews") async def test_setup(hass: HomeAssistant) -> None: """Test Samsung TV integration is setup.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -102,7 +102,7 @@ async def test_setup_from_yaml_without_port_device_online(hass: HomeAssistant) - config_entries_domain = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) assert len(config_entries_domain) == 1 - assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert config_entries_domain[0].data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" @pytest.mark.usefixtures("remote") @@ -123,7 +123,7 @@ async def test_setup_duplicate_config( assert "duplicate host entries found" in caplog.text -@pytest.mark.usefixtures("remote", "remotews", "no_mac_address") +@pytest.mark.usefixtures("remote", "remotews") async def test_setup_duplicate_entries(hass: HomeAssistant) -> None: """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index af35c40d074..2969f2ba2d0 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -125,9 +125,6 @@ MOCK_CONFIG_NOTURNON = { ] } -# Fake mac address in all mediaplayer tests. -pytestmark = pytest.mark.usefixtures("no_mac_address") - @pytest.fixture(name="delay") def delay_fixture(): @@ -181,12 +178,10 @@ async def test_setup_websocket(hass: HomeAssistant) -> None: config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) assert len(config_entries) == 1 - assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert config_entries[0].data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" -async def test_setup_websocket_2( - hass: HomeAssistant, mock_now: datetime, rest_api: Mock -) -> None: +async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> None: """Test setup of platform from config entry.""" entity_id = f"{DOMAIN}.fake" @@ -201,16 +196,6 @@ async def test_setup_websocket_2( assert len(config_entries) == 1 assert entry is config_entries[0] - rest_api.rest_device_info.return_value = { - "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", - "device": { - "modelName": "82GXARRS", - "wifiMac": "aa:bb:cc:dd:ee:ff", - "name": "[TV] Living Room", - "type": "Samsung SmartTV", - "networkType": "wireless", - }, - } with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote" ) as remote_class: @@ -223,7 +208,7 @@ async def test_setup_websocket_2( assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) await hass.async_block_till_done() - assert config_entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert config_entries[0].data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): From cfed1ff7994295aa30dfc3b0e4da3377e48dbbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20=C3=96berg?= <62830707+droberg@users.noreply.github.com> Date: Thu, 3 Mar 2022 20:41:59 +0100 Subject: [PATCH 0203/1054] Add config flow for selecting precision of DS18B20 devices (#64315) Co-authored-by: epenet --- homeassistant/components/onewire/__init__.py | 8 + .../components/onewire/config_flow.py | 177 ++++++++++++- homeassistant/components/onewire/const.py | 14 ++ homeassistant/components/onewire/sensor.py | 48 +++- homeassistant/components/onewire/strings.json | 27 ++ .../components/onewire/translations/en.json | 27 ++ tests/components/onewire/conftest.py | 7 +- tests/components/onewire/const.py | 48 ++++ tests/components/onewire/test_diagnostics.py | 7 +- tests/components/onewire/test_options_flow.py | 237 ++++++++++++++++++ 10 files changed, 590 insertions(+), 10 deletions(-) create mode 100644 tests/components/onewire/test_options_flow.py diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 70a0a5fc856..ceef037bfcc 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -30,6 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + return True @@ -41,3 +43,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok + + +async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + _LOGGER.info("Configuration options updated, reloading OneWire integration") + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 20b76ff236b..a7373206666 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -1,14 +1,17 @@ """Config flow for 1-Wire component.""" from __future__ import annotations +import logging from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.device_registry import DeviceRegistry from .const import ( CONF_MOUNT_DIR, @@ -17,8 +20,15 @@ from .const import ( DEFAULT_OWSERVER_HOST, DEFAULT_OWSERVER_PORT, DEFAULT_SYSBUS_MOUNT_DIR, + DEVICE_SUPPORT_OPTIONS, DOMAIN, + INPUT_ENTRY_CLEAR_OPTIONS, + INPUT_ENTRY_DEVICE_SELECTION, + OPTION_ENTRY_DEVICE_OPTIONS, + OPTION_ENTRY_SENSOR_PRECISION, + PRECISION_MAPPING_FAMILY_28, ) +from .model import OWServerDeviceDescription from .onewirehub import CannotConnect, InvalidPath, OneWireHub DATA_SCHEMA_USER = vol.Schema( @@ -37,6 +47,9 @@ DATA_SCHEMA_MOUNTDIR = vol.Schema( ) +_LOGGER = logging.getLogger(__name__) + + async def validate_input_owserver( hass: HomeAssistant, data: dict[str, Any] ) -> dict[str, str]: @@ -164,3 +177,163 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=DATA_SCHEMA_MOUNTDIR, errors=errors, ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return OnewireOptionsFlowHandler(config_entry) + + +class OnewireOptionsFlowHandler(OptionsFlow): + """Handle OneWire Config options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize OneWire Network options flow.""" + self.entry_id = config_entry.entry_id + self.options = dict(config_entry.options) + self.configurable_devices: dict[str, OWServerDeviceDescription] = {} + self.devices_to_configure: dict[str, OWServerDeviceDescription] = {} + self.current_device: str = "" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + controller: OneWireHub = self.hass.data[DOMAIN][self.entry_id] + if controller.type == CONF_TYPE_SYSBUS: + return self.async_abort( + reason="SysBus setup does not have any config options." + ) + + all_devices: list[OWServerDeviceDescription] = controller.devices # type: ignore[assignment] + if not all_devices: + return self.async_abort(reason="No configurable devices found.") + + device_registry = dr.async_get(self.hass) + self.configurable_devices = { + self._get_device_long_name(device_registry, device.id): device + for device in all_devices + if device.family in DEVICE_SUPPORT_OPTIONS + } + + return await self.async_step_device_selection(user_input=None) + + async def async_step_device_selection( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select what devices to configure.""" + errors = {} + if user_input is not None: + if user_input.get(INPUT_ENTRY_CLEAR_OPTIONS): + # Reset all options + self.options = {} + return await self._update_options() + + selected_devices: list[str] = ( + user_input.get(INPUT_ENTRY_DEVICE_SELECTION) or [] + ) + if selected_devices: + self.devices_to_configure = { + device_name: self.configurable_devices[device_name] + for device_name in selected_devices + } + + return await self.async_step_configure_device(user_input=None) + errors["base"] = "device_not_selected" + + return self.async_show_form( + step_id="device_selection", + data_schema=vol.Schema( + { + vol.Optional( + INPUT_ENTRY_CLEAR_OPTIONS, + default=False, + ): bool, + vol.Optional( + INPUT_ENTRY_DEVICE_SELECTION, + default=self._get_current_configured_sensors(), + description="Multiselect with list of devices to choose from", + ): cv.multi_select( + {device: False for device in self.configurable_devices.keys()} + ), + } + ), + errors=errors, + ) + + async def async_step_configure_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Config precision option for device.""" + if user_input is not None: + self._update_device_options(user_input) + if self.devices_to_configure: + return await self.async_step_configure_device(user_input=None) + return await self._update_options() + + self.current_device, description = self.devices_to_configure.popitem() + data_schema: vol.Schema + if description.family == "28": + data_schema = vol.Schema( + { + vol.Required( + OPTION_ENTRY_SENSOR_PRECISION, + default=self._get_current_setting( + description.id, OPTION_ENTRY_SENSOR_PRECISION, "temperature" + ), + ): vol.In(PRECISION_MAPPING_FAMILY_28), + } + ) + + return self.async_show_form( + step_id="configure_device", + data_schema=data_schema, + description_placeholders={"sensor_id": self.current_device}, + ) + + async def _update_options(self) -> FlowResult: + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) + + @staticmethod + def _get_device_long_name( + device_registry: DeviceRegistry, current_device: str + ) -> str: + device = device_registry.async_get_device({(DOMAIN, current_device)}) + if device and device.name_by_user: + return f"{device.name_by_user} ({current_device})" + return current_device + + def _get_current_configured_sensors(self) -> list[str]: + """Get current list of sensors that are configured.""" + configured_sensors = self.options.get(OPTION_ENTRY_DEVICE_OPTIONS) + if not configured_sensors: + return [] + return [ + device_name + for device_name, description in self.configurable_devices.items() + if description.id in configured_sensors + ] + + def _get_current_setting(self, device_id: str, setting: str, default: Any) -> Any: + """Get current value for setting.""" + if entry_device_options := self.options.get(OPTION_ENTRY_DEVICE_OPTIONS): + if device_options := entry_device_options.get(device_id): + return device_options.get(setting) + return default + + def _update_device_options(self, user_input: dict[str, Any]) -> None: + """Update the global config with the new options for the current device.""" + options: dict[str, dict[str, Any]] = self.options.setdefault( + OPTION_ENTRY_DEVICE_OPTIONS, {} + ) + + description = self.configurable_devices[self.current_device] + device_options: dict[str, Any] = options.setdefault(description.id, {}) + if description.family == "28": + device_options[OPTION_ENTRY_SENSOR_PRECISION] = user_input[ + OPTION_ENTRY_SENSOR_PRECISION + ] + + self.options.update({OPTION_ENTRY_DEVICE_OPTIONS: options}) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 285b2c51be5..7fce90cc012 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -37,6 +37,20 @@ DEVICE_SUPPORT_OWSERVER = { } DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] +DEVICE_SUPPORT_OPTIONS = ["28"] + +PRECISION_MAPPING_FAMILY_28 = { + "temperature": "Default", + "temperature9": "9 Bits", + "temperature10": "10 Bits", + "temperature11": "11 Bits", + "temperature12": "12 Bits", +} + +OPTION_ENTRY_DEVICE_OPTIONS = "device_options" +OPTION_ENTRY_SENSOR_PRECISION = "precision" +INPUT_ENTRY_CLEAR_OPTIONS = "clear_device_options" +INPUT_ENTRY_DEVICE_SELECTION = "device_selection" MANUFACTURER_MAXIM = "Maxim Integrated" MANUFACTURER_HOBBYBOARDS = "Hobby Boards" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index cac3630d473..759b1f9eccf 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Mapping import copy from dataclasses import dataclass import logging @@ -38,6 +39,9 @@ from .const import ( DEVICE_KEYS_0_3, DEVICE_KEYS_A_B, DOMAIN, + OPTION_ENTRY_DEVICE_OPTIONS, + OPTION_ENTRY_SENSOR_PRECISION, + PRECISION_MAPPING_FAMILY_28, READ_MODE_FLOAT, READ_MODE_INT, ) @@ -54,7 +58,24 @@ from .onewirehub import OneWireHub class OneWireSensorEntityDescription(OneWireEntityDescription, SensorEntityDescription): """Class describing OneWire sensor entities.""" - override_key: str | None = None + override_key: Callable[[str, Mapping[str, Any]], str] | None = None + + +def _get_sensor_precision_family_28(device_id: str, options: Mapping[str, Any]) -> str: + """Get precision form config flow options.""" + precision: str = ( + options.get(OPTION_ENTRY_DEVICE_OPTIONS, {}) + .get(device_id, {}) + .get(OPTION_ENTRY_SENSOR_PRECISION, "temperature") + ) + if precision in PRECISION_MAPPING_FAMILY_28: + return precision + _LOGGER.warning( + "Invalid sensor precision `%s` for device `%s`: reverting to default", + precision, + device_id, + ) + return "temperature" SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( @@ -185,7 +206,17 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), - "28": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), + "28": ( + OneWireSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + override_key=_get_sensor_precision_family_28, + read_mode=READ_MODE_FLOAT, + state_class=SensorStateClass.MEASUREMENT, + ), + ), "30": ( SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION, OneWireSensorEntityDescription( @@ -195,7 +226,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Thermocouple temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - override_key="typeK/temperature", + override_key=lambda d, o: "typeK/temperature", state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( @@ -352,13 +383,15 @@ async def async_setup_entry( """Set up 1-Wire platform.""" onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( - get_entities, onewirehub, config_entry.data + get_entities, onewirehub, config_entry.data, config_entry.options ) async_add_entities(entities, True) def get_entities( - onewirehub: OneWireHub, config: MappingProxyType[str, Any] + onewirehub: OneWireHub, + config: MappingProxyType[str, Any], + options: MappingProxyType[str, Any], ) -> list[SensorEntity]: """Get a list of entities.""" if not onewirehub.devices: @@ -400,9 +433,12 @@ def get_entities( description.device_class = SensorDeviceClass.HUMIDITY description.native_unit_of_measurement = PERCENTAGE description.name = f"Wetness {s_id}" + override_key = None + if description.override_key: + override_key = description.override_key(device_id, options) device_file = os.path.join( os.path.split(device.path)[0], - description.override_key or description.key, + override_key or description.key, ) name = f"{device_id} {description.name}" entities.append( diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 928907b319a..2a6ee1eed6f 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -22,5 +22,32 @@ "title": "Set up 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Select devices to configure" + }, + "step": { + "ack_no_options": { + "data": { }, + "description": "There are no options for the SysBus implementation", + "title": "OneWire SysBus Options" + }, + "device_selection": { + "data": { + "clear_device_options": "Clear all device configurations", + "device_selection": "Select devices to configure" + }, + "description": "Select what configuration steps to process", + "title": "OneWire Device Options" + }, + "configure_device": { + "data": { + "precision": "Sensor Precision" + }, + "description": "Select sensor precision for {sensor_id}", + "title": "OneWire Sensor Precision" + } + } } } diff --git a/homeassistant/components/onewire/translations/en.json b/homeassistant/components/onewire/translations/en.json index ff4d1cb53b9..4b48e216b63 100644 --- a/homeassistant/components/onewire/translations/en.json +++ b/homeassistant/components/onewire/translations/en.json @@ -22,5 +22,32 @@ "title": "Set up 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Select devices to configure" + }, + "step": { + "ack_no_options": { + "data": {}, + "description": "There are no options for the SysBus implementation", + "title": "OneWire SysBus Options" + }, + "configure_device": { + "data": { + "precision": "Sensor Precision" + }, + "description": "Select sensor precision for {sensor_id}", + "title": "OneWire Sensor Precision" + }, + "device_selection": { + "data": { + "clear_device_options": "Clear all device configurations", + "device_selection": "Select devices to configure" + }, + "description": "Select what configuration steps to process", + "title": "OneWire Device Options" + } + } } } \ No newline at end of file diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 189baa3e7da..ab456c7d7df 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -37,7 +37,12 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: CONF_HOST: "1.2.3.4", CONF_PORT: 1234, }, - options={}, + options={ + "device_options": { + "28.222222222222": {"precision": "temperature9"}, + "28.222222222223": {"precision": "temperature5"}, + } + }, entry_id="2", ) config_entry.add_to_hass(hass) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 8d3a8270752..d77b374c7c4 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -435,6 +435,54 @@ MOCK_OWPROXY_DEVICES = { }, ], }, + "28.222222222222": { + # This device has precision options in the config entry + ATTR_INJECT_READS: [ + b"DS18B20", # read device type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "28.222222222222")}, + ATTR_MANUFACTURER: MANUFACTURER_MAXIM, + ATTR_MODEL: "DS18B20", + ATTR_NAME: "28.222222222222", + }, + Platform.SENSOR: [ + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_DEVICE_FILE: "/28.222222222222/temperature9", + ATTR_ENTITY_ID: "sensor.28_222222222222_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "/28.222222222222/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ], + }, + "28.222222222223": { + # This device has an illegal precision option in the config entry + ATTR_INJECT_READS: [ + b"DS18B20", # read device type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "28.222222222223")}, + ATTR_MANUFACTURER: MANUFACTURER_MAXIM, + ATTR_MODEL: "DS18B20", + ATTR_NAME: "28.222222222223", + }, + Platform.SENSOR: [ + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_DEVICE_FILE: "/28.222222222223/temperature", + ATTR_ENTITY_ID: "sensor.28_222222222223_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "/28.222222222223/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ], + }, "29.111111111111": { ATTR_INJECT_READS: [ b"DS2408", # read device type diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index bc164a9b138..ded4811a586 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -54,7 +54,12 @@ async def test_entry_diagnostics( "port": 1234, "type": "OWServer", }, - "options": {}, + "options": { + "device_options": { + "28.222222222222": {"precision": "temperature9"}, + "28.222222222223": {"precision": "temperature5"}, + } + }, "title": "Mock Title", }, "devices": [DEVICE_DETAILS], diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py new file mode 100644 index 00000000000..2edb9da7ffc --- /dev/null +++ b/tests/components/onewire/test_options_flow.py @@ -0,0 +1,237 @@ +"""Tests for 1-Wire config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant.components.onewire.const import ( + CONF_TYPE_SYSBUS, + DOMAIN, + INPUT_ENTRY_CLEAR_OPTIONS, + INPUT_ENTRY_DEVICE_SELECTION, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + + +class FakeDevice: + """Mock Class for mocking DeviceEntry.""" + + name_by_user = "Given Name" + + +class FakeOWHubSysBus: + """Mock Class for mocking onewire hub.""" + + type = CONF_TYPE_SYSBUS + + +async def test_user_owserver_options_clear( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test clearing the options.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that the clear-input action clears the options dict + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + + +async def test_user_owserver_options_empty_selection( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test leaving the selection of devices empty.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that an empty selection does not modify the options + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "device_selection" + assert result["errors"] == {"base": "device_not_selected"} + + +async def test_user_owserver_options_set_single( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test configuring a single device.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Clear config options to certify functionality when starting from scratch + config_entry.options = {} + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that a single selected device to configure comes back as a form with the device to configure + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["description_placeholders"]["sensor_id"] == "28.111111111111" + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature" + ) + + +async def test_user_owserver_options_set_multiple( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test configuring multiple consecutive devices in a row.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Initialize onewire hub + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that first config step comes back with a selection list of all the 28-family devices + with patch( + "homeassistant.helpers.device_registry.DeviceRegistry.async_get_device", + return_value=FakeDevice(), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "Given Name (28.111111111111)": False, + "Given Name (28.222222222222)": False, + "Given Name (28.222222222223)": False, + } + + # Verify that selecting two devices to configure comes back as a + # form with the first device to configure using it's long name as entry + with patch( + "homeassistant.helpers.device_registry.DeviceRegistry.async_get_device", + return_value=FakeDevice(), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + INPUT_ENTRY_DEVICE_SELECTION: [ + "Given Name (28.111111111111)", + "Given Name (28.222222222222)", + ] + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert ( + result["description_placeholders"]["sensor_id"] + == "Given Name (28.222222222222)" + ) + + # Verify that next sensor is coming up for configuration after the first + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"precision": "temperature"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert ( + result["description_placeholders"]["sensor_id"] + == "Given Name (28.111111111111)" + ) + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"precision": "temperature9"}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.222222222222"]["precision"] + == "temperature" + ) + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature9" + ) + + +async def test_user_owserver_options_no_devices( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test that options does not change when no devices are available.""" + # Initialize onewire hub + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that first config step comes back with an empty list of possible devices to choose from + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "No configurable devices found." + + +async def test_user_sysbus_options( + hass: HomeAssistant, + config_entry: ConfigEntry, +): + """Test that SysBus options flow aborts on init.""" + hass.data[DOMAIN] = {config_entry.entry_id: FakeOWHubSysBus()} + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "SysBus setup does not have any config options." From 423c14e2a1b6b5806a5eab8663009aba87af00e5 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 3 Mar 2022 14:42:33 -0500 Subject: [PATCH 0204/1054] Add light entity to SleepIQ (#67363) --- homeassistant/components/sleepiq/__init__.py | 8 +- .../components/sleepiq/binary_sensor.py | 4 +- .../components/sleepiq/coordinator.py | 5 +- homeassistant/components/sleepiq/entity.py | 20 +++-- homeassistant/components/sleepiq/light.py | 59 +++++++++++++++ homeassistant/components/sleepiq/sensor.py | 4 +- homeassistant/components/sleepiq/switch.py | 18 +++-- tests/components/sleepiq/conftest.py | 11 ++- tests/components/sleepiq/test_config_flow.py | 3 + tests/components/sleepiq/test_light.py | 73 +++++++++++++++++++ 10 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/sleepiq/light.py create mode 100644 tests/components/sleepiq/test_light.py diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index a32e61b972c..da3f38fe560 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -26,7 +26,13 @@ from .coordinator import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index 53611edc66b..b176320e671 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED from .coordinator import SleepIQData -from .entity import SleepIQSensor +from .entity import SleepIQSleeperEntity async def async_setup_entry( @@ -29,7 +29,7 @@ async def async_setup_entry( ) -class IsInBedBinarySensor(SleepIQSensor, BinarySensorEntity): +class IsInBedBinarySensor(SleepIQSleeperEntity, BinarySensorEntity): """Implementation of a SleepIQ presence sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index a2394de20b1..ef84b17ba9b 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -34,7 +34,10 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): self.client = client async def _async_update_data(self) -> None: - await self.client.fetch_bed_statuses() + tasks = [self.client.fetch_bed_statuses()] + [ + bed.foundation.update_lights() for bed in self.client.beds.values() + ] + await asyncio.gather(*tasks) class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]): diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 6d0c8784eec..7fa14f2cbe7 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -33,7 +33,7 @@ class SleepIQEntity(Entity): self._attr_device_info = device_from_bed(bed) -class SleepIQSensor(CoordinatorEntity): +class SleepIQBedEntity(CoordinatorEntity): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED @@ -42,17 +42,11 @@ class SleepIQSensor(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, - sleeper: SleepIQSleeper, - name: str, ) -> None: """Initialize the SleepIQ sensor entity.""" super().__init__(coordinator) - self.sleeper = sleeper self.bed = bed self._attr_device_info = device_from_bed(bed) - - self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" - self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" self._async_update_attrs() @callback @@ -67,7 +61,7 @@ class SleepIQSensor(CoordinatorEntity): """Update sensor attributes.""" -class SleepIQBedCoordinator(CoordinatorEntity): +class SleepIQSleeperEntity(SleepIQBedEntity): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED @@ -76,8 +70,12 @@ class SleepIQBedCoordinator(CoordinatorEntity): self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, + sleeper: SleepIQSleeper, + name: str, ) -> None: """Initialize the SleepIQ sensor entity.""" - super().__init__(coordinator) - self.bed = bed - self._attr_device_info = device_from_bed(bed) + self.sleeper = sleeper + super().__init__(coordinator, bed) + + self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" + self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" diff --git a/homeassistant/components/sleepiq/light.py b/homeassistant/components/sleepiq/light.py new file mode 100644 index 00000000000..1017051f94c --- /dev/null +++ b/homeassistant/components/sleepiq/light.py @@ -0,0 +1,59 @@ +"""Support for SleepIQ outlet lights.""" +import logging +from typing import Any + +from asyncsleepiq import SleepIQBed, SleepIQLight + +from homeassistant.components.light import LightEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN +from .coordinator import SleepIQData +from .entity import SleepIQBedEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ bed lights.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepIQLightEntity(data.data_coordinator, bed, light) + for bed in data.client.beds.values() + for light in bed.foundation.lights + ) + + +class SleepIQLightEntity(SleepIQBedEntity, LightEntity): + """Representation of a light.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, light: SleepIQLight + ) -> None: + """Initialize the light.""" + self.light = light + super().__init__(coordinator, bed) + self._attr_name = f"SleepNumber {bed.name} Light {light.outlet_id}" + self._attr_unique_id = f"{bed.id}-light-{light.outlet_id}" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on light.""" + await self.light.turn_on() + self._handle_coordinator_update() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off light.""" + await self.light.turn_off() + self._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update light attributes.""" + self._attr_is_on = self.light.is_on diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 7d50876b1b2..cb9bf91cf8a 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, SLEEP_NUMBER from .coordinator import SleepIQData -from .entity import SleepIQSensor +from .entity import SleepIQSleeperEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( ) -class SleepNumberSensorEntity(SleepIQSensor, SensorEntity): +class SleepNumberSensorEntity(SleepIQSleeperEntity, SensorEntity): """Representation of an SleepIQ Entity with CoordinatorEntity.""" _attr_icon = "mdi:bed" diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py index c8977f0ce73..ebc0f720b43 100644 --- a/homeassistant/components/sleepiq/switch.py +++ b/homeassistant/components/sleepiq/switch.py @@ -7,12 +7,12 @@ from asyncsleepiq import SleepIQBed from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SleepIQData, SleepIQPauseUpdateCoordinator -from .entity import SleepIQBedCoordinator +from .entity import SleepIQBedEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( ) -class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): +class SleepNumberPrivateSwitch(SleepIQBedEntity, SwitchEntity): """Representation of SleepIQ privacy mode.""" def __init__( @@ -39,15 +39,17 @@ class SleepNumberPrivateSwitch(SleepIQBedCoordinator, SwitchEntity): self._attr_name = f"SleepNumber {bed.name} Pause Mode" self._attr_unique_id = f"{bed.id}-pause-mode" - @property - def is_on(self) -> bool: - """Return whether the switch is on or off.""" - return bool(self.bed.paused) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self.bed.set_pause_mode(True) + self._handle_coordinator_update() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self.bed.set_pause_mode(False) + self._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update switch attributes.""" + self._attr_is_on = self.bed.paused diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 9ecc1edd0b6..caf65a99b3d 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import create_autospec, patch -from asyncsleepiq import SleepIQBed, SleepIQSleeper +from asyncsleepiq import SleepIQBed, SleepIQFoundation, SleepIQLight, SleepIQSleeper import pytest from homeassistant.components.sleepiq import DOMAIN @@ -54,6 +54,15 @@ def mock_asyncsleepiq(): sleeper_r.in_bed = False sleeper_r.sleep_number = 80 + bed.foundation = create_autospec(SleepIQFoundation) + light_1 = create_autospec(SleepIQLight) + light_1.outlet_id = 1 + light_1.is_on = False + light_2 = create_autospec(SleepIQLight) + light_2.outlet_id = 2 + light_2.is_on = False + bed.foundation.lights = [light_1, light_2] + yield client diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index bb6742821f6..3101a7ecdfe 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -118,6 +118,9 @@ async def test_reauth_password(hass): with patch( "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", return_value=True, + ), patch( + "homeassistant.components.sleepiq.async_setup_entry", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/sleepiq/test_light.py b/tests/components/sleepiq/test_light.py new file mode 100644 index 00000000000..d7386cceb7b --- /dev/null +++ b/tests/components/sleepiq/test_light.py @@ -0,0 +1,73 @@ +"""The tests for SleepIQ light platform.""" +from homeassistant.components.light import DOMAIN +from homeassistant.components.sleepiq.coordinator import LONGER_UPDATE_INTERVAL +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + setup_platform, +) + + +async def test_setup(hass, mock_asyncsleepiq): + """Test for successfully setting up the SleepIQ platform.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + assert len(entity_registry.entities) == 2 + + entry = entity_registry.async_get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1") + assert entry + assert entry.original_name == f"SleepNumber {BED_NAME} Light 1" + assert entry.unique_id == f"{BED_ID}-light-1" + + entry = entity_registry.async_get(f"light.sleepnumber_{BED_NAME_LOWER}_light_2") + assert entry + assert entry.original_name == f"SleepNumber {BED_NAME} Light 2" + assert entry.unique_id == f"{BED_ID}-light-2" + + +async def test_light_set_states(hass, mock_asyncsleepiq): + """Test light change.""" + await setup_platform(hass, DOMAIN) + + await hass.services.async_call( + DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: f"light.sleepnumber_{BED_NAME_LOWER}_light_1"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].turn_on.assert_called_once() + + await hass.services.async_call( + DOMAIN, + "turn_off", + {ATTR_ENTITY_ID: f"light.sleepnumber_{BED_NAME_LOWER}_light_1"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].turn_off.assert_called_once() + + +async def test_switch_get_states(hass, mock_asyncsleepiq): + """Test light update.""" + await setup_platform(hass, DOMAIN) + + assert ( + hass.states.get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1").state + == STATE_OFF + ) + mock_asyncsleepiq.beds[BED_ID].foundation.lights[0].is_on = True + + async_fire_time_changed(hass, utcnow() + LONGER_UPDATE_INTERVAL) + await hass.async_block_till_done() + + assert ( + hass.states.get(f"light.sleepnumber_{BED_NAME_LOWER}_light_1").state == STATE_ON + ) From 24e0c0b092f5592d75fd2855fe17cc16ce57e316 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 3 Mar 2022 15:27:22 -0500 Subject: [PATCH 0205/1054] Add pressure sensor for SleepIQ (#67574) --- homeassistant/components/sleepiq/const.py | 7 +++- homeassistant/components/sleepiq/sensor.py | 15 +++++--- tests/components/sleepiq/conftest.py | 2 ++ tests/components/sleepiq/test_sensor.py | 42 ++++++++++++++++++++-- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index 63e86270925..c1c28a7b5a8 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -9,7 +9,12 @@ ICON_EMPTY = "mdi:bed-empty" ICON_OCCUPIED = "mdi:bed" IS_IN_BED = "is_in_bed" SLEEP_NUMBER = "sleep_number" -SENSOR_TYPES = {SLEEP_NUMBER: "SleepNumber", IS_IN_BED: "Is In Bed"} +PRESSURE = "pressure" +SENSOR_TYPES = { + SLEEP_NUMBER: "SleepNumber", + IS_IN_BED: "Is In Bed", + PRESSURE: "Pressure", +} LEFT = "left" RIGHT = "right" diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index cb9bf91cf8a..b101aee8a6e 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -9,10 +9,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, SLEEP_NUMBER +from .const import DOMAIN, PRESSURE, SLEEP_NUMBER from .coordinator import SleepIQData from .entity import SleepIQSleeperEntity +SENSORS = [PRESSURE, SLEEP_NUMBER] + async def async_setup_entry( hass: HomeAssistant, @@ -22,13 +24,14 @@ async def async_setup_entry( """Set up the SleepIQ bed sensors.""" data: SleepIQData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - SleepNumberSensorEntity(data.data_coordinator, bed, sleeper) + SleepIQSensorEntity(data.data_coordinator, bed, sleeper, sensor_type) for bed in data.client.beds.values() for sleeper in bed.sleepers + for sensor_type in SENSORS ) -class SleepNumberSensorEntity(SleepIQSleeperEntity, SensorEntity): +class SleepIQSensorEntity(SleepIQSleeperEntity, SensorEntity): """Representation of an SleepIQ Entity with CoordinatorEntity.""" _attr_icon = "mdi:bed" @@ -38,11 +41,13 @@ class SleepNumberSensorEntity(SleepIQSleeperEntity, SensorEntity): coordinator: DataUpdateCoordinator, bed: SleepIQBed, sleeper: SleepIQSleeper, + sensor_type: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, bed, sleeper, SLEEP_NUMBER) + self.sensor_type = sensor_type + super().__init__(coordinator, bed, sleeper, sensor_type) @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" - self._attr_native_value = self.sleeper.sleep_number + self._attr_native_value = getattr(self.sleeper, self.sensor_type) diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index caf65a99b3d..5b51b0f4670 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -48,11 +48,13 @@ def mock_asyncsleepiq(): sleeper_l.name = SLEEPER_L_NAME sleeper_l.in_bed = True sleeper_l.sleep_number = 40 + sleeper_l.pressure = 1000 sleeper_r.side = "R" sleeper_r.name = SLEEPER_R_NAME sleeper_r.in_bed = False sleeper_r.sleep_number = 80 + sleeper_r.pressure = 1400 bed.foundation = create_autospec(SleepIQFoundation) light_1 = create_autospec(SleepIQLight) diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 26ddc9aa485..c2d8648ebd5 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -15,8 +15,8 @@ from tests.components.sleepiq.conftest import ( ) -async def test_sensors(hass, mock_asyncsleepiq): - """Test the SleepIQ binary sensors for a bed with two sides.""" +async def test_sleepnumber_sensors(hass, mock_asyncsleepiq): + """Test the SleepIQ sleepnumber for a bed with two sides.""" entry = await setup_platform(hass, DOMAIN) entity_registry = er.async_get(hass) @@ -51,3 +51,41 @@ async def test_sensors(hass, mock_asyncsleepiq): ) assert entry assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_sleep_number" + + +async def test_pressure_sensors(hass, mock_asyncsleepiq): + """Test the SleepIQ pressure for a bed with two sides.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_pressure" + ) + assert state.state == "1000" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Pressure" + ) + + entry = entity_registry.async_get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_pressure" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_pressure" + + state = hass.states.get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_pressure" + ) + assert state.state == "1400" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Pressure" + ) + + entry = entity_registry.async_get( + f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_pressure" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_pressure" From 9356bf1a8e6df72a498a9b3517e1a8f2a0abefcb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Mar 2022 21:40:15 +0100 Subject: [PATCH 0206/1054] Fix MQTT config flow with advanced parameters (#67556) * Fix MQTT config flow with advanced parameters * Add test --- homeassistant/components/mqtt/__init__.py | 106 +++++++++++-------- homeassistant/components/mqtt/config_flow.py | 29 ++--- homeassistant/components/mqtt/const.py | 7 ++ tests/components/mqtt/test_config_flow.py | 97 ++++++++++++++++- 4 files changed, 177 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 090d9cdfa73..bbe773f141c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -70,11 +70,16 @@ from .const import ( ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, DATA_MQTT_CONFIG, @@ -89,6 +94,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + PROTOCOL_31, PROTOCOL_311, ) from .discovery import LAST_DISCOVERY @@ -113,13 +119,6 @@ SERVICE_DUMP = "dump" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_KEEPALIVE = "keepalive" -CONF_CERTIFICATE = "certificate" -CONF_CLIENT_KEY = "client_key" -CONF_CLIENT_CERT = "client_cert" -CONF_TLS_INSECURE = "tls_insecure" -CONF_TLS_VERSION = "tls_version" - -PROTOCOL_31 = "3.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 @@ -751,6 +750,58 @@ class Subscription: encoding: str | None = attr.ib(default="utf-8") +class MqttClientSetup: + """Helper class to setup the paho mqtt client from config.""" + + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper.""" + + if config[CONF_PROTOCOL] == PROTOCOL_31: + proto = self.mqtt.MQTTv31 + else: + proto = self.mqtt.MQTTv311 + + if (client_id := config.get(CONF_CLIENT_ID)) is None: + # PAHO MQTT relies on the MQTT server to generate random client IDs. + # However, that feature is not mandatory so we generate our own. + client_id = self.mqtt.base62(uuid.uuid4().int, padding=22) + self._client = self.mqtt.Client(client_id, protocol=proto) + + # Enable logging + self._client.enable_logger() + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if username is not None: + self._client.username_pw_set(username, password) + + if (certificate := config.get(CONF_CERTIFICATE)) == "auto": + certificate = certifi.where() + + client_key = config.get(CONF_CLIENT_KEY) + client_cert = config.get(CONF_CLIENT_CERT) + tls_insecure = config.get(CONF_TLS_INSECURE) + if certificate is not None: + self._client.tls_set( + certificate, + certfile=client_cert, + keyfile=client_key, + tls_version=ssl.PROTOCOL_TLS, + ) + + if tls_insecure is not None: + self._client.tls_insecure_set(tls_insecure) + + @property + def client(self) -> mqtt.Client: + """Return the paho MQTT client.""" + return self._client + + class MQTT: """Home Assistant MQTT client.""" @@ -815,46 +866,7 @@ class MQTT: def init_client(self): """Initialize paho client.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - if self.conf[CONF_PROTOCOL] == PROTOCOL_31: - proto: int = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - if (client_id := self.conf.get(CONF_CLIENT_ID)) is None: - # PAHO MQTT relies on the MQTT server to generate random client IDs. - # However, that feature is not mandatory so we generate our own. - client_id = mqtt.base62(uuid.uuid4().int, padding=22) - self._mqttc = mqtt.Client(client_id, protocol=proto) - - # Enable logging - self._mqttc.enable_logger() - - username = self.conf.get(CONF_USERNAME) - password = self.conf.get(CONF_PASSWORD) - if username is not None: - self._mqttc.username_pw_set(username, password) - - if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto": - certificate = certifi.where() - - client_key = self.conf.get(CONF_CLIENT_KEY) - client_cert = self.conf.get(CONF_CLIENT_CERT) - tls_insecure = self.conf.get(CONF_TLS_INSECURE) - if certificate is not None: - self._mqttc.tls_set( - certificate, - certfile=client_cert, - keyfile=client_key, - tls_version=ssl.PROTOCOL_TLS, - ) - - if tls_insecure is not None: - self._mqttc.tls_insecure_set(tls_insecure) - + self._mqttc = MqttClientSetup(self.conf).client self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_message = self._mqtt_on_message diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 3f93e50829a..99e7e9718d0 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult +from . import MqttClientSetup from .const import ( ATTR_PAYLOAD, ATTR_QOS, @@ -62,6 +63,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -102,6 +104,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = self._hassio_discovery can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, data[CONF_HOST], data[CONF_PORT], data.get(CONF_USERNAME), @@ -152,6 +155,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, + self.hass, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -313,25 +317,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) -def try_connection(broker, port, username, password, protocol="3.1"): +def try_connection(hass, broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if protocol == "3.1": - proto = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - client = mqtt.Client(protocol=proto) - if username and password: - client.username_pw_set(username, password) + # Get the config from configuration.yaml + yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) + entry_config = { + CONF_BROKER: broker, + CONF_PORT: port, + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_PROTOCOL: protocol, + } + client = MqttClientSetup({**yaml_config, **entry_config}).client result = queue.Queue(maxsize=1) def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" - result.put(result_code == mqtt.CONNACK_ACCEPTED) + result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED) client.on_connect = on_connect diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index f04348ee002..69865733763 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -22,6 +22,12 @@ CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" CONF_WILL_MESSAGE = "will_message" +CONF_CERTIFICATE = "certificate" +CONF_CLIENT_KEY = "client_key" +CONF_CLIENT_CERT = "client_cert" +CONF_TLS_INSECURE = "tls_insecure" +CONF_TLS_VERSION = "tls_version" + DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" @@ -56,4 +62,5 @@ MQTT_DISCONNECTED = "mqtt_disconnected" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" +PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index d9aab02e821..88c6137bf94 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -3,8 +3,9 @@ from unittest.mock import patch import pytest import voluptuous as vol +import yaml -from homeassistant import config_entries, data_entry_flow +from homeassistant import config as hass_config, config_entries, data_entry_flow from homeassistant.components import mqtt from homeassistant.components.hassio import HassioServiceInfo from homeassistant.core import HomeAssistant @@ -151,7 +152,7 @@ async def test_manual_config_set( "discovery": True, } # Check we tried the connection, with precedence for config entry settings - mock_try_connection.assert_called_once_with("127.0.0.1", 1883, None, None) + mock_try_connection.assert_called_once_with(hass, "127.0.0.1", 1883, None, None) # Check config entry got setup assert len(mock_finish_setup.mock_calls) == 1 @@ -642,3 +643,95 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection): mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PORT: 1234, } + + +async def test_try_connection_with_advanced_parameters( + hass, mock_try_connection_success, tmp_path +): + """Test config flow with advanced parameters from config.""" + # Mock certificate files + certfile = tmp_path / "cert.pem" + certfile.write_text("## mock certificate file ##") + keyfile = tmp_path / "key.pem" + keyfile.write_text("## mock key file ##") + config = { + "certificate": "auto", + "tls_insecure": True, + "client_cert": certfile, + "client_key": keyfile, + } + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({mqtt.DOMAIN: config}) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + config_entry.data = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + mqtt.CONF_DISCOVERY: True, + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/online", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 1, + mqtt.ATTR_RETAIN: True, + }, + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/offline", + mqtt.ATTR_PAYLOAD: "offline", + mqtt.ATTR_QOS: 2, + mqtt.ATTR_RETAIN: False, + }, + } + + # Test default/suggested values from config + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "broker" + defaults = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + } + suggested = { + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + } + for k, v in defaults.items(): + assert get_default(result["data_schema"].schema, k) == v + for k, v in suggested.items(): + assert get_suggested(result["data_schema"].schema, k) == v + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "another-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "us3r", + mqtt.CONF_PASSWORD: "p4ss", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + + # check if the username and password was set from config flow and not from configuration.yaml + assert mock_try_connection_success.username_pw_set.mock_calls[0][1] == ( + "us3r", + "p4ss", + ) + + # check if tls_insecure_set is called + assert mock_try_connection_success.tls_insecure_set.mock_calls[0][1] == (True,) + + # check if the certificate settings were set from configuration.yaml + assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ + "certfile" + ] == str(certfile) + assert mock_try_connection_success.tls_set.mock_calls[0].kwargs[ + "keyfile" + ] == str(keyfile) From a55d20f164f4495cee033f958ef731020893ca4b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 Mar 2022 23:59:48 +0100 Subject: [PATCH 0207/1054] Use cached_property instead of stacked property + cache (#67515) --- homeassistant/components/dlna_dms/dms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 0fa4d77d005..4cb1477778a 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -680,8 +680,7 @@ class DmsDeviceSource: """Make an identifier for BrowseMediaSource.""" return f"{self.source_id}/{action}{object_id}" - @property # type: ignore - @functools.cache + @functools.cached_property def _sort_criteria(self) -> list[str]: """Return criteria to be used for sorting results. From e7ca6b6e3889c006d6fa2e723cf38bb1b3f451c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Mar 2022 15:03:03 -0800 Subject: [PATCH 0208/1054] Highlight in logs it is a custom component when setup fails (#67559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- homeassistant/setup.py | 18 +++++++++++++----- tests/test_setup.py | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 36292989dce..2599b4b3c85 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -149,10 +149,17 @@ async def _async_setup_component( This method is a coroutine. """ + integration: loader.Integration | None = None - def log_error(msg: str, link: str | None = None) -> None: + def log_error(msg: str) -> None: """Log helper.""" - _LOGGER.error("Setup failed for %s: %s", domain, msg) + if integration is None: + custom = "" + link = None + else: + custom = "" if integration.is_built_in else "custom integration " + link = integration.documentation + _LOGGER.error("Setup failed for %s%s: %s", custom, domain, msg) async_notify_setup_error(hass, domain, link) try: @@ -174,7 +181,7 @@ async def _async_setup_component( try: await async_process_deps_reqs(hass, config, integration) except HomeAssistantError as err: - log_error(str(err), integration.documentation) + log_error(str(err)) return False # Some integrations fail on import because they call functions incorrectly. @@ -182,7 +189,7 @@ async def _async_setup_component( try: component = integration.get_component() except ImportError as err: - log_error(f"Unable to import component: {err}", integration.documentation) + log_error(f"Unable to import component: {err}") return False processed_config = await conf_util.async_process_component_config( @@ -190,7 +197,7 @@ async def _async_setup_component( ) if processed_config is None: - log_error("Invalid config.", integration.documentation) + log_error("Invalid config.") return False start = timer() @@ -287,6 +294,7 @@ async def async_prepare_setup_platform( def log_error(msg: str) -> None: """Log helper.""" + _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) async_notify_setup_error(hass, platform_path) diff --git a/tests/test_setup.py b/tests/test_setup.py index f71ba01410b..04924344c2b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, setup from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, @@ -621,6 +622,22 @@ async def test_integration_disabled(hass, caplog): assert disabled_reason in caplog.text +async def test_integration_logs_is_custom(hass, caplog): + """Test we highlight it's a custom component when errors happen.""" + mock_integration( + hass, + MockModule("test_component1"), + built_in=False, + ) + with patch( + "homeassistant.setup.async_process_deps_reqs", + side_effect=HomeAssistantError("Boom"), + ): + result = await setup.async_setup_component(hass, "test_component1", {}) + assert not result + assert "Setup failed for custom integration test_component1: Boom" in caplog.text + + async def test_async_get_loaded_integrations(hass): """Test we can enumerate loaded integations.""" hass.config.components.add("notbase") From 6d2302b703d87f03a12ab4c6440606cf3cd0e378 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Mar 2022 13:03:46 -1000 Subject: [PATCH 0209/1054] Add guards for HomeKit version/names that break apple watches (#67585) --- .../components/homekit/accessories.py | 8 +++-- homeassistant/components/homekit/util.py | 31 +++++++++++++++---- tests/components/homekit/test_accessories.py | 17 +++++++--- tests/components/homekit/test_type_sensors.py | 2 +- tests/components/homekit/test_util.py | 15 +++++++-- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d348b4c1f42..4129c3225b7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -274,7 +274,7 @@ class HomeAccessory(Accessory): if self.config.get(ATTR_SW_VERSION) is not None: sw_version = format_version(self.config[ATTR_SW_VERSION]) if sw_version is None: - sw_version = __version__ + sw_version = format_version(__version__) hw_version = None if self.config.get(ATTR_HW_VERSION) is not None: hw_version = format_version(self.config[ATTR_HW_VERSION]) @@ -289,7 +289,9 @@ class HomeAccessory(Accessory): serv_info = self.get_service(SERV_ACCESSORY_INFO) char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION) serv_info.add_characteristic(char) - serv_info.configure_char(CHAR_HARDWARE_REVISION, value=hw_version) + serv_info.configure_char( + CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH] + ) self.iid_manager.assign(char) char.broker = self @@ -532,7 +534,7 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(driver, name) self.set_info_service( - firmware_revision=__version__, + firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER, diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8c64b9b0443..7fa4ffa8bf6 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -100,6 +100,7 @@ _LOGGER = logging.getLogger(__name__) NUMBERS_ONLY_RE = re.compile(r"[^\d.]+") VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?") +MAX_VERSION_PART = 2**32 - 1 MAX_PORT = 65535 @@ -363,7 +364,15 @@ def convert_to_float(state): return None -def cleanup_name_for_homekit(name: str | None) -> str | None: +def coerce_int(state: str) -> int: + """Return int.""" + try: + return int(state) + except (ValueError, TypeError): + return 0 + + +def cleanup_name_for_homekit(name: str | None) -> str: """Ensure the name of the device will not crash homekit.""" # # This is not a security measure. @@ -371,7 +380,7 @@ def cleanup_name_for_homekit(name: str | None) -> str | None: # UNICODE_EMOJI is also not allowed but that # likely isn't a problem if name is None: - return None + return "None" # None crashes apple watches return name.translate(HOMEKIT_CHAR_TRANSLATIONS)[:MAX_NAME_LENGTH] @@ -420,13 +429,23 @@ def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): ) +def _format_version_part(version_part: str) -> str: + return str(max(0, min(MAX_VERSION_PART, coerce_int(version_part)))) + + def format_version(version): """Extract the version string in a format homekit can consume.""" - split_ver = str(version).replace("-", ".") + split_ver = str(version).replace("-", ".").replace(" ", ".") num_only = NUMBERS_ONLY_RE.sub("", split_ver) - if match := VERSION_RE.search(num_only): - return match.group(0) - return None + if (match := VERSION_RE.search(num_only)) is None: + return None + value = ".".join(map(_format_version_part, match.group(0).split("."))) + return None if _is_zero_but_true(value) else value + + +def _is_zero_but_true(value): + """Zero but true values can crash apple watches.""" + return convert_to_float(value) == 0 def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 103ee9ea2da..704bb368d64 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -42,7 +42,6 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - __version__, __version__ as hass_version, ) from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS @@ -166,7 +165,9 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) hass.states.async_set(entity_id, "on") await hass.async_block_till_done() @@ -216,7 +217,9 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert isinstance(acc.to_HAP(), dict) @@ -244,7 +247,9 @@ async def test_accessory_with_hardware_revision(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == hass_version + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" assert isinstance(acc.to_HAP(), dict) @@ -687,7 +692,9 @@ def test_home_bridge(hk_driver): serv = bridge.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME - assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert hass_version.startswith( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value + ) assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == BRIDGE_SERIAL_NUMBER diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index d864a90fe61..9b6d1c9cee2 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -399,4 +399,4 @@ async def test_empty_name(hass, hk_driver): assert acc.category == 10 # Sensor assert acc.char_humidity.value == 20 - assert acc.display_name is None + assert acc.display_name == "None" diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0432fb27426..3dd30af2056 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -30,6 +30,7 @@ from homeassistant.components.homekit.util import ( async_port_is_available, async_show_setup_message, cleanup_name_for_homekit, + coerce_int, convert_to_float, density_to_air_quality, format_version, @@ -349,13 +350,23 @@ async def test_format_version(): assert format_version("undefined-undefined-1.6.8") == "1.6.8" assert format_version("56.0-76060") == "56.0.76060" assert format_version(3.6) == "3.6" - assert format_version("AK001-ZJ100") == "001.100" + assert format_version("AK001-ZJ100") == "1.100" assert format_version("HF-LPB100-") == "100" - assert format_version("AK001-ZJ2149") == "001.2149" + assert format_version("AK001-ZJ2149") == "1.2149" + assert format_version("13216407885") == "4294967295" # max value + assert format_version("000132 16407885") == "132.16407885" assert format_version("0.1") == "0.1" + assert format_version("0") is None assert format_version("unknown") is None +async def test_coerce_int(): + """Test coerce_int method.""" + assert coerce_int("1") == 1 + assert coerce_int("") == 0 + assert coerce_int(0) == 0 + + async def test_accessory_friendly_name(): """Test we provide a helpful friendly name.""" From 81a509e69e78748f382eea7924ab3deae9308c9a Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Thu, 3 Mar 2022 23:05:13 +0000 Subject: [PATCH 0210/1054] Fix data type for growatt lastdataupdate (#67511) (#67582) Co-authored-by: Paulus Schoutsen --- homeassistant/components/growatt_server/sensor.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 71c1c69e08a..67095492de7 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -221,12 +221,9 @@ class GrowattData: # Create datetime from the latest entry date_now = dt.now().date() last_updated_time = dt.parse_time(str(sorted_keys[-1])) - combined_timestamp = datetime.datetime.combine( + mix_detail["lastdataupdate"] = datetime.datetime.combine( date_now, last_updated_time ) - # Convert datetime to UTC - combined_timestamp_utc = dt.as_utc(combined_timestamp) - mix_detail["lastdataupdate"] = combined_timestamp_utc.isoformat() # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined # imported from grid value that is the combination of charging AND load consumption From 02391663c199fb4bd4e031041788ece809faa20a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Mar 2022 00:12:33 +0100 Subject: [PATCH 0211/1054] Add config flow to Moon (#67444) --- .strict-typing | 3 +- CODEOWNERS | 4 +- homeassistant/components/moon/__init__.py | 17 ++++- homeassistant/components/moon/config_flow.py | 37 ++++++++++ homeassistant/components/moon/const.py | 9 +++ homeassistant/components/moon/manifest.json | 5 +- homeassistant/components/moon/sensor.py | 33 ++++++--- homeassistant/components/moon/strings.json | 13 ++++ .../components/moon/translations/en.json | 13 ++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 13 +++- tests/components/moon/conftest.py | 27 +++++++ tests/components/moon/test_config_flow.py | 72 +++++++++++++++++++ tests/components/moon/test_init.py | 55 ++++++++++++++ tests/components/moon/test_sensor.py | 44 +++++------- 15 files changed, 304 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/moon/config_flow.py create mode 100644 homeassistant/components/moon/const.py create mode 100644 homeassistant/components/moon/strings.json create mode 100644 homeassistant/components/moon/translations/en.json create mode 100644 tests/components/moon/conftest.py create mode 100644 tests/components/moon/test_config_flow.py create mode 100644 tests/components/moon/test_init.py diff --git a/.strict-typing b/.strict-typing index a5a2127eb68..1213a8b73b7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -127,10 +127,11 @@ homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* +homeassistant.components.media_source.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* -homeassistant.components.media_source.* +homeassistant.components.moon.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.nanoleaf.* diff --git a/CODEOWNERS b/CODEOWNERS index 82930f875f4..1c725643535 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -616,8 +616,8 @@ homeassistant/components/moehlenhoff_alpha2/* @j-a-n tests/components/moehlenhoff_alpha2/* @j-a-n homeassistant/components/monoprice/* @etsinko @OnFreund tests/components/monoprice/* @etsinko @OnFreund -homeassistant/components/moon/* @fabaff -tests/components/moon/* @fabaff +homeassistant/components/moon/* @fabaff @frenck +tests/components/moon/* @fabaff @frenck homeassistant/components/motion_blinds/* @starkillerOG tests/components/motion_blinds/* @starkillerOG homeassistant/components/motioneye/* @dermotduffy diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index f7049608dda..0b36ba59198 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -1 +1,16 @@ -"""The moon component.""" +"""The Moon integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py new file mode 100644 index 00000000000..1f7c5715f9e --- /dev/null +++ b/homeassistant/components/moon/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Moon integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class MoonConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Moon.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={}, + ) + + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/moon/const.py b/homeassistant/components/moon/const.py new file mode 100644 index 00000000000..87c525758b9 --- /dev/null +++ b/homeassistant/components/moon/const.py @@ -0,0 +1,9 @@ +"""Constants for the Moon integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "moon" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Moon" diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 19fb952f59f..0402a87cf1a 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -2,7 +2,8 @@ "domain": "moon", "name": "Moon", "documentation": "https://www.home-assistant.io/integrations/moon", - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@frenck"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index cd10d9168d9..c5078771af8 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -DEFAULT_NAME = "Moon" +from .const import DEFAULT_NAME, DOMAIN STATE_FIRST_QUARTER = "first_quarter" STATE_FULL_MOON = "full_moon" @@ -49,23 +50,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Moon sensor.""" - name: str = config[CONF_NAME] - - async_add_entities([MoonSensor(name)], True) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -class MoonSensor(SensorEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config_entry.""" + async_add_entities([MoonSensorEntity(entry)], True) + + +class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_device_class = "moon__phase" - def __init__(self, name: str) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the moon sensor.""" - self._attr_name = name + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id - async def async_update(self): + async def async_update(self) -> None: """Get the time and updates the states.""" - today = dt_util.as_local(dt_util.utcnow()).date() + today = dt_util.now().date() state = moon.phase(today) if state < 0.5 or state > 27.5: diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json new file mode 100644 index 00000000000..d5bb204a740 --- /dev/null +++ b/homeassistant/components/moon/strings.json @@ -0,0 +1,13 @@ +{ + "title": "Moon", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/moon/translations/en.json b/homeassistant/components/moon/translations/en.json new file mode 100644 index 00000000000..0f324f7b64b --- /dev/null +++ b/homeassistant/components/moon/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Moon" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5530c73ed50..7a36cfebc7b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -203,6 +203,7 @@ FLOWS = [ "modern_forms", "moehlenhoff_alpha2", "monoprice", + "moon", "motion_blinds", "motioneye", "mqtt", diff --git a/mypy.ini b/mypy.ini index 045e9a4eee8..7a927a3c6a7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1198,6 +1198,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.media_source.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1231,7 +1242,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.media_source.*] +[mypy-homeassistant.components.moon.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/tests/components/moon/conftest.py b/tests/components/moon/conftest.py new file mode 100644 index 00000000000..5c8157f257d --- /dev/null +++ b/tests/components/moon/conftest.py @@ -0,0 +1,27 @@ +"""Fixtures for Moon integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.moon.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Moon", + domain=DOMAIN, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.moon.async_setup_entry", return_value=True): + yield diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py new file mode 100644 index 00000000000..4bfb61166aa --- /dev/null +++ b/tests/components/moon/test_config_flow.py @@ -0,0 +1,72 @@ +"""Tests for the Moon config flow.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.moon.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Moon" + assert result2.get("data") == {} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_NAME: "My Moon"}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "My Moon" + assert result.get("data") == {} diff --git a/tests/components/moon/test_init.py b/tests/components/moon/test_init.py new file mode 100644 index 00000000000..f0f7e593545 --- /dev/null +++ b/tests/components/moon/test_init.py @@ -0,0 +1,55 @@ +"""Tests for the Moon integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.moon.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Moon configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_import_config( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test Moon being set up from config via import.""" + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_NAME: "My Moon", + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + + entry = config_entries[0] + assert entry.title == "My Moon" + assert entry.unique_id is None + assert entry.data == {} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 066620b1051..bb9e5dcc157 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -5,10 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) from homeassistant.components.moon.sensor import ( MOON_ICONS, STATE_FIRST_QUARTER, @@ -20,9 +16,11 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_CRESCENT, STATE_WAXING_GIBBOUS, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry @pytest.mark.parametrize( @@ -39,33 +37,27 @@ from homeassistant.setup import async_setup_component ], ) async def test_moon_day( - hass: HomeAssistant, moon_value: float, native_value: str, icon: str + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + moon_value: float, + native_value: str, + icon: str, ) -> None: """Test the Moon sensor.""" - config = {"sensor": {"platform": "moon"}} - - await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.states.get("sensor.moon") + mock_config_entry.add_to_hass(hass) with patch( "homeassistant.components.moon.sensor.moon.phase", return_value=moon_value ): - await async_update_entity(hass, "sensor.moon") + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("sensor.moon") + assert state assert state.state == native_value - assert state.attributes["icon"] == icon + assert state.attributes[ATTR_ICON] == icon - -async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: - """Run an update action for an entity.""" - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - await hass.async_block_till_done() + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.moon") + assert entry + assert entry.unique_id == mock_config_entry.entry_id From a8842d3636deac24e3b637c4b724fb3fc121659a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 3 Mar 2022 15:22:11 -0800 Subject: [PATCH 0212/1054] Bump httplib2 to 0.20.4 (#67552) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/remember_the_milk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index bd4355ac4e3..53b85ef7aa2 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": [ "google-api-python-client==2.38.0", - "httplib2==0.19.0", + "httplib2==0.20.4", "oauth2client==4.1.3" ], "codeowners": ["@allenporter"], diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 40bfbe1683c..2dd01f7fe6d 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -2,7 +2,7 @@ "domain": "remember_the_milk", "name": "Remember The Milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", - "requirements": ["RtmAPI==0.7.2", "httplib2==0.19.0"], + "requirements": ["RtmAPI==0.7.2", "httplib2==0.20.4"], "dependencies": ["configurator"], "codeowners": [], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 8a017abebab..219716edebb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ horimote==0.4.1 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.19.0 +httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.4.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac07e9037ad..e53bed06e4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -569,7 +569,7 @@ homepluscontrol==0.0.5 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.19.0 +httplib2==0.20.4 # homeassistant.components.huawei_lte huawei-lte-api==1.4.18 From 69f08ec524980268dbef8c44576ce2cd83b32f61 Mon Sep 17 00:00:00 2001 From: Emory Penney Date: Thu, 3 Mar 2022 15:22:36 -0800 Subject: [PATCH 0213/1054] Bump pyobihai (#67571) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index f908ad16179..96f803cebdd 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -2,7 +2,7 @@ "domain": "obihai", "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": ["pyobihai==1.3.1"], + "requirements": ["pyobihai==1.3.2"], "codeowners": ["@dshokouhi"], "iot_class": "local_polling", "loggers": ["pyobihai"] diff --git a/requirements_all.txt b/requirements_all.txt index 219716edebb..3707317d306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1672,7 +1672,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.3.1 +pyobihai==1.3.2 # homeassistant.components.octoprint pyoctoprintapi==0.1.8 From 20073797012be0141608195a02f35fd376d0ab45 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 00:27:39 +0100 Subject: [PATCH 0214/1054] Restore state of template binary sensor with on or off delay (#67546) --- .../components/template/binary_sensor.py | 15 +++- .../components/template/test_binary_sensor.py | 73 ++++++++++++++++++- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 4c080c736d0..f416454f388 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -30,6 +30,9 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import TemplateError @@ -38,6 +41,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import TriggerUpdateCoordinator @@ -186,7 +190,7 @@ async def async_setup_platform( ) -class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): +class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): """A virtual binary sensor that triggers from another sensor.""" def __init__( @@ -212,7 +216,14 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): self._delay_off_raw = config.get(CONF_DELAY_OFF) async def async_added_to_hass(self): - """Register callbacks.""" + """Restore state and register callbacks.""" + if ( + (self._delay_on_raw is not None or self._delay_off_raw is not None) + and (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + ): + self._state = last_state.state == STATE_ON + self.add_template_attribute("_state", self._template, None, self._update_state) if self._delay_on_raw is not None: diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 8f76caa2cb9..c3773e8eb13 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -15,11 +15,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Context, CoreState +from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + mock_restore_cache, +) ON = "on" OFF = "off" @@ -914,6 +919,70 @@ async def test_availability_icon_picture(hass, start_ha, entity_id): } +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "binary_sensor": { + "name": "test", + "state": "{{ states.sensor.test_state.state == 'on' }}", + }, + }, + }, + ], +) +@pytest.mark.parametrize( + "extra_config, restored_state, initial_state", + [ + ({}, ON, OFF), + ({}, OFF, OFF), + ({}, STATE_UNAVAILABLE, OFF), + ({}, STATE_UNKNOWN, OFF), + ({"delay_off": 5}, ON, ON), + ({"delay_off": 5}, OFF, OFF), + ({"delay_off": 5}, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_off": 5}, STATE_UNKNOWN, STATE_UNKNOWN), + ({"delay_on": 5}, ON, ON), + ({"delay_on": 5}, OFF, OFF), + ({"delay_on": 5}, STATE_UNAVAILABLE, STATE_UNKNOWN), + ({"delay_on": 5}, STATE_UNKNOWN, STATE_UNKNOWN), + ], +) +async def test_restore_state( + hass, count, domain, config, extra_config, restored_state, initial_state +): + """Test restoring template binary sensor.""" + + fake_state = State( + "binary_sensor.test", + restored_state, + {}, + ) + mock_restore_cache(hass, (fake_state,)) + config = dict(config) + config["template"]["binary_sensor"].update(**extra_config) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + + context = Context() + hass.bus.async_fire("test_event", {"beer": 2}, context=context) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == initial_state + + @pytest.mark.parametrize("count,domain", [(2, "template")]) @pytest.mark.parametrize( "config", From b75993d8095bfcfce02eb3a7a7ce9d1d9430116a Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 00:41:50 +0100 Subject: [PATCH 0215/1054] Check if UPnP is enabled on Fritz device (#67512) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/fritz/__init__.py | 6 ++++++ homeassistant/components/fritz/common.py | 10 ++++++++++ homeassistant/components/fritz/config_flow.py | 7 +++++++ homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/strings.json | 1 + .../components/fritz/translations/en.json | 14 ++------------ 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index a0e0413366b..0b334ff616a 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -33,6 +33,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except FRITZ_EXCEPTIONS as ex: raise ConfigEntryNotReady from ex + if ( + "X_AVM-DE_UPnP1" in avm_wrapper.connection.services + and not (await avm_wrapper.async_get_upnp_configuration())["NewEnable"] + ): + raise ConfigEntryAuthFailed("Missing UPnP configuration") + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = avm_wrapper diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index b2a429bfa3c..2fc28433e56 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -630,6 +630,11 @@ class AvmWrapper(FritzBoxTools): ) return {} + async def async_get_upnp_configuration(self) -> dict[str, Any]: + """Call X_AVM-DE_UPnP service.""" + + return await self.hass.async_add_executor_job(self.get_upnp_configuration) + async def async_get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" @@ -698,6 +703,11 @@ class AvmWrapper(FritzBoxTools): partial(self.set_allow_wan_access, ip_address, turn_on) ) + def get_upnp_configuration(self) -> dict[str, Any]: + """Call X_AVM-DE_UPnP service.""" + + return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo") + def get_ontel_num_deflections(self) -> dict[str, Any]: """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 0844d725522..046f00ba3a9 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -29,6 +29,7 @@ from .const import ( ERROR_AUTH_INVALID, ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, + ERROR_UPNP_NOT_CONFIGURED, ) _LOGGER = logging.getLogger(__name__) @@ -79,6 +80,12 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") return ERROR_UNKNOWN + if ( + "X_AVM-DE_UPnP1" in self.avm_wrapper.connection.services + and not (await self.avm_wrapper.async_get_upnp_configuration())["NewEnable"] + ): + return ERROR_UPNP_NOT_CONFIGURED + return None async def async_check_configured_entry(self) -> ConfigEntry | None: diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index f33cf463996..f739ccf6858 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -46,6 +46,7 @@ DEFAULT_USERNAME = "" ERROR_AUTH_INVALID = "invalid_auth" ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_UPNP_NOT_CONFIGURED = "upnp_not_configured" ERROR_UNKNOWN = "unknown_error" FRITZ_SERVICES = "fritz_services" diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 450566f101b..a65b2900f66 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -36,6 +36,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "upnp_not_configured": "Missing UPnP settings on device.", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 3314f47278c..4ec647bac88 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,8 +9,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", - "connection_error": "Failed to connect", - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "upnp_not_configured": "Missing UPnP settings on device." }, "flow_title": "{name}", "step": { @@ -30,16 +30,6 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, - "start_config": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - }, - "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", - "title": "Setup FRITZ!Box Tools - mandatory" - }, "user": { "data": { "host": "Host", From 2242b023d383c5e8debfa813abe0871de4d30b06 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 4 Mar 2022 00:21:35 +0000 Subject: [PATCH 0216/1054] [ci skip] Translation update --- .../binary_sensor/translations/sv.json | 3 ++ .../components/fritz/translations/en.json | 11 +++++++ .../components/moon/translations/it.json | 13 ++++++++ .../components/onewire/translations/ca.json | 26 ++++++++++++++++ .../components/onewire/translations/en.json | 1 - .../components/onewire/translations/it.json | 30 +++++++++++++++++++ .../onewire/translations/pt-BR.json | 30 +++++++++++++++++++ .../components/sensibo/translations/ca.json | 9 +++++- .../components/sensibo/translations/de.json | 9 +++++- .../components/sensibo/translations/el.json | 9 +++++- .../components/sensibo/translations/et.json | 9 +++++- .../components/sensibo/translations/it.json | 9 +++++- .../components/sensibo/translations/ja.json | 9 +++++- .../components/sensibo/translations/no.json | 9 +++++- .../sensibo/translations/pt-BR.json | 9 +++++- .../components/sensibo/translations/ru.json | 9 +++++- .../sensibo/translations/zh-Hant.json | 9 +++++- .../components/update/translations/ca.json | 3 ++ .../components/update/translations/de.json | 3 ++ .../components/update/translations/el.json | 3 ++ .../components/update/translations/it.json | 3 ++ .../components/update/translations/pt-BR.json | 3 ++ 22 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/moon/translations/it.json create mode 100644 homeassistant/components/update/translations/ca.json create mode 100644 homeassistant/components/update/translations/de.json create mode 100644 homeassistant/components/update/translations/el.json create mode 100644 homeassistant/components/update/translations/it.json create mode 100644 homeassistant/components/update/translations/pt-BR.json diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index c651d895fda..c6685403e24 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -89,6 +89,9 @@ "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" } }, + "device_class": { + "motion": "r\u00f6relse" + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 4ec647bac88..f44c41f2ef9 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -9,6 +9,7 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", + "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication", "upnp_not_configured": "Missing UPnP settings on device." }, @@ -30,6 +31,16 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools - mandatory" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/moon/translations/it.json b/homeassistant/components/moon/translations/it.json new file mode 100644 index 00000000000..af891532233 --- /dev/null +++ b/homeassistant/components/moon/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "title": "Luna" +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ca.json b/homeassistant/components/onewire/translations/ca.json index 73c6cb0991e..b50e3abc1f6 100644 --- a/homeassistant/components/onewire/translations/ca.json +++ b/homeassistant/components/onewire/translations/ca.json @@ -22,5 +22,31 @@ "title": "Configuraci\u00f3 d'1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Selecciona els dispositius a configurar" + }, + "step": { + "ack_no_options": { + "description": "No hi ha opcions per a la implementaci\u00f3 de SysBus", + "title": "Opcions SysBus de OneWire" + }, + "configure_device": { + "data": { + "precision": "Precisi\u00f3 del sensor" + }, + "description": "Selecciona la precisi\u00f3 del sensor {sensor_id}", + "title": "Precisi\u00f3 del sensor OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Esborra totes les configuracions de dispositiu", + "device_selection": "Selecciona els dispositius a configurar" + }, + "description": "Seleccioneu els passos de configuraci\u00f3 a processar", + "title": "Opcions de dispositiu OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/en.json b/homeassistant/components/onewire/translations/en.json index 4b48e216b63..df61b436f65 100644 --- a/homeassistant/components/onewire/translations/en.json +++ b/homeassistant/components/onewire/translations/en.json @@ -29,7 +29,6 @@ }, "step": { "ack_no_options": { - "data": {}, "description": "There are no options for the SysBus implementation", "title": "OneWire SysBus Options" }, diff --git a/homeassistant/components/onewire/translations/it.json b/homeassistant/components/onewire/translations/it.json index 118cee0de4a..ec2edddc275 100644 --- a/homeassistant/components/onewire/translations/it.json +++ b/homeassistant/components/onewire/translations/it.json @@ -22,5 +22,35 @@ "title": "Configurazione 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Seleziona i dispositivi da configurare" + }, + "step": { + "ack_no_options": { + "data": { + "one": "Vuoto", + "other": "Vuoti" + }, + "description": "Non ci sono opzioni per l'implementazione SysBus", + "title": "Opzioni SysBus OneWire" + }, + "configure_device": { + "data": { + "precision": "Precisione del sensore" + }, + "description": "Seleziona la precisione del sensore per {sensor_id}", + "title": "Precisione del sensore OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Cancella tutte le configurazioni del dispositivo", + "device_selection": "Seleziona i dispositivi da configurare" + }, + "description": "Seleziona quali passaggi di configurazione elaborare", + "title": "Opzioni del dispositivo OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json index 401452bcaf5..1fa61c78a41 100644 --- a/homeassistant/components/onewire/translations/pt-BR.json +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -22,5 +22,35 @@ "title": "Configurar 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Selecione os dispositivos para configurar" + }, + "step": { + "ack_no_options": { + "data": { + "one": "um", + "other": "outros" + }, + "description": "N\u00e3o h\u00e1 op\u00e7\u00f5es para a implementa\u00e7\u00e3o do SysBus", + "title": "Op\u00e7\u00f5es de OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "Precis\u00e3o do Sensor" + }, + "description": "Selecione a precis\u00e3o do sensor para {sensor_id}", + "title": "Precis\u00e3o do Sensor OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Limpar todas as configura\u00e7\u00f5es do dispositivo", + "device_selection": "Selecione os dispositivos para configurar" + }, + "description": "Selecione as etapas de configura\u00e7\u00e3o a serem processadas", + "title": "Op\u00e7\u00f5es de dispositivo OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index 72893fb79e5..f062af9e519 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "El compte ja est\u00e0 configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "incorrect_api_key": "Clau API inv\u00e0lida per al compte seleccionat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "no_devices": "No s'ha descobert cap dispositiu", "no_username": "No s'ha pogut obtenir el nom d'usuari" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + } + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index ffcdbc66036..9e3b02c726c 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "incorrect_api_key": "Ung\u00fcltiger API-Schl\u00fcssel f\u00fcr ausgew\u00e4hltes Konto", "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_devices": "Keine Ger\u00e4te gefunden", "no_username": "Benutzername konnte nicht ermittelt werden" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 1e92956c738..baa2545a7e0 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "incorrect_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "no_devices": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2", "no_username": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index 276ad37bc8c..de5a158c1ad 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", + "incorrect_api_key": "Valitud konto API-v\u00f5ti on kehtetu", "invalid_auth": "Tuvastamine nurjus", "no_devices": "\u00dchtegi seadet ei leitud", "no_username": "Kasutajanime ei \u00f5nnestunud hankida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + } + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index d67ac8a9ad3..c10adf2cf38 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", + "incorrect_api_key": "Chiave API non valida per l'account selezionato", "invalid_auth": "Autenticazione non valida", "no_devices": "Nessun dispositivo rilevato", "no_username": "Impossibile ottenere il nome utente" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + } + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index 1913198827c..859722fbe73 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "incorrect_api_key": "\u9078\u629e\u3057\u305f\u30a2\u30ab\u30a6\u30f3\u30c8\u306eAPI\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "no_devices": "\u30c7\u30d0\u30a4\u30b9\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "no_username": "\u30e6\u30fc\u30b6\u30fc\u540d\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, "user": { "data": { "api_key": "API\u30ad\u30fc", diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 117ec5951ce..9d3098ccd9e 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", + "incorrect_api_key": "Ugyldig API-n\u00f8kkel for valgt konto", "invalid_auth": "Ugyldig godkjenning", "no_devices": "Ingen enheter oppdaget", "no_username": "Kunne ikke hente brukernavn" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index e4b8509ede7..40719894b2a 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 foi configurada" + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", + "incorrect_api_key": "Chave de API inv\u00e1lida para a conta selecionada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "no_devices": "Nenhum dispositivo descoberto", "no_username": "N\u00e3o foi poss\u00edvel obter o nome de usu\u00e1rio" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave de API" + } + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index bf8aa9660e7..96574822758 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "incorrect_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", "no_username": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 9977208f646..39cc7dc9662 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "incorrect_api_key": "\u6240\u9078\u64c7\u5e33\u865f\u4e4b API \u91d1\u9470\u7121\u6548\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_devices": "\u672a\u767c\u73fe\u4efb\u4f55\u88dd\u7f6e", "no_username": "\u7121\u6cd5\u53d6\u5f97\u4f7f\u7528\u8005\u540d\u7a31" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + } + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/update/translations/ca.json b/homeassistant/components/update/translations/ca.json new file mode 100644 index 00000000000..396e79c14c0 --- /dev/null +++ b/homeassistant/components/update/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Actualitza" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/de.json b/homeassistant/components/update/translations/de.json new file mode 100644 index 00000000000..18562d81eaf --- /dev/null +++ b/homeassistant/components/update/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Aktualisieren" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/el.json b/homeassistant/components/update/translations/el.json new file mode 100644 index 00000000000..d687d342ec3 --- /dev/null +++ b/homeassistant/components/update/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/it.json b/homeassistant/components/update/translations/it.json new file mode 100644 index 00000000000..539f0bb4294 --- /dev/null +++ b/homeassistant/components/update/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Aggiornamento" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/pt-BR.json b/homeassistant/components/update/translations/pt-BR.json new file mode 100644 index 00000000000..4003445e2c3 --- /dev/null +++ b/homeassistant/components/update/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Atualiza\u00e7\u00e3o" +} \ No newline at end of file From d0bc5410ccb284de985abc98af9da1104ac56eab Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 4 Mar 2022 05:55:01 +0100 Subject: [PATCH 0217/1054] Remove use of deprecated xiaomi_miio classes (#67590) --- homeassistant/components/xiaomi_miio/__init__.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e1b9a044201..1bfc9410d4d 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -14,7 +14,6 @@ from miio import ( AirHumidifierMiot, AirHumidifierMjjsq, AirPurifier, - AirPurifierMB4, AirPurifierMiot, CleaningDetails, CleaningSummary, @@ -23,10 +22,8 @@ from miio import ( DNDStatus, Fan, Fan1C, + FanMiot, FanP5, - FanP9, - FanP10, - FanP11, FanZA5, RoborockVacuum, Timer, @@ -52,7 +49,6 @@ from .const import ( KEY_DEVICE, MODEL_AIRFRESH_A1, MODEL_AIRFRESH_T2017, - MODEL_AIRPURIFIER_3C, MODEL_FAN_1C, MODEL_FAN_P5, MODEL_FAN_P9, @@ -112,10 +108,10 @@ AIR_MONITOR_PLATFORMS = [Platform.AIR_QUALITY, Platform.SENSOR] MODEL_TO_CLASS_MAP = { MODEL_FAN_1C: Fan1C, - MODEL_FAN_P10: FanP10, - MODEL_FAN_P11: FanP11, + MODEL_FAN_P9: FanMiot, + MODEL_FAN_P10: FanMiot, + MODEL_FAN_P11: FanMiot, MODEL_FAN_P5: FanP5, - MODEL_FAN_P9: FanP9, MODEL_FAN_ZA5: FanZA5, } @@ -315,8 +311,6 @@ async def async_create_miio_device_and_coordinator( device = AirHumidifier(host, token, model=model) migrate = True # Airpurifiers and Airfresh - elif model == MODEL_AIRPURIFIER_3C: - device = AirPurifierMB4(host, token) elif model in MODELS_PURIFIER_MIOT: device = AirPurifierMiot(host, token) elif model.startswith("zhimi.airpurifier."): From 79e9eb1b94f0d1b79a57eaa1d6aee4d4dc54ef41 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Mar 2022 23:08:29 -0600 Subject: [PATCH 0218/1054] Suppress roku power off timeout errors (#67414) --- homeassistant/components/roku/__init__.py | 32 --------------- homeassistant/components/roku/helpers.py | 40 +++++++++++++++++++ homeassistant/components/roku/manifest.json | 2 +- homeassistant/components/roku/media_player.py | 29 +++++++------- homeassistant/components/roku/remote.py | 8 ++-- homeassistant/components/roku/select.py | 5 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roku/test_media_player.py | 9 ++++- 9 files changed, 70 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index e6e31f08713..f24d08909b8 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,14 +1,6 @@ """Support for Roku.""" from __future__ import annotations -from collections.abc import Awaitable, Callable, Coroutine -from functools import wraps -import logging -from typing import Any, TypeVar - -from rokuecp import RokuConnectionError, RokuError -from typing_extensions import Concatenate, ParamSpec - from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant @@ -16,7 +8,6 @@ from homeassistant.helpers import config_validation as cv from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator -from .entity import RokuEntity CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -27,10 +18,6 @@ PLATFORMS = [ Platform.SELECT, Platform.SENSOR, ] -_LOGGER = logging.getLogger(__name__) - -_T = TypeVar("_T", bound="RokuEntity") -_P = ParamSpec("_P") async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -53,22 +40,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -def roku_exception_handler( - func: Callable[Concatenate[_T, _P], Awaitable[None]] # type: ignore[misc] -) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc] - """Decorate Roku calls to handle Roku exceptions.""" - - @wraps(func) - async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: - try: - await func(self, *args, **kwargs) - except RokuConnectionError as error: - if self.available: - _LOGGER.error("Error communicating with API: %s", error) - except RokuError as error: - if self.available: - _LOGGER.error("Invalid response from API: %s", error) - - return wrapper diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py index 7f507a9fe52..26fdb53c935 100644 --- a/homeassistant/components/roku/helpers.py +++ b/homeassistant/components/roku/helpers.py @@ -1,6 +1,21 @@ """Helpers for Roku.""" from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +import logging +from typing import Any, TypeVar + +from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError +from typing_extensions import Concatenate, ParamSpec + +from .entity import RokuEntity + +_LOGGER = logging.getLogger(__name__) + +_T = TypeVar("_T", bound=RokuEntity) +_P = ParamSpec("_P") + def format_channel_name(channel_number: str, channel_name: str | None = None) -> str: """Format a Roku Channel name.""" @@ -8,3 +23,28 @@ def format_channel_name(channel_number: str, channel_name: str | None = None) -> return f"{channel_name} ({channel_number})" return channel_number + + +def roku_exception_handler(ignore_timeout: bool = False) -> Callable[..., Callable]: + """Decorate Roku calls to handle Roku exceptions.""" + + def decorator( + func: Callable[Concatenate[_T, _P], Awaitable[None]], # type: ignore[misc] + ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: # type: ignore[misc] + @wraps(func) + async def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + await func(self, *args, **kwargs) + except RokuConnectionTimeoutError as error: + if not ignore_timeout and self.available: + _LOGGER.error("Error communicating with API: %s", error) + except RokuConnectionError as error: + if self.available: + _LOGGER.error("Error communicating with API: %s", error) + except RokuError as error: + if self.available: + _LOGGER.error("Invalid response from API: %s", error) + + return wrapper + + return decorator diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 4918e7742be..433ce6b29d1 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.14.1"], + "requirements": ["rokuecp==0.15.0"], "homekit": { "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 9cf17d890a4..8dd76f0b9cb 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -51,7 +51,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import roku_exception_handler from .browse_media import async_browse_media from .const import ( ATTR_ARTIST_NAME, @@ -65,7 +64,7 @@ from .const import ( ) from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity -from .helpers import format_channel_name +from .helpers import format_channel_name, roku_exception_handler _LOGGER = logging.getLogger(__name__) @@ -289,7 +288,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): app.name for app in self.coordinator.data.apps if app.name is not None ) - @roku_exception_handler + @roku_exception_handler() async def search(self, keyword: str) -> None: """Emulate opening the search screen and entering the search keyword.""" await self.coordinator.roku.search(keyword) @@ -321,68 +320,68 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): media_content_type, ) - @roku_exception_handler + @roku_exception_handler() async def async_turn_on(self) -> None: """Turn on the Roku.""" await self.coordinator.roku.remote("poweron") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler(ignore_timeout=True) async def async_turn_off(self) -> None: """Turn off the Roku.""" await self.coordinator.roku.remote("poweroff") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_pause(self) -> None: """Send pause command.""" if self.state not in (STATE_STANDBY, STATE_PAUSED): await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_play(self) -> None: """Send play command.""" if self.state not in (STATE_STANDBY, STATE_PLAYING): await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_play_pause(self) -> None: """Send play/pause command.""" if self.state != STATE_STANDBY: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.coordinator.roku.remote("reverse") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_media_next_track(self) -> None: """Send next track command.""" await self.coordinator.roku.remote("forward") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self.coordinator.roku.remote("volume_mute") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_volume_up(self) -> None: """Volume up media player.""" await self.coordinator.roku.remote("volume_up") - @roku_exception_handler + @roku_exception_handler() async def async_volume_down(self) -> None: """Volume down media player.""" await self.coordinator.roku.remote("volume_down") - @roku_exception_handler + @roku_exception_handler() async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: @@ -487,7 +486,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_select_source(self, source: str) -> None: """Select input source.""" if source == "Home": diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 9a0cd6f51e3..6d1312c0b03 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -9,10 +9,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import roku_exception_handler from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity +from .helpers import roku_exception_handler async def async_setup_entry( @@ -44,19 +44,19 @@ class RokuRemote(RokuEntity, RemoteEntity): """Return true if device is on.""" return not self.coordinator.data.state.standby - @roku_exception_handler + @roku_exception_handler() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.coordinator.roku.remote("poweron") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler(ignore_timeout=True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.coordinator.roku.remote("poweroff") await self.coordinator.async_request_refresh() - @roku_exception_handler + @roku_exception_handler() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] diff --git a/homeassistant/components/roku/select.py b/homeassistant/components/roku/select.py index 9120a4fe9ce..e11748114d1 100644 --- a/homeassistant/components/roku/select.py +++ b/homeassistant/components/roku/select.py @@ -12,11 +12,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import roku_exception_handler from .const import DOMAIN from .coordinator import RokuDataUpdateCoordinator from .entity import RokuEntity -from .helpers import format_channel_name +from .helpers import format_channel_name, roku_exception_handler @dataclass @@ -163,7 +162,7 @@ class RokuSelectEntity(RokuEntity, SelectEntity): """Return a set of selectable options.""" return self.entity_description.options_fn(self.coordinator.data) - @roku_exception_handler + @roku_exception_handler() async def async_select_option(self, option: str) -> None: """Set the option.""" await self.entity_description.set_fn( diff --git a/requirements_all.txt b/requirements_all.txt index 3707317d306..a5b88f0e02b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2063,7 +2063,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.14.1 +rokuecp==0.15.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e53bed06e4d..c216e921e61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1309,7 +1309,7 @@ rflink==0.0.62 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.14.1 +rokuecp==0.15.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 050814e3817..21fd2e861b6 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import MagicMock, patch import pytest -from rokuecp import RokuError +from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( @@ -164,10 +164,15 @@ async def test_tv_setup( assert device_entry.suggested_area == "Living room" +@pytest.mark.parametrize( + "error", + [RokuConnectionTimeoutError, RokuConnectionError, RokuError], +) async def test_availability( hass: HomeAssistant, mock_roku: MagicMock, mock_config_entry: MockConfigEntry, + error: RokuError, ) -> None: """Test entity availability.""" now = dt_util.utcnow() @@ -179,7 +184,7 @@ async def test_availability( await hass.async_block_till_done() with patch("homeassistant.util.dt.utcnow", return_value=future): - mock_roku.update.side_effect = RokuError + mock_roku.update.side_effect = error async_fire_time_changed(hass, future) await hass.async_block_till_done() assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE From 4e52f26ed17b18cc23898f80b9b8df1c093a82e5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 3 Mar 2022 22:48:55 -0800 Subject: [PATCH 0219/1054] Fix flaky google calendar test (#67600) --- tests/components/google/test_init.py | 57 +++++++++++++++++++++------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index ae803e9fd3b..4baa577851f 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -16,6 +16,7 @@ from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.dt import utcnow from .conftest import CALENDAR_ID, ApiResult, YieldFixture @@ -378,23 +379,12 @@ async def test_add_event( datetime.timedelta(days=7), datetime.timedelta(days=8), ), - ( - { - "start_date": datetime.date.today().isoformat(), - "end_date": ( - datetime.date.today() + datetime.timedelta(days=2) - ).isoformat(), - }, - datetime.timedelta(days=0), - datetime.timedelta(days=2), - ), ], - ids=["in_days", "in_weeks", "explit_date"], + ids=["in_days", "in_weeks"], ) -async def test_add_event_date_ranges( +async def test_add_event_date_in_x( hass: HomeAssistant, mock_token_read: None, - calendars_config: list[dict[str, Any]], component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_calendar: dict[str, Any], @@ -433,3 +423,44 @@ async def test_add_event_date_ranges( "end": {"date": end_date.date().isoformat()}, }, ) + + +async def test_add_event_date_range( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_calendar: dict[str, Any], + mock_insert_event: Mock, +) -> None: + """Test service call that sets a date range.""" + + assert await component_setup() + + now = dt_util.utcnow() + today = now.date() + end_date = today + datetime.timedelta(days=2) + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_EVENT, + { + "calendar_id": CALENDAR_ID, + "summary": "Summary", + "description": "Description", + "start_date": today.isoformat(), + "end_date": end_date.isoformat(), + }, + blocking=True, + ) + mock_insert_event.assert_called() + + assert mock_insert_event.mock_calls[0] == call( + calendarId=CALENDAR_ID, + body={ + "summary": "Summary", + "description": "Description", + "start": {"date": today.isoformat()}, + "end": {"date": end_date.isoformat()}, + }, + ) From 57ffc65af238dd504f04fe00c1abe04a29a9fcee Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 3 Mar 2022 23:12:24 -0800 Subject: [PATCH 0220/1054] Improve google calendar test quality and share setup (#67441) * Improve google calendar test quality and share setup Improve google calendar test quality by exercising different combinations of cases and new coverage. The existing test cases do achieve very high coverage, however some of the subtle interactions between the components are not as well exercised, which is needed when we start changing how the internal code is structured moving to async or to config entries. Ipmrovement include: - Exercising additional cases around different types of configuration parameters that control how calendars are tracked or not tracked. The tracking can be configured in google_calendars.yaml, or by defaults set in configuration.yaml for new calendars. - Share even more test setup, used when exercising the above different scenarios - Add new test cases for event creation. - Improve test readability by making more clear the differences between tests exercising yaml and API test calendars. The data types are now more clearly separated in the two cases, as well as the entity names created in the two cases. * Undo some diffs for readability * Improve pydoc readability * Improve pydoc readability * Incorporate improvements from Martin * Make test parameters a State instance * Update test naming to be more correct * Fix flake8 errors --- tests/components/google/conftest.py | 128 ++++++++++- tests/components/google/test_calendar.py | 196 ++++++---------- tests/components/google/test_init.py | 275 ++++++++++++++++------- 3 files changed, 385 insertions(+), 214 deletions(-) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index f224f3f2f31..5e1411c76c8 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,25 +1,44 @@ """Test configuration and mocks for the google integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable import datetime from typing import Any, Generator, TypeVar -from unittest.mock import Mock, patch +from unittest.mock import Mock, mock_open, patch from googleapiclient import discovery as google_discovery from oauth2client.client import Credentials, OAuth2Credentials import pytest +import yaml +from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE + ApiResult = Callable[[dict[str, Any]], None] +ComponentSetup = Callable[[], Awaitable[bool]] T = TypeVar("T") YieldFixture = Generator[T, None, None] CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" -TEST_CALENDAR = { + +# Entities can either be created based on data directly from the API, or from +# the yaml config that overrides the entity name and other settings. A test +# can use a fixture to exercise either case. +TEST_API_ENTITY = "calendar.we_are_we_are_a_test_calendar" +TEST_API_ENTITY_NAME = "We are, we are, a... Test Calendar" +# Name of the entity when using yaml configuration overrides +TEST_YAML_ENTITY = "calendar.backyard_light" +TEST_YAML_ENTITY_NAME = "Backyard Light" + +# A calendar object returned from the API +TEST_API_CALENDAR = { "id": CALENDAR_ID, "etag": '"3584134138943410"', "timeZone": "UTC", @@ -32,14 +51,63 @@ TEST_CALENDAR = { "summary": "We are, we are, a... Test Calendar", "colorId": "8", "defaultReminders": [], - "track": True, } @pytest.fixture -def test_calendar(): - """Return a test calendar.""" - return TEST_CALENDAR +def test_api_calendar(): + """Return a test calendar object used in API responses.""" + return TEST_API_CALENDAR + + +@pytest.fixture +def calendars_config_track() -> bool: + """Fixture that determines the 'track' setting in yaml config.""" + return True + + +@pytest.fixture +def calendars_config_ignore_availability() -> bool: + """Fixture that determines the 'ignore_availability' setting in yaml config.""" + return None + + +@pytest.fixture +def calendars_config_entity( + calendars_config_track: bool, calendars_config_ignore_availability: bool | None +) -> dict[str, Any]: + """Fixture that creates an entity within the yaml configuration.""" + entity = { + "device_id": "backyard_light", + "name": "Backyard Light", + "search": "#Backyard", + "track": calendars_config_track, + } + if calendars_config_ignore_availability is not None: + entity["ignore_availability"] = calendars_config_ignore_availability + return entity + + +@pytest.fixture +def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, Any]]: + """Fixture that specifies the calendar yaml configuration.""" + return [ + { + "cal_id": CALENDAR_ID, + "entities": [calendars_config_entity], + } + ] + + +@pytest.fixture +async def mock_calendars_yaml( + hass: HomeAssistant, + calendars_config: list[dict[str, Any]], +) -> None: + """Fixture that prepares the google_calendars.yaml mocks.""" + mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) + with patch("homeassistant.components.google.open", mocked_open_function): + yield class FakeStorage: @@ -156,3 +224,49 @@ def mock_insert_event( insert_mock = Mock() calendar_resource.return_value.events.return_value.insert = insert_mock return insert_mock + + +@pytest.fixture(autouse=True) +def set_time_zone(hass): + """Set the time zone for the tests.""" + # Set our timezone to CST/Regina so we can check calculations + # This keeps UTC-6 all year round + hass.config.time_zone = "CST" + dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina")) + yield + dt_util.set_default_time_zone(ORIG_TIMEZONE) + + +@pytest.fixture +def google_config_track_new() -> None: + """Fixture for tests to set the 'track_new' configuration.yaml setting.""" + return None + + +@pytest.fixture +def google_config(google_config_track_new: bool | None) -> dict[str, Any]: + """Fixture for overriding component config.""" + google_config = {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-secret"} + if google_config_track_new is not None: + google_config[CONF_TRACK_NEW] = google_config_track_new + return google_config + + +@pytest.fixture +async def config(google_config: dict[str, Any]) -> dict[str, Any]: + """Fixture for overriding component config.""" + return {DOMAIN: google_config} + + +@pytest.fixture +async def component_setup( + hass: HomeAssistant, config: dict[str, Any] +) -> ComponentSetup: + """Fixture for setting up the integration.""" + + async def _setup_func() -> bool: + result = await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + return result + + return _setup_func diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index dda30a1d83e..76ab20ff41b 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -2,38 +2,22 @@ from __future__ import annotations +import datetime from http import HTTPStatus from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import Mock import httplib2 import pytest -from homeassistant.components.google import ( - CONF_CAL_ID, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_DEVICE_ID, - CONF_ENTITIES, - CONF_IGNORE_AVAILABILITY, - CONF_NAME, - CONF_TRACK, - DEVICE_SCHEMA, - SERVICE_SCAN_CALENDARS, -) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from homeassistant.setup import async_setup_component -from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from .conftest import TEST_CALENDAR +from .conftest import TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME -from tests.common import async_mock_service - -GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} -TEST_ENTITY = "calendar.we_are_we_are_a_test_calendar" -TEST_ENTITY_NAME = "We are, we are, a... Test Calendar" +TEST_ENTITY = TEST_YAML_ENTITY +TEST_ENTITY_NAME = TEST_YAML_ENTITY_NAME TEST_EVENT = { "summary": "Test All Day Event", @@ -65,48 +49,10 @@ TEST_EVENT = { } -def get_calendar_info(calendar): - """Convert data from Google into DEVICE_SCHEMA.""" - calendar_info = DEVICE_SCHEMA( - { - CONF_CAL_ID: calendar["id"], - CONF_ENTITIES: [ - { - CONF_TRACK: calendar["track"], - CONF_NAME: calendar["summary"], - CONF_DEVICE_ID: slugify(calendar["summary"]), - CONF_IGNORE_AVAILABILITY: calendar.get("ignore_availability", True), - } - ], - } - ) - return calendar_info - - @pytest.fixture(autouse=True) -def mock_google_setup(hass, test_calendar, mock_token_read): - """Mock the google set up functions.""" - hass.loop.run_until_complete(async_setup_component(hass, "group", {"group": {}})) - calendar = get_calendar_info(test_calendar) - calendars = {calendar[CONF_CAL_ID]: calendar} - patch_google_load = patch( - "homeassistant.components.google.load_config", return_value=calendars - ) - patch_google_services = patch("homeassistant.components.google.setup_services") - async_mock_service(hass, "google", SERVICE_SCAN_CALENDARS) - - with patch_google_load, patch_google_services: - yield - - -@pytest.fixture(autouse=True) -def set_time_zone(): - """Set the time zone for the tests.""" - # Set our timezone to CST/Regina so we can check calculations - # This keeps UTC-6 all year round - dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina")) - yield - dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) +def mock_test_setup(mock_calendars_yaml, mock_token_read): + """Fixture that pulls in the default fixtures for tests in this file.""" + return def upcoming() -> dict[str, Any]: @@ -114,22 +60,24 @@ def upcoming() -> dict[str, Any]: now = dt_util.now() return { "start": {"dateTime": now.isoformat()}, - "end": {"dateTime": (now + dt_util.dt.timedelta(minutes=5)).isoformat()}, + "end": {"dateTime": (now + datetime.timedelta(minutes=5)).isoformat()}, } def upcoming_event_url() -> str: """Return a calendar API to return events created by upcoming().""" now = dt_util.now() - start = (now - dt_util.dt.timedelta(minutes=60)).isoformat() - end = (now + dt_util.dt.timedelta(minutes=60)).isoformat() + start = (now - datetime.timedelta(minutes=60)).isoformat() + end = (now + datetime.timedelta(minutes=60)).isoformat() return f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}" -async def test_all_day_event(hass, mock_events_list_items, mock_token_read): +async def test_all_day_event( + hass, mock_events_list_items, mock_token_read, component_setup +): """Test that we can create an event trigger on device.""" - week_from_today = dt_util.dt.date.today() + dt_util.dt.timedelta(days=7) - end_event = week_from_today + dt_util.dt.timedelta(days=1) + week_from_today = dt_util.now().date() + datetime.timedelta(days=7) + end_event = week_from_today + datetime.timedelta(days=1) event = { **TEST_EVENT, "start": {"date": week_from_today.isoformat()}, @@ -137,8 +85,7 @@ async def test_all_day_event(hass, mock_events_list_items, mock_token_read): } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -155,10 +102,10 @@ async def test_all_day_event(hass, mock_events_list_items, mock_token_read): } -async def test_future_event(hass, mock_events_list_items): +async def test_future_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) - end_event = one_hour_from_now + dt_util.dt.timedelta(minutes=60) + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) + end_event = one_hour_from_now + datetime.timedelta(minutes=60) event = { **TEST_EVENT, "start": {"dateTime": one_hour_from_now.isoformat()}, @@ -166,8 +113,7 @@ async def test_future_event(hass, mock_events_list_items): } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -184,10 +130,10 @@ async def test_future_event(hass, mock_events_list_items): } -async def test_in_progress_event(hass, mock_events_list_items): +async def test_in_progress_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() - dt_util.dt.timedelta(minutes=30) - end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) + middle_of_event = dt_util.now() - datetime.timedelta(minutes=30) + end_event = middle_of_event + datetime.timedelta(minutes=60) event = { **TEST_EVENT, "start": {"dateTime": middle_of_event.isoformat()}, @@ -195,8 +141,7 @@ async def test_in_progress_event(hass, mock_events_list_items): } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -213,10 +158,10 @@ async def test_in_progress_event(hass, mock_events_list_items): } -async def test_offset_in_progress_event(hass, mock_events_list_items): +async def test_offset_in_progress_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - middle_of_event = dt_util.now() + dt_util.dt.timedelta(minutes=14) - end_event = middle_of_event + dt_util.dt.timedelta(minutes=60) + middle_of_event = dt_util.now() + datetime.timedelta(minutes=14) + end_event = middle_of_event + datetime.timedelta(minutes=60) event_summary = "Test Event in Progress" event = { **TEST_EVENT, @@ -226,8 +171,7 @@ async def test_offset_in_progress_event(hass, mock_events_list_items): } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -244,11 +188,12 @@ async def test_offset_in_progress_event(hass, mock_events_list_items): } -@pytest.mark.skip -async def test_all_day_offset_in_progress_event(hass, mock_events_list_items): +async def test_all_day_offset_in_progress_event( + hass, mock_events_list_items, component_setup +): """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=1) - end_event = tomorrow + dt_util.dt.timedelta(days=1) + tomorrow = dt_util.now().date() + datetime.timedelta(days=1) + end_event = tomorrow + datetime.timedelta(days=1) event_summary = "Test All Day Event Offset In Progress" event = { **TEST_EVENT, @@ -258,8 +203,7 @@ async def test_all_day_offset_in_progress_event(hass, mock_events_list_items): } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -276,22 +220,22 @@ async def test_all_day_offset_in_progress_event(hass, mock_events_list_items): } -async def test_all_day_offset_event(hass, mock_events_list_items): +async def test_all_day_offset_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" - tomorrow = dt_util.dt.date.today() + dt_util.dt.timedelta(days=2) - end_event = tomorrow + dt_util.dt.timedelta(days=1) - offset_hours = 1 + dt_util.now().hour + now = dt_util.now() + day_after_tomorrow = now.date() + datetime.timedelta(days=2) + end_event = day_after_tomorrow + datetime.timedelta(days=1) + offset_hours = 1 + now.hour event_summary = "Test All Day Event Offset" event = { **TEST_EVENT, - "start": {"date": tomorrow.isoformat()}, + "start": {"date": day_after_tomorrow.isoformat()}, "end": {"date": end_event.isoformat()}, "summary": f"{event_summary} !!-{offset_hours}:0", } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME @@ -301,30 +245,28 @@ async def test_all_day_offset_event(hass, mock_events_list_items): "message": event_summary, "all_day": True, "offset_reached": False, - "start_time": tomorrow.strftime(DATE_STR_FORMAT), + "start_time": day_after_tomorrow.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT), "location": event["location"], "description": event["description"], } -async def test_update_error(hass, calendar_resource): +async def test_update_error(hass, calendar_resource, component_setup): """Test that the calendar handles a server error.""" calendar_resource.return_value.get = Mock( side_effect=httplib2.ServerNotFoundError("unit test") ) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" -async def test_calendars_api(hass, hass_client): +async def test_calendars_api(hass, hass_client, component_setup): """Test the Rest API returns the calendar.""" - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() response = await client.get("/api/calendars") @@ -338,12 +280,13 @@ async def test_calendars_api(hass, hass_client): ] -async def test_http_event_api_failure(hass, hass_client, calendar_resource): +async def test_http_event_api_failure( + hass, hass_client, calendar_resource, component_setup +): """Test the Rest API response during a calendar failure.""" calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() response = await client.get(upcoming_event_url()) @@ -353,15 +296,16 @@ async def test_http_event_api_failure(hass, hass_client, calendar_resource): assert events == [] -async def test_http_api_event(hass, hass_client, mock_events_list_items): +async def test_http_api_event( + hass, hass_client, mock_events_list_items, component_setup +): """Test querying the API and fetching events from the server.""" event = { **TEST_EVENT, **upcoming(), } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() response = await client.get(upcoming_event_url()) @@ -372,22 +316,27 @@ async def test_http_api_event(hass, hass_client, mock_events_list_items): assert events[0]["summary"] == event["summary"] -def create_ignore_avail_calendar() -> dict[str, Any]: - """Create a calendar with ignore_availability set.""" - calendar = TEST_CALENDAR.copy() - calendar["ignore_availability"] = False - return calendar - - @pytest.mark.parametrize( - "test_calendar,transparency,expect_visible_event", + "calendars_config_ignore_availability,transparency,expect_visible_event", [ - (create_ignore_avail_calendar(), "opaque", True), - (create_ignore_avail_calendar(), "transparent", False), + # Look at visibility to determine if entity is created + (False, "opaque", True), + (False, "transparent", False), + # Ignoring availability and always show the entity + (True, "opaque", True), + (True, "transparency", True), + # Default to ignore availability + (None, "opaque", True), + (None, "transparency", True), ], ) async def test_opaque_event( - hass, hass_client, mock_events_list_items, transparency, expect_visible_event + hass, + hass_client, + mock_events_list_items, + component_setup, + transparency, + expect_visible_event, ): """Test querying the API and fetching events from the server.""" event = { @@ -396,8 +345,7 @@ async def test_opaque_event( "transparency": transparency, } mock_events_list_items([event]) - assert await async_setup_component(hass, "google", {"google": GOOGLE_CONFIG}) - await hass.async_block_till_done() + assert await component_setup() client = await hass_client() response = await client.get(upcoming_event_url()) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 4baa577851f..a0766c8256e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,8 +1,10 @@ """The tests for the Google Calendar component.""" +from __future__ import annotations + from collections.abc import Awaitable, Callable import datetime from typing import Any -from unittest.mock import Mock, call, mock_open, patch +from unittest.mock import Mock, call, patch from oauth2client.client import ( FlowExchangeError, @@ -10,21 +12,26 @@ from oauth2client.client import ( OAuth2DeviceCodeError, ) import pytest -import yaml from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_OFF -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant, State from homeassistant.util.dt import utcnow -from .conftest import CALENDAR_ID, ApiResult, YieldFixture +from .conftest import ( + CALENDAR_ID, + TEST_API_ENTITY, + TEST_API_ENTITY_NAME, + TEST_YAML_ENTITY, + TEST_YAML_ENTITY_NAME, + ApiResult, + ComponentSetup, + YieldFixture, +) from tests.common import async_fire_time_changed # Typing helpers -ComponentSetup = Callable[[], Awaitable[bool]] HassApi = Callable[[], Awaitable[dict[str, Any]]] CODE_CHECK_INTERVAL = 1 @@ -59,35 +66,6 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: yield mock -@pytest.fixture -async def calendars_config() -> list[dict[str, Any]]: - """Fixture for tests to override default calendar configuration.""" - return [ - { - "cal_id": CALENDAR_ID, - "entities": [ - { - "device_id": "backyard_light", - "name": "Backyard Light", - "search": "#Backyard", - "track": True, - } - ], - } - ] - - -@pytest.fixture -async def mock_calendars_yaml( - hass: HomeAssistant, - calendars_config: list[dict[str, Any]], -) -> None: - """Fixture that prepares the calendars.yaml file.""" - mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) - with patch("homeassistant.components.google.open", mocked_open_function): - yield - - @pytest.fixture async def mock_notification() -> YieldFixture[Mock]: """Fixture for capturing persistent notifications.""" @@ -95,26 +73,6 @@ async def mock_notification() -> YieldFixture[Mock]: yield mock -@pytest.fixture -async def config() -> dict[str, Any]: - """Fixture for overriding component config.""" - return {DOMAIN: {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-ecret"}} - - -@pytest.fixture -async def component_setup( - hass: HomeAssistant, config: dict[str, Any] -) -> ComponentSetup: - """Fixture for setting up the integration.""" - - async def _setup_func() -> bool: - result = await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - return result - - return _setup_func - - async def fire_alarm(hass, point_in_time): """Fire an alarm and wait for callbacks to run.""" with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): @@ -133,7 +91,17 @@ async def test_setup_config_empty( mock_notification.assert_not_called() - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) + + +def assert_state(actual: State | None, expected: State | None) -> None: + """Assert that the two states are equal.""" + if actual is None: + assert actual == expected + return + assert actual.entity_id == expected.entity_id + assert actual.state == expected.state + assert actual.attributes == expected.attributes async def test_init_success( @@ -151,9 +119,9 @@ async def test_init_success( now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - state = hass.states.get("calendar.backyard_light") + state = hass.states.get(TEST_YAML_ENTITY) assert state - assert state.name == "Backyard Light" + assert state.name == TEST_YAML_ENTITY_NAME assert state.state == STATE_OFF mock_notification.assert_called() @@ -174,7 +142,7 @@ async def test_code_error( ): assert await component_setup() - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) mock_notification.assert_called() assert "Error: Test Failure" in mock_notification.call_args[0][1] @@ -194,7 +162,7 @@ async def test_expired_after_exchange( now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) mock_notification.assert_called() assert ( @@ -220,7 +188,7 @@ async def test_exchange_error( now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) mock_notification.assert_called() assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1] @@ -236,9 +204,9 @@ async def test_existing_token( """Test setup with an existing token file.""" assert await component_setup() - state = hass.states.get("calendar.backyard_light") + state = hass.states.get(TEST_YAML_ENTITY) assert state - assert state.name == "Backyard Light" + assert state.name == TEST_YAML_ENTITY_NAME assert state.state == STATE_OFF mock_notification.assert_not_called() @@ -265,9 +233,9 @@ async def test_existing_token_missing_scope( await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) assert len(mock_exchange.mock_calls) == 1 - state = hass.states.get("calendar.backyard_light") + state = hass.states.get(TEST_YAML_ENTITY) assert state - assert state.name == "Backyard Light" + assert state.name == TEST_YAML_ENTITY_NAME assert state.state == STATE_OFF # No notifications on success @@ -287,7 +255,7 @@ async def test_calendar_yaml_missing_required_fields( """Test setup with a missing schema fields, ignores the error and continues.""" assert await component_setup() - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) mock_notification.assert_not_called() @@ -306,38 +274,133 @@ async def test_invalid_calendar_yaml( # Integration fails to setup assert not await component_setup() - assert not hass.states.get("calendar.backyard_light") + assert not hass.states.get(TEST_YAML_ENTITY) mock_notification.assert_not_called() -async def test_found_calendar_from_api( +@pytest.mark.parametrize( + "google_config_track_new,calendars_config,expected_state", + [ + ( + None, + [], + State( + TEST_API_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_API_ENTITY_NAME, + }, + ), + ), + ( + True, + [], + State( + TEST_API_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_API_ENTITY_NAME, + }, + ), + ), + (False, [], None), + ], + ids=["default", "True", "False"], +) +async def test_track_new( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], + mock_calendars_yaml: None, + expected_state: State, +) -> None: + """Test behavior of configuration.yaml settings for tracking new calendars not in the config.""" + + mock_calendars_list({"items": [test_api_calendar]}) + assert await component_setup() + + # The calendar does not + state = hass.states.get(TEST_API_ENTITY) + assert_state(state, expected_state) + + +@pytest.mark.parametrize("calendars_config", [[]]) +async def test_found_calendar_from_api( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], ) -> None: """Test finding a calendar from the API.""" - mock_calendars_list({"items": [test_calendar]}) + mock_calendars_list({"items": [test_api_calendar]}) + assert await component_setup() - mocked_open_function = mock_open(read_data=yaml.dump([])) - with patch("homeassistant.components.google.open", mocked_open_function): - assert await component_setup() - - state = hass.states.get("calendar.we_are_we_are_a_test_calendar") + # The calendar does not + state = hass.states.get(TEST_API_ENTITY) assert state - assert state.name == "We are, we are, a... Test Calendar" + assert state.name == TEST_API_ENTITY_NAME assert state.state == STATE_OFF + # No yaml config loaded that overwrites the entity name + assert not hass.states.get(TEST_YAML_ENTITY) + + +@pytest.mark.parametrize( + "calendars_config_track,expected_state", + [ + ( + True, + State( + TEST_YAML_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_YAML_ENTITY_NAME, + }, + ), + ), + (False, None), + ], +) +async def test_calendar_config_track_new( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + calendars_config_track: bool, + expected_state: State, +) -> None: + """Test calendar config that overrides whether or not a calendar is tracked.""" + + mock_calendars_list({"items": [test_api_calendar]}) + assert await component_setup() + + state = hass.states.get(TEST_YAML_ENTITY) + assert_state(state, expected_state) + if calendars_config_track: + assert state + assert state.name == TEST_YAML_ENTITY_NAME + assert state.state == STATE_OFF + else: + assert not state + async def test_add_event( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], mock_insert_event: Mock, ) -> None: """Test service call that adds an event.""" @@ -387,7 +450,7 @@ async def test_add_event_date_in_x( mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], + test_api_calendar: dict[str, Any], mock_insert_event: Mock, date_fields: dict[str, Any], start_timedelta: datetime.timedelta, @@ -425,19 +488,18 @@ async def test_add_event_date_in_x( ) -async def test_add_event_date_range( +async def test_add_event_date( hass: HomeAssistant, mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, - test_calendar: dict[str, Any], mock_insert_event: Mock, ) -> None: """Test service call that sets a date range.""" assert await component_setup() - now = dt_util.utcnow() + now = utcnow() today = now.date() end_date = today + datetime.timedelta(days=2) @@ -464,3 +526,50 @@ async def test_add_event_date_range( "end": {"date": end_date.isoformat()}, }, ) + + +async def test_add_event_date_time( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_insert_event: Mock, +) -> None: + """Test service call that adds an event with a date time range.""" + + assert await component_setup() + + start_datetime = datetime.datetime.now() + delta = datetime.timedelta(days=3, hours=3) + end_datetime = start_datetime + delta + + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_EVENT, + { + "calendar_id": CALENDAR_ID, + "summary": "Summary", + "description": "Description", + "start_date_time": start_datetime.isoformat(), + "end_date_time": end_datetime.isoformat(), + }, + blocking=True, + ) + mock_insert_event.assert_called() + + assert mock_insert_event.mock_calls[0] == call( + calendarId=CALENDAR_ID, + body={ + "summary": "Summary", + "description": "Description", + "start": { + "dateTime": start_datetime.isoformat(timespec="seconds"), + "timeZone": "CST", + }, + "end": { + "dateTime": end_datetime.isoformat(timespec="seconds"), + "timeZone": "CST", + }, + }, + ) From fbc39d1206b40fe458cba7829cbe2964c9c7d767 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 00:48:07 -0800 Subject: [PATCH 0221/1054] Log stack trace if exception without message (#67587) --- .../components/websocket_api/connection.py | 13 +++++++++---- tests/components/websocket_api/test_connection.py | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 075aed86453..9b53f358b85 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -121,6 +121,9 @@ class ActiveConnection: """Handle an exception while processing a handler.""" log_handler = self.logger.error + code = const.ERR_UNKNOWN_ERROR + err_message = None + if isinstance(err, Unauthorized): code = const.ERR_UNAUTHORIZED err_message = "Unauthorized" @@ -131,13 +134,15 @@ class ActiveConnection: code = const.ERR_TIMEOUT err_message = "Timeout" elif isinstance(err, HomeAssistantError): - code = const.ERR_UNKNOWN_ERROR err_message = str(err) - else: - code = const.ERR_UNKNOWN_ERROR + + # This if-check matches all other errors but also matches errors which + # result in an empty message. In that case we will also log the stack + # trace so it can be fixed. + if not err_message: err_message = "Unknown error" log_handler = self.logger.exception - log_handler("Error handling message: %s", err_message) + log_handler("Error handling message: %s (%s)", err_message, code) self.send_message(messages.error_message(msg["id"], code, err_message)) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 0a9ae710bc5..3a54d5912e0 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -54,6 +54,11 @@ async def test_exception_handling(): "Failed to do X", ), (ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"), + ( + exceptions.HomeAssistantError(), + websocket_api.ERR_UNKNOWN_ERROR, + "Unknown error", + ), ): send_messages.clear() conn.async_handle_exception({"id": 5}, exc) From cd25769667688abe79f737e35e05e579df93f03f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 09:51:53 +0100 Subject: [PATCH 0222/1054] Rename async_resolve_entity_ids to async_validate_entity_ids (#67603) --- .../homeassistant/triggers/numeric_state.py | 2 +- .../homeassistant/triggers/state.py | 2 +- homeassistant/components/zone/trigger.py | 2 +- homeassistant/helpers/condition.py | 4 ++-- homeassistant/helpers/entity_registry.py | 19 ++++++++++++++----- homeassistant/helpers/service.py | 2 +- tests/helpers/test_entity_registry.py | 10 +++++----- 7 files changed, 25 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 823bb608b4d..2d73f38d110 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -74,7 +74,7 @@ async def async_validate_trigger_config( """Validate trigger config.""" config = _TRIGGER_SCHEMA(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) return config diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index cafaea40c31..91629cc9933 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -77,7 +77,7 @@ async def async_validate_trigger_config( config = TRIGGER_STATE_SCHEMA(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 5a11bf2068d..8c5af3a0ac2 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -49,7 +49,7 @@ async def async_validate_trigger_config( """Validate trigger config.""" config = _TRIGGER_SCHEMA(config) registry = er.async_get(hass) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, config[CONF_ENTITY_ID] ) return config diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 3355424b710..71e238938cb 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -897,7 +897,7 @@ def numeric_state_validate_config( registry = er.async_get(hass) config = dict(config) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) return config @@ -908,7 +908,7 @@ def state_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType registry = er.async_get(hass) config = dict(config) - config[CONF_ENTITY_ID] = er.async_resolve_entity_ids( + config[CONF_ENTITY_ID] = er.async_validate_entity_ids( registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID]) ) return config diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4d4fce6e685..5cce811b4b3 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -924,13 +924,22 @@ async def async_migrate_entries( @callback -def async_resolve_entity_ids( +def async_validate_entity_ids( registry: EntityRegistry, entity_ids_or_uuids: list[str] ) -> list[str]: - """Resolve a list of entity ids or UUIDs to a list of entity ids.""" + """Validate and resolve a list of entity ids or UUIDs to a list of entity ids. - def resolve_entity(entity_id_or_uuid: str) -> str | None: - """Resolve an entity id or UUID to an entity id or None.""" + Returns a list with UUID resolved to entity_ids. + Raises vol.Invalid if any item is invalid, or if any a UUID is not associated with + an entity registry item. + """ + + def async_validate_entity_id(entity_id_or_uuid: str) -> str | None: + """Resolve an entity id or UUID to an entity id. + + Raises vol.Invalid if the entity or UUID is invalid, or if the UUID is not + associated with an entity registry item. + """ if valid_entity_id(entity_id_or_uuid): return entity_id_or_uuid if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: @@ -940,6 +949,6 @@ def async_resolve_entity_ids( tmp = [ resolved_item for item in entity_ids_or_uuids - if (resolved_item := resolve_entity(item)) is not None + if (resolved_item := async_validate_entity_id(item)) is not None ] return tmp diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e638288a58c..82c98c37b84 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -218,7 +218,7 @@ def async_prepare_call_from_config( if CONF_ENTITY_ID in target: registry = entity_registry.async_get(hass) - target[CONF_ENTITY_ID] = entity_registry.async_resolve_entity_ids( + target[CONF_ENTITY_ID] = entity_registry.async_validate_entity_ids( registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) ) except TemplateError as ex: diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index bad19bf78cb..7dc749b8592 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1174,19 +1174,19 @@ async def test_resolve_entity_ids(hass, registry): assert entry2.entity_id == "light.milk" expected = ["light.beer", "light.milk"] - assert er.async_resolve_entity_ids(registry, [entry1.id, entry2.id]) == expected + assert er.async_validate_entity_ids(registry, [entry1.id, entry2.id]) == expected expected = ["light.beer", "light.milk"] - assert er.async_resolve_entity_ids(registry, ["light.beer", entry2.id]) == expected + assert er.async_validate_entity_ids(registry, ["light.beer", entry2.id]) == expected with pytest.raises(vol.Invalid): - er.async_resolve_entity_ids(registry, ["light.beer", "bad_uuid"]) + er.async_validate_entity_ids(registry, ["light.beer", "bad_uuid"]) expected = ["light.unknown"] - assert er.async_resolve_entity_ids(registry, ["light.unknown"]) == expected + assert er.async_validate_entity_ids(registry, ["light.unknown"]) == expected with pytest.raises(vol.Invalid): - er.async_resolve_entity_ids(registry, ["unknown_uuid"]) + er.async_validate_entity_ids(registry, ["unknown_uuid"]) def test_entity_registry_items(): From 5f421252a6ceebce98f019edf7ada29e59dfc30f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 4 Mar 2022 10:03:38 +0100 Subject: [PATCH 0223/1054] Downgrade Renault warning (#67601) Co-authored-by: epenet --- homeassistant/components/renault/renault_vehicle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 12860bc6b9a..2d15e9c14a3 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -104,7 +104,7 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.warning( + LOGGER.info( "Ignoring endpoint %s as it is not supported for this vehicle: %s", coordinator.name, coordinator.last_exception, @@ -112,7 +112,7 @@ class RenaultVehicleProxy: del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.warning( + LOGGER.info( "Ignoring endpoint %s as it is denied for this vehicle: %s", coordinator.name, coordinator.last_exception, From 5ab4b5d15a0e185b428614eceb16e5a755626073 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 4 Mar 2022 12:39:29 +0100 Subject: [PATCH 0224/1054] MQTT Improve warning override deprecated settings (#67609) --- homeassistant/components/mqtt/__init__.py | 6 +++--- tests/components/mqtt/test_init.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index bbe773f141c..199b6238770 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -615,9 +615,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: override = {k: entry.data[k] for k in shared_keys} if CONF_PASSWORD in override: override[CONF_PASSWORD] = "********" - _LOGGER.info( - "Data in your configuration entry is going to override your " - "configuration.yaml: %s", + _LOGGER.warning( + "Deprecated configuration settings found in configuration.yaml. " + "These settings from your configuration entry will override: %s", override, ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 44803f2a5f6..87eddd4421f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1248,7 +1248,8 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): await hass.async_block_till_done() assert ( - "Data in your configuration entry is going to override your configuration.yaml:" + "Deprecated configuration settings found in configuration.yaml. " + "These settings from your configuration entry will override:" in caplog.text ) From f3ca52c2a16d2ce6d56fa7f9d35bbe2e07252898 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Mar 2022 12:39:44 +0100 Subject: [PATCH 0225/1054] Bump actions/upload-artifact from 2.3.1 to 3.0.0 (#67598) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .github/workflows/ci.yaml | 2 +- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cf96d3254ff..b4dba1fca52 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -829,7 +829,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.0.0 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 448389fb5bb..9a561d11997 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -50,13 +50,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.0.0 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v2.3.1 + uses: actions/upload-artifact@v3.0.0 with: name: requirements_diff path: ./requirements_diff.txt From 624c3be3aed79f1d8321169abb5cb85098768160 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 04:21:52 -0800 Subject: [PATCH 0226/1054] Add media source support to panasonic_viera (#67568) --- .../components/panasonic_viera/media_player.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 4b16d9361a1..f9cff28f6ff 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -5,12 +5,17 @@ import logging from panasonic_viera import Keys +from homeassistant.components import media_source from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_URL, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -52,6 +57,7 @@ SUPPORT_VIERATV = ( | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_STOP + | SUPPORT_BROWSE_MEDIA ) _LOGGER = logging.getLogger(__name__) @@ -198,8 +204,18 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type != MEDIA_TYPE_URL: _LOGGER.warning("Unsupported media_type: %s", media_type) return + media_id = async_process_play_media_url(self.hass, media_id) await self._remote.async_play_media(media_type, media_id) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media(self.hass, media_content_id) From f91a8093506f32bccfcdf43ef996ec1a46e688ea Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Mar 2022 14:47:27 +0100 Subject: [PATCH 0227/1054] Add mysensors notify tests (#67634) --- .coveragerc | 1 - tests/components/mysensors/conftest.py | 14 +++ .../mysensors/fixtures/text_node_state.json | 21 ++++ tests/components/mysensors/test_notify.py | 95 +++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 tests/components/mysensors/fixtures/text_node_state.json create mode 100644 tests/components/mysensors/test_notify.py diff --git a/.coveragerc b/.coveragerc index 11db4118ebd..4144b38cc0f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -736,7 +736,6 @@ omit = homeassistant/components/mysensors/handler.py homeassistant/components/mysensors/helpers.py homeassistant/components/mysensors/light.py - homeassistant/components/mysensors/notify.py homeassistant/components/mysensors/switch.py homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 7e38d42f011..54ae88cdccb 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -278,3 +278,17 @@ def temperature_sensor( nodes = update_gateway_nodes(gateway_nodes, temperature_sensor_state) node = nodes[1] return node + + +@pytest.fixture(name="text_node_state", scope="session") +def text_node_state_fixture() -> dict: + """Load the text node state.""" + return load_nodes_state("mysensors/text_node_state.json") + + +@pytest.fixture +def text_node(gateway_nodes: dict[int, Sensor], text_node_state: dict) -> Sensor: + """Load the text child node.""" + nodes = update_gateway_nodes(gateway_nodes, text_node_state) + node = nodes[1] + return node diff --git a/tests/components/mysensors/fixtures/text_node_state.json b/tests/components/mysensors/fixtures/text_node_state.json new file mode 100644 index 00000000000..a36d78b72a8 --- /dev/null +++ b/tests/components/mysensors/fixtures/text_node_state.json @@ -0,0 +1,21 @@ +{ + "1": { + "sensor_id": 1, + "children": { + "1": { + "id": 1, + "type": 36, + "description": "", + "values": { + "47": "test" + } + } + }, + "type": 17, + "sketch_name": "Text Node", + "sketch_version": "1.0", + "battery_level": 0, + "protocol_version": "2.3.2", + "heartbeat": 0 + } +} diff --git a/tests/components/mysensors/test_notify.py b/tests/components/mysensors/test_notify.py new file mode 100644 index 00000000000..e96b463cc78 --- /dev/null +++ b/tests/components/mysensors/test_notify.py @@ -0,0 +1,95 @@ +"""Provide tests for mysensors notify platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.sensor import Sensor + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_text_type( + hass: HomeAssistant, + text_node: Sensor, + transport_write: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test a text type child.""" + # Test without target. + await hass.services.async_call( + NOTIFY_DOMAIN, "mysensors", {"message": "Hello World"}, blocking=True + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;0;47;Hello World\n") + + # Test with target. + await hass.services.async_call( + NOTIFY_DOMAIN, + "mysensors", + {"message": "Hello", "target": "Text Node 1 1"}, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args == call("1;1;1;0;47;Hello\n") + + transport_write.reset_mock() + + # Test a message longer than 25 characters. + await hass.services.async_call( + NOTIFY_DOMAIN, + "mysensors", + { + "message": "This is a long message that will be split", + "target": "Text Node 1 1", + }, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list == [ + call("1;1;1;0;47;This is a long message th\n"), + call("1;1;1;0;47;at will be split\n"), + ] + + +async def test_text_type_discovery( + hass: HomeAssistant, + text_node: Sensor, + transport_write: MagicMock, + receive_message: Callable[[str], None], +) -> None: + """Test text type discovery.""" + receive_message("1;2;0;0;36;\n") + receive_message("1;2;1;0;47;test\n") + receive_message("1;2;1;0;47;test2\n") # Test that more than one set message works. + await hass.async_block_till_done() + + # Test targeting the discovered child. + await hass.services.async_call( + NOTIFY_DOMAIN, + "mysensors", + {"message": "Hello", "target": "Text Node 1 2"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;2;1;0;47;Hello\n") + + transport_write.reset_mock() + + # Test targeting all notify children. + await hass.services.async_call( + NOTIFY_DOMAIN, "mysensors", {"message": "Hello World"}, blocking=True + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list == [ + call("1;1;1;0;47;Hello World\n"), + call("1;2;1;0;47;Hello World\n"), + ] From 12a7b64e64e0068241dfd284ea65202e7c3b59fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 05:51:42 -0800 Subject: [PATCH 0228/1054] Add media source support to bluesound (#67563) --- .../components/bluesound/media_player.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 1995d7810a4..443d8faa5de 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -15,10 +15,15 @@ import async_timeout import voluptuous as vol import xmltodict +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -800,7 +805,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE - supported = SUPPORT_CLEAR_PLAYLIST + supported = SUPPORT_CLEAR_PLAYLIST | SUPPORT_BROWSE_MEDIA if self._status.get("indexing", "0") == "0": supported = ( @@ -1029,6 +1034,12 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + + media_id = async_process_play_media_url(self.hass, media_id) + url = f"Play?url={media_id}" if kwargs.get(ATTR_MEDIA_ENQUEUE): @@ -1063,3 +1074,11 @@ class BluesoundPlayer(MediaPlayerEntity): if mute: return await self.send_bluesound_command("Volume?mute=1") return await self.send_bluesound_command("Volume?mute=0") + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From cf1a21eb6e1ae20e2703aaf55693b68d6d9eb5b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 06:41:51 -0800 Subject: [PATCH 0229/1054] Allow squeezebox to play media sources (#67561) --- .../components/squeezebox/browse_media.py | 21 ++++++++++++++- .../components/squeezebox/media_player.py | 27 ++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 81d0231ae70..76627119b85 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -1,4 +1,7 @@ """Support for media browsing.""" +import contextlib + +from homeassistant.components import media_source from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, @@ -134,7 +137,7 @@ async def build_item_response(entity, player, payload): ) -async def library_payload(player): +async def library_payload(hass, player): """Create response payload to describe contents of library.""" library_info = { "title": "Music Library", @@ -161,13 +164,29 @@ async def library_payload(player): media_content_type=item, can_play=True, can_expand=True, + thumbnail="https://brands.home-assistant.io/_/squeezebox/logo.png", ) ) + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media( + hass, None, content_filter=media_source_content_filter + ) + # If domain is None, it's overview of available sources + if item.domain is None: + library_info["children"].extend(item.children) + else: + library_info["children"].append(item) + response = BrowseMedia(**library_info) return response +def media_source_content_filter(item: BrowseMedia) -> bool: + """Content filter for media sources.""" + return item.media_content_type.startswith("audio/") + + async def generate_playlist(player, payload): """Generate playlist from browsing payload.""" media_type = payload["search_type"] diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 5956b9fdf06..d8a1c29b723 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -8,7 +8,11 @@ import logging from pysqueezebox import Server, async_discover import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, @@ -53,7 +57,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow -from .browse_media import build_item_response, generate_playlist, library_payload +from .browse_media import ( + build_item_response, + generate_playlist, + library_payload, + media_source_content_filter, +) from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB SERVICE_CALL_METHOD = "call_method" @@ -460,7 +469,14 @@ class SqueezeBoxEntity(MediaPlayerEntity): if kwargs.get(ATTR_MEDIA_ENQUEUE): cmd = "add" - if media_type == MEDIA_TYPE_MUSIC: + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + + if media_type in MEDIA_TYPE_MUSIC: + media_id = async_process_play_media_url(self.hass, media_id) + await self._player.async_load_url(media_id, cmd) return @@ -554,7 +570,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): ) if media_content_type in [None, "library"]: - return await library_payload(self._player) + return await library_payload(self.hass, self._player) + + if media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media( + self.hass, media_content_id, content_filter=media_source_content_filter + ) payload = { "search_type": media_content_type, From 84c460ce784440e41f313c6f8c26911c8a16a992 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 06:44:57 -0800 Subject: [PATCH 0230/1054] Add media source support to mpd (#67565) --- homeassistant/components/mpd/media_player.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 4d3df8f00ca..9eda625b039 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -11,13 +11,18 @@ import mpd from mpd.asyncio import MPDClient import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, REPEAT_MODE_ALL, REPEAT_MODE_OFF, REPEAT_MODE_ONE, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -71,6 +76,7 @@ SUPPORT_MPD = ( | SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON + | SUPPORT_BROWSE_MEDIA ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -445,8 +451,13 @@ class MpdDevice(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Send the media player the command for playing a playlist.""" - _LOGGER.debug("Playing playlist: %s", media_id) + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type == MEDIA_TYPE_PLAYLIST: + _LOGGER.debug("Playing playlist: %s", media_id) if media_id in self._playlists: self._currentplaylist = media_id else: @@ -456,6 +467,8 @@ class MpdDevice(MediaPlayerEntity): await self._client.load(media_id) await self._client.play() else: + media_id = async_process_play_media_url(self.hass, media_id) + await self._client.clear() self._currentplaylist = None await self._client.add(media_id) @@ -507,3 +520,11 @@ class MpdDevice(MediaPlayerEntity): async def async_media_seek(self, position): """Send seek command.""" await self._client.seekcur(position) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From abf4f50515fac7ae95941b703aeb559e549b3024 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 06:47:06 -0800 Subject: [PATCH 0231/1054] Add media browser support to forked_daapd (#67564) --- .../components/forked_daapd/const.py | 2 ++ .../components/forked_daapd/media_player.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index 29da9f7244e..482218630a7 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,5 +1,6 @@ """Const for forked-daapd.""" from homeassistant.components.media_player.const import ( + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -78,6 +79,7 @@ SUPPORTED_FEATURES = ( | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) SUPPORTED_FEATURES_ZONE = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index a68f56e2965..f2c64fa81da 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -6,7 +6,11 @@ import logging from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -660,7 +664,14 @@ class ForkedDaapdMaster(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play a URI.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type == MEDIA_TYPE_MUSIC: + media_id = async_process_play_media_url(self.hass, media_id) + saved_state = self.state # save play state saved_mute = self.is_volume_muted sleep_future = asyncio.create_task( @@ -875,3 +886,11 @@ class ForkedDaapdUpdater: self._api, outputs_to_add, ) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From 749a76c4e1440a0765ac7c44d0b78203764b2e54 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 15:49:22 +0100 Subject: [PATCH 0232/1054] Improve logging for Fritz switches creation (#67640) --- homeassistant/components/fritz/const.py | 1 + homeassistant/components/fritz/switch.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/fritz/const.py b/homeassistant/components/fritz/const.py index f739ccf6858..a3ba907366c 100644 --- a/homeassistant/components/fritz/const.py +++ b/homeassistant/components/fritz/const.py @@ -57,6 +57,7 @@ SERVICE_SET_GUEST_WIFI_PW = "set_guest_wifi_password" SWITCH_TYPE_DEFLECTION = "CallDeflection" SWITCH_TYPE_PORTFORWARD = "PortForward" +SWITCH_TYPE_PROFILE = "Profile" SWITCH_TYPE_WIFINETWORK = "WiFiNetwork" UPTIME_DEVIATION = 5 diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 730ffb7fc0d..cac6e735a81 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -30,6 +30,7 @@ from .const import ( DOMAIN, SWITCH_TYPE_DEFLECTION, SWITCH_TYPE_PORTFORWARD, + SWITCH_TYPE_PROFILE, SWITCH_TYPE_WIFINETWORK, WIFI_STANDARD, MeshRoles, @@ -185,6 +186,7 @@ def profile_entities_list( data_fritz: FritzData, ) -> list[FritzBoxProfileSwitch]: """Add new tracker entities from the AVM device.""" + _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_PROFILE) new_profiles: list[FritzBoxProfileSwitch] = [] @@ -198,11 +200,15 @@ def profile_entities_list( if device_filter_out_from_trackers( mac, device, data_fritz.profile_switches.values() ): + _LOGGER.debug( + "Skipping profile switch creation for device %s", device.hostname + ) continue new_profiles.append(FritzBoxProfileSwitch(avm_wrapper, device)) data_fritz.profile_switches[avm_wrapper.unique_id].add(mac) + _LOGGER.debug("Creating %s profile switches", len(new_profiles)) return new_profiles From 209a5854f82bbf73454f8776f13089fb60774e82 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 15:50:23 +0100 Subject: [PATCH 0233/1054] End JSON files updated by scaffold script with a newline (#67639) --- script/scaffold/model.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 93801f973ea..019ed33fad1 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -50,7 +50,7 @@ class Info: """Update the integration manifest.""" print(f"Updating {self.domain} manifest: {kwargs}") self.manifest_path.write_text( - json.dumps({**self.manifest(), **kwargs}, indent=2) + json.dumps({**self.manifest(), **kwargs}, indent=2 + "\n") ) @property @@ -67,4 +67,6 @@ class Info: def update_strings(self, **kwargs) -> None: """Update the integration strings.""" print(f"Updating {self.domain} strings: {list(kwargs)}") - self.strings_path.write_text(json.dumps({**self.strings(), **kwargs}, indent=2)) + self.strings_path.write_text( + json.dumps({**self.strings(), **kwargs}, indent=2) + "\n" + ) From 3eadc67d592ab904934918fa75a51cf464e605ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Mar 2022 07:41:22 -0800 Subject: [PATCH 0234/1054] Add media source support to HeOS (#67562) --- homeassistant/components/heos/media_player.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index dad97e25653..227f7737ef4 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -7,13 +7,18 @@ from operator import ior from pyheos import HeosError, const as heos_const +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_URL, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, @@ -57,6 +62,7 @@ BASE_SUPPORTED_FEATURES = ( | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | SUPPORT_GROUPING + | SUPPORT_BROWSE_MEDIA ) PLAY_STATE_TO_STATE = { @@ -186,7 +192,14 @@ class HeosMediaPlayer(MediaPlayerEntity): @log_command_error("play media") async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_URL + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): + media_id = async_process_play_media_url(self.hass, media_id) + await self._player.play_url(media_id) return @@ -420,3 +433,11 @@ class HeosMediaPlayer(MediaPlayerEntity): def volume_level(self) -> float: """Volume level of the media player (0..1).""" return self._player.volume / 100 + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From 5965b015dd0a5cb12dd0ed80a1abac764495cc36 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Mar 2022 16:42:02 +0100 Subject: [PATCH 0235/1054] Adjust data entry flow to have an option data_schema (#67637) --- homeassistant/components/adguard/config_flow.py | 1 - homeassistant/components/almond/config_flow.py | 2 -- homeassistant/components/lyric/config_flow.py | 7 +------ homeassistant/components/moon/config_flow.py | 4 +--- homeassistant/components/neato/config_flow.py | 6 +----- homeassistant/components/nest/config_flow.py | 5 +---- homeassistant/components/netatmo/config_flow.py | 5 +---- homeassistant/components/powerwall/config_flow.py | 1 - homeassistant/components/profiler/config_flow.py | 4 +--- homeassistant/components/rtsp_to_webrtc/config_flow.py | 1 - homeassistant/components/sonarr/config_flow.py | 1 - homeassistant/components/spotify/config_flow.py | 2 -- homeassistant/components/velbus/config_flow.py | 1 - homeassistant/components/vizio/config_flow.py | 1 - homeassistant/components/vlc_telnet/config_flow.py | 1 - homeassistant/components/wiz/config_flow.py | 1 - homeassistant/components/xiaomi_miio/config_flow.py | 4 +--- homeassistant/components/zha/config_flow.py | 1 - homeassistant/components/zwave_js/config_flow.py | 1 - homeassistant/data_entry_flow.py | 4 ++-- tests/components/config/test_config_entries.py | 2 +- 21 files changed, 10 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 9afdc4e02b8..c7cb261d7c8 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -56,7 +56,6 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, - data_schema=vol.Schema({}), errors=errors or {}, ) diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index bc945025c24..dfbdae219ca 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -8,7 +8,6 @@ from typing import Any from aiohttp import ClientError import async_timeout from pyalmond import AlmondLocalAuth, WebAlmondAPI -import voluptuous as vol from yarl import URL from homeassistant import core, data_entry_flow @@ -122,5 +121,4 @@ class AlmondFlowHandler( return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": data["addon"]}, - data_schema=vol.Schema({}), ) diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 38341113206..698e7e19a26 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,8 +1,6 @@ """Config flow for Honeywell Lyric.""" import logging -import voluptuous as vol - from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -27,10 +25,7 @@ class OAuth2FlowHandler( async def async_step_reauth_confirm(self, user_input=None): """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() async def async_oauth_create_entry(self, data: dict) -> dict: diff --git a/homeassistant/components/moon/config_flow.py b/homeassistant/components/moon/config_flow.py index 1f7c5715f9e..abdd60c7b65 100644 --- a/homeassistant/components/moon/config_flow.py +++ b/homeassistant/components/moon/config_flow.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import Any -import voluptuous as vol - from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult @@ -30,7 +28,7 @@ class MoonConfigFlow(ConfigFlow, domain=DOMAIN): data={}, ) - return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + return self.async_show_form(step_id="user") async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle import from configuration.yaml.""" diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 07aea0a7e9c..15544371b2e 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -5,8 +5,6 @@ import logging from types import MappingProxyType from typing import Any -import voluptuous as vol - from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow @@ -46,9 +44,7 @@ class OAuth2FlowHandler( ) -> FlowResult: """Confirm reauth upon migration of old entries.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", data_schema=vol.Schema({}) - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index b257c7b51bb..aeebd48abb4 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -232,10 +232,7 @@ class NestFlowHandler( """Confirm reauth dialog.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") existing_entries = self._async_current_entries() if existing_entries: # Pick an existing auth implementation for Reauth if present. Note diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index ad8f75f5d45..bbd28e8398f 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -75,10 +75,7 @@ class NetatmoFlowHandler( ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({}), - ) + return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 08e9f90df1b..836aa46e2a4 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -145,7 +145,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return self.async_show_form( step_id="confirm_discovery", - data_schema=vol.Schema({}), description_placeholders={ "name": self.title, "ip_address": self.ip_address, diff --git a/homeassistant/components/profiler/config_flow.py b/homeassistant/components/profiler/config_flow.py index b63246ce386..db3241bf1d3 100644 --- a/homeassistant/components/profiler/config_flow.py +++ b/homeassistant/components/profiler/config_flow.py @@ -1,6 +1,4 @@ """Config flow for Profiler integration.""" -import voluptuous as vol - from homeassistant import config_entries from .const import DEFAULT_NAME, DOMAIN @@ -19,4 +17,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: return self.async_create_entry(title=DEFAULT_NAME, data={}) - return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + return self.async_show_form(step_id="user") diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 32735f3e824..815c5e5db7b 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -97,7 +97,6 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, - data_schema=vol.Schema({}), errors=errors, ) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index c9ef2d3ecc8..0bdcca6c033 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -76,7 +76,6 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", description_placeholders={"url": self.entry.data[CONF_URL]}, - data_schema=vol.Schema({}), errors={}, ) diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index bd01fa64acb..e1780ff9d40 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -5,7 +5,6 @@ import logging from typing import Any from spotipy import Spotify -import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry @@ -83,7 +82,6 @@ class SpotifyFlowHandler( return self.async_show_form( step_id="reauth_confirm", description_placeholders={"account": self.reauth_entry.data["id"]}, - data_schema=vol.Schema({}), errors={}, ) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 189dc029ded..993146d375c 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -115,5 +115,4 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="discovery_confirm", description_placeholders={CONF_NAME: self._title}, - data_schema=vol.Schema({}), ) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 019d016eb2a..8acc602dd36 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -454,7 +454,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._must_show_form = False return self.async_show_form( step_id=step_id, - data_schema=vol.Schema({}), description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]}, ) diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index d714057e6f7..29508ad1120 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -165,7 +165,6 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form( step_id="hassio_confirm", - data_schema=vol.Schema({}), description_placeholders={"addon": self.hassio_discovery["addon"]}, ) diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index 7d86502784c..04a0884059f 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -103,7 +103,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="discovery_confirm", description_placeholders=placeholders, - data_schema=vol.Schema({}), ) async def async_step_pick_device( diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 04e2b58ad0f..9796cd22746 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -136,9 +136,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_cloud() - return self.async_show_form( - step_id="reauth_confirm", data_schema=vol.Schema({}) - ) + return self.async_show_form(step_id="reauth_confirm") async def async_step_import(self, conf: dict): """Import a configuration from config.yaml.""" diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 4d6b22a82e5..f8f696c56e3 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -156,7 +156,6 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="confirm", description_placeholders={CONF_NAME: self._title}, - data_schema=vol.Schema({}), ) async def async_step_zeroconf( diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 32f406d7476..186695d8fbb 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -385,7 +385,6 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="usb_confirm", description_placeholders={CONF_NAME: self._title}, - data_schema=vol.Schema({}), ) self._usb_discovery = True diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index b69cf44dc6c..46311af94e0 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -69,7 +69,7 @@ class FlowResult(TypedDict, total=False): title: str data: Mapping[str, Any] step_id: str - data_schema: vol.Schema + data_schema: vol.Schema | None extra: str required: bool errors: dict[str, str] | None @@ -408,7 +408,7 @@ class FlowHandler: self, *, step_id: str, - data_schema: vol.Schema = None, + data_schema: vol.Schema | None = None, errors: dict[str, str] | None = None, description_placeholders: dict[str, Any] | None = None, last_step: bool | None = None, diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 06b9f1ae7f6..a24e0961f9c 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1017,7 +1017,7 @@ async def test_ignore_flow(hass, hass_ws_client): async def async_step_user(self, user_input=None): await self.async_set_unique_id("mock-unique-id") - return self.async_show_form(step_id="account", data_schema=vol.Schema({})) + return self.async_show_form(step_id="account") ws_client = await hass_ws_client(hass) From 3f9a6bbaa76c64a2ac7b14adc67a7faea8ed4cf8 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 4 Mar 2022 19:20:10 +0100 Subject: [PATCH 0236/1054] Fix sql false warning (#67614) --- homeassistant/components/sql/sensor.py | 2 +- tests/components/sql/test_sensor.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 1c8514d0d26..0a240469f83 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -172,7 +172,7 @@ class SQLSensor(SensorEntity): else: self._attr_native_value = data - if not data: + if data is None: _LOGGER.warning("%s returned no results", self._query) sess.close() diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 0e543f98a21..05f49d553e9 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -115,7 +115,9 @@ async def test_query_limit(hass: HomeAssistant) -> None: assert state.attributes["value"] == 5 -async def test_query_no_value(hass: HomeAssistant) -> None: +async def test_query_no_value( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test the SQL sensor with a query that returns no value.""" config = { "sensor": { @@ -137,6 +139,9 @@ async def test_query_no_value(hass: HomeAssistant) -> None: state = hass.states.get("sensor.count_tables") assert state.state == STATE_UNKNOWN + text = "SELECT 5 as value where 1=2 returned no results" + assert text in caplog.text + async def test_invalid_query(hass: HomeAssistant) -> None: """Test the SQL sensor for invalid queries.""" From 0c129145486764ca481d167ccdd97c97f775c200 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 20:02:17 +0100 Subject: [PATCH 0237/1054] Add config flow for switch.light (#67447) * Add config flow for switch.light * Refactor according to code review * Setup light switch from config entry * Improve async_resolve_entity * Prepare for multiple steps * Remove name and options flow from switch light * Check type before adding description to schema keys * Remove options flow enabler * Copy name from the switch * Move helper flows to new file * Improve test coverage * Fix name * Remove dead code from abstract method * Remove manifest 'helper' option * Validate registry entry id before forwarding to light platform * Improve test * Add translations * Improve config entry setup * Log when config entry fails setup * Update homeassistant/components/switch/__init__.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/switch/__init__.py | 23 ++- .../components/switch/config_flow.py | 45 +++++ homeassistant/components/switch/const.py | 3 + homeassistant/components/switch/light.py | 24 +++ homeassistant/components/switch/manifest.json | 7 +- homeassistant/components/switch/strings.json | 10 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/helpers/entity_registry.py | 33 ++-- .../helpers/helper_config_entry_flow.py | 105 ++++++++++++ tests/components/switch/test_config_flow.py | 154 ++++++++++++++++++ tests/components/switch/test_light.py | 70 ++++++++ 11 files changed, 454 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/switch/config_flow.py create mode 100644 homeassistant/components/switch/const.py create mode 100644 homeassistant/helpers/helper_config_entry_flow.py create mode 100644 tests/components/switch/test_config_flow.py diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 157a5cd40c7..b7e0ffac59c 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,12 +11,15 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -26,7 +29,8 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -DOMAIN = "switch" +from .const import DOMAIN + SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -59,6 +63,8 @@ DEVICE_CLASSES = [cls.value for cls in SwitchDeviceClass] DEVICE_CLASS_OUTLET = SwitchDeviceClass.OUTLET.value DEVICE_CLASS_SWITCH = SwitchDeviceClass.SWITCH.value +PLATFORMS: list[Platform] = [Platform.LIGHT] + @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -85,6 +91,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" + if entry.domain == DOMAIN: + registry = er.async_get(hass) + try: + er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + except vol.Invalid: + # The entity is identified by an unknown entity registry ID + _LOGGER.error( + "Failed to setup light switch for unknown entity %s", + entry.options[CONF_ENTITY_ID], + ) + return False + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + component: EntityComponent = hass.data[DOMAIN] return await component.async_setup_entry(entry) diff --git a/homeassistant/components/switch/config_flow.py b/homeassistant/components/switch/config_flow.py new file mode 100644 index 00000000000..1adc4ec0aee --- /dev/null +++ b/homeassistant/components/switch/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow for Switch integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.core import split_entity_id +from homeassistant.helpers import ( + entity_registry as er, + helper_config_entry_flow, + selector, +) + +from .const import DOMAIN + +STEPS = { + "init": vol.Schema( + { + vol.Required("entity_id"): selector.selector( + {"entity": {"domain": "switch"}} + ), + } + ) +} + + +class SwitchLightConfigFlowHandler( + helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN +): + """Handle a config or options flow for Switch Light.""" + + steps = STEPS + + def async_config_entry_title(self, user_input: dict[str, Any]) -> str: + """Return config entry title.""" + registry = er.async_get(self.hass) + object_id = split_entity_id(user_input["entity_id"])[1] + entry = registry.async_get(user_input["entity_id"]) + if entry: + return entry.name or entry.original_name or object_id + state = self.hass.states.get(user_input["entity_id"]) + if state: + return state.name or object_id + return object_id diff --git a/homeassistant/components/switch/const.py b/homeassistant/components/switch/const.py new file mode 100644 index 00000000000..aaff452c5ce --- /dev/null +++ b/homeassistant/components/switch/const.py @@ -0,0 +1,3 @@ +"""Constants for the Switch integration.""" + +DOMAIN = "switch" diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 32c0aff74fa..7c732d7750d 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -11,6 +11,7 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -59,6 +60,29 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + + async_add_entities( + [ + LightSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + ) + ] + ) + + class LightSwitch(LightEntity): """Represents a Switch as a Light.""" diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 4c52e596648..f087ace1bce 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -2,6 +2,9 @@ "domain": "switch", "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", - "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "codeowners": [ + "@home-assistant/core" + ], + "quality_scale": "internal", + "config_flow": true } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 7ea84e649ef..5cdd0c35936 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -1,5 +1,15 @@ { "title": "Switch", + "config": { + "step": { + "init": { + "description": "Select the switch for the light switch.", + "data": { + "entity_id": "Switch entity" + } + } + } + }, "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7a36cfebc7b..2ee6e235e91 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -319,6 +319,7 @@ FLOWS = [ "stookalert", "subaru", "surepetcare", + "switch", "switchbot", "switcher_kis", "syncthing", diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5cce811b4b3..7c86bfaa501 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -923,6 +923,20 @@ async def async_migrate_entries( ent_reg.async_update_entity(entry.entity_id, **updates) +@callback +def async_validate_entity_id(registry: EntityRegistry, entity_id_or_uuid: str) -> str: + """Validate and resolve an entity id or UUID to an entity id. + + Raises vol.Invalid if the entity or UUID is invalid, or if the UUID is not + associated with an entity registry item. + """ + if valid_entity_id(entity_id_or_uuid): + return entity_id_or_uuid + if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: + raise vol.Invalid(f"Unknown entity registry entry {entity_id_or_uuid}") + return entry.entity_id + + @callback def async_validate_entity_ids( registry: EntityRegistry, entity_ids_or_uuids: list[str] @@ -934,21 +948,4 @@ def async_validate_entity_ids( an entity registry item. """ - def async_validate_entity_id(entity_id_or_uuid: str) -> str | None: - """Resolve an entity id or UUID to an entity id. - - Raises vol.Invalid if the entity or UUID is invalid, or if the UUID is not - associated with an entity registry item. - """ - if valid_entity_id(entity_id_or_uuid): - return entity_id_or_uuid - if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: - raise vol.Invalid(f"Unknown entity registry entry {entity_id_or_uuid}") - return entry.entity_id - - tmp = [ - resolved_item - for item in entity_ids_or_uuids - if (resolved_item := async_validate_entity_id(item)) is not None - ] - return tmp + return [async_validate_entity_id(registry, item) for item in entity_ids_or_uuids] diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py new file mode 100644 index 00000000000..82d10868d01 --- /dev/null +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -0,0 +1,105 @@ +"""Helpers for data entry flows for helper config entries.""" +from __future__ import annotations + +from abc import abstractmethod +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResult + + +class HelperCommonFlowHandler: + """Handle a config or options flow for helper.""" + + def __init__( + self, + handler: HelperConfigFlowHandler, + config_entry: config_entries.ConfigEntry | None, + ) -> None: + """Initialize a common handler.""" + self._handler = handler + self._options = dict(config_entry.options) if config_entry is not None else {} + + async def async_step(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle a step.""" + errors = None + step_id = ( + self._handler.cur_step["step_id"] if self._handler.cur_step else "init" + ) + if _user_input is not None: + errors = {} + try: + user_input = await self._handler.async_validate_input( + self._handler.hass, step_id, _user_input + ) + except vol.Invalid as exc: + errors["base"] = str(exc) + else: + if ( + next_step_id := self._handler.async_next_step(step_id, user_input) + ) is None: + title = self._handler.async_config_entry_title(user_input) + return self._handler.async_create_entry( + title=title, data=user_input + ) + return self._handler.async_show_form( + step_id=next_step_id, data_schema=self._handler.steps[next_step_id] + ) + + return self._handler.async_show_form( + step_id=step_id, data_schema=self._handler.steps[step_id], errors=errors + ) + + +class HelperConfigFlowHandler(config_entries.ConfigFlow): + """Handle a config flow for helper integrations.""" + + steps: dict[str, vol.Schema] + + VERSION = 1 + + # pylint: disable-next=arguments-differ + def __init_subclass__(cls, **kwargs: Any) -> None: + """Initialize a subclass, register if possible.""" + super().__init_subclass__(**kwargs) + + for step in cls.steps: + setattr(cls, f"async_step_{step}", cls.async_step) + + def __init__(self) -> None: + """Initialize config flow.""" + self._common_handler = HelperCommonFlowHandler(self, None) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + return await self.async_step() + + async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle a step.""" + result = await self._common_handler.async_step(user_input) + if result["type"] == RESULT_TYPE_CREATE_ENTRY: + result["options"] = result["data"] + result["data"] = {} + return result + + # pylint: disable-next=no-self-use + @abstractmethod + def async_config_entry_title(self, user_input: dict[str, Any]) -> str: + """Return config entry title.""" + + # pylint: disable-next=no-self-use + def async_next_step(self, step_id: str, user_input: dict[str, Any]) -> str | None: + """Return next step_id, or None to finish the flow.""" + return None + + # pylint: disable-next=no-self-use + async def async_validate_input( + self, hass: HomeAssistant, step_id: str, user_input: dict[str, Any] + ) -> dict[str, Any]: + """Validate user input.""" + return user_input diff --git a/tests/components/switch/test_config_flow.py b/tests/components/switch/test_config_flow.py new file mode 100644 index 00000000000..ca838b7b972 --- /dev/null +++ b/tests/components/switch/test_config_flow.py @@ -0,0 +1,154 @@ +"""Test the switch light config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.switch import async_setup_entry +from homeassistant.components.switch.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.helpers import entity_registry as er + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.switch.async_setup_entry", + wraps=async_setup_entry, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "entity_id": "switch.ceiling", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ceiling" + assert result["data"] == {} + assert result["options"] == {"entity_id": "switch.ceiling"} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {"entity_id": "switch.ceiling"} + + assert hass.states.get("light.ceiling") + + +async def test_name(hass: HomeAssistant) -> None: + """Test the config flow name is copied from registry entry, with fallback to state.""" + registry = er.async_get(hass) + + # No entry or state, use Object ID + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"entity_id": "switch.ceiling"}, + ) + assert result["title"] == "ceiling" + + # State set, use name from state + hass.states.async_set("switch.ceiling", "on", {"friendly_name": "State Name"}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"entity_id": "switch.ceiling"}, + ) + assert result["title"] == "State Name" + + # Entity registered, use original name from registry entry + hass.states.async_remove("switch.ceiling") + entry = registry.async_get_or_create( + "switch", + "test", + "unique", + suggested_object_id="ceiling", + original_name="Original Name", + ) + assert entry.entity_id == "switch.ceiling" + hass.states.async_set("switch.ceiling", "on", {"friendly_name": "State Name"}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"entity_id": "switch.ceiling"}, + ) + assert result["title"] == "Original Name" + + # Entity has customized name + registry.async_update_entity("switch.ceiling", name="Custom Name") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"entity_id": "switch.ceiling"}, + ) + assert result["title"] == "Custom Name" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + + +async def test_options(hass: HomeAssistant) -> None: + """Test reconfiguring.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert get_suggested(result["data_schema"].schema, "entity_id") is None + assert get_suggested(result["data_schema"].schema, "name") is None + + with patch( + "homeassistant.components.switch.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "entity_id": "switch.ceiling", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ceiling" + assert result["data"] == {} + assert result["options"] == {"entity_id": "switch.ceiling"} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {"entity_id": "switch.ceiling"} + + # Switch light has no options flow + with pytest.raises(data_entry_flow.UnknownHandler): + await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index 62fef242e9f..518fd8db20b 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -5,8 +5,12 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, COLOR_MODE_ONOFF, ) +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.components.light import common from tests.components.switch import common as switch_common @@ -96,3 +100,69 @@ async def test_switch_service_calls(hass): assert hass.states.get("switch.decorative_lights").state == "on" assert hass.states.get("light.light_switch").state == "on" + + +async def test_config_entry(hass: HomeAssistant): + """Test light switch setup from config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=SWITCH_DOMAIN, + options={"entity_id": "switch.abc"}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert SWITCH_DOMAIN in hass.config.components + + state = hass.states.get("light.abc") + assert state.state == "unavailable" + # Name copied from config entry title + assert state.name == "ABC" + + # Check the light is added to the entity registry + registry = er.async_get(hass) + entity_entry = registry.async_get("light.abc") + assert entity_entry.unique_id == config_entry.entry_id + + +async def test_config_entry_uuid(hass: HomeAssistant): + """Test light switch setup from config entry with entity registry id.""" + registry = er.async_get(hass) + registry_entry = registry.async_get_or_create("switch", "test", "unique") + + config_entry = MockConfigEntry( + data={}, + domain=SWITCH_DOMAIN, + options={"entity_id": registry_entry.id}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("light.abc") + + +async def test_config_entry_unregistered_uuid(hass: HomeAssistant): + """Test light switch setup from config entry with unknown entity registry id.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + + config_entry = MockConfigEntry( + data={}, + domain=SWITCH_DOMAIN, + options={"entity_id": fake_uuid}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From c337e69f0a1f3c9ebc35889f7b93573b1eb7f9c7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 4 Mar 2022 20:09:49 +0100 Subject: [PATCH 0238/1054] Bump pydroid-ipcam to 1.3.1 (#67655) * Bump pydroid-ipcam to 1.3.1 * Remove loop and set ssl to False --- homeassistant/components/android_ip_webcam/__init__.py | 2 +- homeassistant/components/android_ip_webcam/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index ca4af7fd68a..67bb00f441d 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -204,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Init ip webcam cam = PyDroidIPCam( - hass.loop, websession, host, cam_config[CONF_PORT], username=username, password=password, timeout=cam_config[CONF_TIMEOUT], + ssl=False, ) if switches is None: diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 637a773ac33..39223e6636d 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -2,7 +2,7 @@ "domain": "android_ip_webcam", "name": "Android IP Webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", - "requirements": ["pydroid-ipcam==0.8"], + "requirements": ["pydroid-ipcam==1.3.1"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index a5b88f0e02b..4fc42ec3c37 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,7 +1431,7 @@ pydispatcher==2.0.5 pydoods==1.0.2 # homeassistant.components.android_ip_webcam -pydroid-ipcam==0.8 +pydroid-ipcam==1.3.1 # homeassistant.components.ebox pyebox==1.1.4 From cb1299b43463d96bac4c9c7d26134b5bdce0ab95 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Mar 2022 20:17:11 +0100 Subject: [PATCH 0239/1054] Fix Fan template loosing percentage/preset (#67648) Co-authored-by: J. Nick Koston --- homeassistant/components/template/fan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 1ddd37ba7bc..1d25c24017f 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -277,8 +277,6 @@ class TemplateFan(TemplateEntity, FanEntity): """Turn off the fan.""" await self._off_script.async_run(context=self._context) self._state = STATE_OFF - self._percentage = 0 - self._preset_mode = None async def async_set_percentage(self, percentage: int) -> None: """Set the percentage speed of the fan.""" From b8420a7e3be432c43df40dcf81da368f2608e8dd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 4 Mar 2022 13:21:11 -0700 Subject: [PATCH 0240/1054] Add missing Ambient PWS sensors (#67322) --- .../ambient_station/binary_sensor.py | 94 ++++++++++++++++++- .../components/ambient_station/sensor.py | 44 +++++++++ 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 153fbf066db..f9fcec6aa6a 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -28,10 +28,21 @@ TYPE_BATT6 = "batt6" TYPE_BATT7 = "batt7" TYPE_BATT8 = "batt8" TYPE_BATT9 = "batt9" -TYPE_BATT_CO2 = "batt_co2" TYPE_BATTOUT = "battout" -TYPE_PM25_BATT = "batt_25" +TYPE_BATT_CO2 = "batt_co2" +TYPE_BATT_LIGHTNING = "batt_lightning" +TYPE_BATT_SM1 = "battsm1" +TYPE_BATT_SM10 = "battsm10" +TYPE_BATT_SM2 = "battsm2" +TYPE_BATT_SM3 = "battsm3" +TYPE_BATT_SM4 = "battsm4" +TYPE_BATT_SM5 = "battsm5" +TYPE_BATT_SM6 = "battsm6" +TYPE_BATT_SM7 = "battsm7" +TYPE_BATT_SM8 = "battsm8" +TYPE_BATT_SM9 = "battsm9" TYPE_PM25IN_BATT = "batt_25in" +TYPE_PM25_BATT = "batt_25" TYPE_RELAY1 = "relay1" TYPE_RELAY10 = "relay10" TYPE_RELAY2 = "relay2" @@ -131,7 +142,77 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), AmbientBinarySensorDescription( key=TYPE_BATT10, - name="Battery 10", + name="Soil Monitor Battery 10", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM1, + name="Soil Monitor Battery 1", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM2, + name="Soil Monitor Battery 2", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM3, + name="Soil Monitor Battery 3", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM4, + name="Soil Monitor Battery 4", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM5, + name="Soil Monitor Battery 5", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM6, + name="Soil Monitor Battery 6", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM7, + name="Soil Monitor Battery 7", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM8, + name="Soil Monitor Battery 8", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM9, + name="Soil Monitor Battery 9", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), + AmbientBinarySensorDescription( + key=TYPE_BATT_SM10, + name="Soil Monitor Battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, @@ -143,6 +224,13 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), + AmbientBinarySensorDescription( + key=TYPE_BATT_LIGHTNING, + name="Lightning Detector Battery", + device_class=BinarySensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + on_state=0, + ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, name="PM25 Indoor Battery", diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 3a17ecfc1f1..c837ef6fdec 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -32,6 +32,10 @@ from . import AmbientStation, AmbientWeatherEntity from .const import ATTR_LAST_DATA, DOMAIN, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX TYPE_24HOURRAININ = "24hourrainin" +TYPE_AQI_PM25 = "aqi_pm25" +TYPE_AQI_PM25_24H = "aqi_pm25_24h" +TYPE_AQI_PM25_IN = "aqi_pm25_in" +TYPE_AQI_PM25_IN_24H = "aqi_pm25_in_24h" TYPE_BAROMABSIN = "baromabsin" TYPE_BAROMRELIN = "baromrelin" TYPE_CO2 = "co2" @@ -53,6 +57,8 @@ TYPE_HUMIDITY8 = "humidity8" TYPE_HUMIDITY9 = "humidity9" TYPE_HUMIDITYIN = "humidityin" TYPE_LASTRAIN = "lastRain" +TYPE_LIGHTNING_PER_DAY = "lightning_day" +TYPE_LIGHTNING_PER_HOUR = "lightning_hour" TYPE_MAXDAILYGUST = "maxdailygust" TYPE_MONTHLYRAININ = "monthlyrainin" TYPE_PM25 = "pm25" @@ -112,6 +118,30 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key=TYPE_AQI_PM25, + name="AQI PM2.5", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_24H, + name="AQI PM2.5 24h Avg", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_IN, + name="AQI PM2.5 Indoor", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=TYPE_AQI_PM25_IN_24H, + name="AQI PM2.5 Indoor 24h Avg", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key=TYPE_BAROMABSIN, name="Abs Pressure", @@ -246,6 +276,20 @@ SENSOR_DESCRIPTIONS = ( icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_DAY, + name="Lightning Strikes Per Day", + icon="mdi:lightning-bolt", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key=TYPE_LIGHTNING_PER_HOUR, + name="Lightning Strikes Per Hour", + icon="mdi:lightning-bolt", + native_unit_of_measurement="strikes", + state_class=SensorStateClass.TOTAL_INCREASING, + ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, name="Max Gust", From 9db3e321e4a6163d5de1fb6d053017207c6352c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 4 Mar 2022 23:06:15 +0100 Subject: [PATCH 0241/1054] Add message for unsupported models in samsungtv (#67549) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 25 +++++++++++++++++-- homeassistant/components/samsungtv/bridge.py | 13 ---------- .../components/samsungtv/test_diagnostics.py | 1 + tests/components/samsungtv/test_init.py | 18 ++++++++++++- 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 664ce1fa439..9f605291dda 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -33,6 +33,7 @@ from .bridge import ( mac_from_device_info, ) from .const import ( + CONF_MODEL, CONF_ON_ACTION, DEFAULT_NAME, DOMAIN, @@ -149,6 +150,7 @@ async def _async_create_bridge_with_updated_data( host: str = entry.data[CONF_HOST] port: int | None = entry.data.get(CONF_PORT) method: str | None = entry.data.get(CONF_METHOD) + load_info_attempted = False info: dict[str, Any] | None = None if not port or not method: @@ -159,6 +161,7 @@ async def _async_create_bridge_with_updated_data( # When we imported from yaml we didn't setup the method # because we didn't know it port, method, info = await async_get_device_info(hass, None, host) + load_info_attempted = True if not port or not method: raise ConfigEntryNotReady( "Failed to determine connection method, make sure the device is on." @@ -171,12 +174,14 @@ async def _async_create_bridge_with_updated_data( bridge = _async_get_device_bridge(hass, {**entry.data, **updated_data}) mac: str | None = entry.data.get(CONF_MAC) + model: str | None = entry.data.get(CONF_MODEL) + if (not mac or not model) and not load_info_attempted: + info = await bridge.async_device_info() + if not mac: LOGGER.debug("Attempting to get mac for %s", host) if info: mac = mac_from_device_info(info) - else: - mac = await bridge.async_mac_from_device() if not mac: mac = await hass.async_add_executor_job( @@ -189,6 +194,22 @@ async def _async_create_bridge_with_updated_data( else: LOGGER.info("Failed to get mac for %s", host) + if not model: + LOGGER.debug("Attempting to get model for %s", host) + if info: + model = info.get("device", {}).get("modelName") + if model: + LOGGER.info("Updated model to %s for %s", model, host) + updated_data[CONF_MODEL] = model + + if model and len(model) > 4 and model[4] in ("H", "J"): + LOGGER.info( + "Detected model %s for %s. Some televisions from H and J series use " + "an encrypted protocol that may not be supported in this integration", + model, + host, + ) + if updated_data: data = {**entry.data, **updated_data} hass.config_entries.async_update_entry(entry, data=data) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 8fbe0572379..210bdd87cb3 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -119,10 +119,6 @@ class SamsungTVBridge(ABC): async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" - @abstractmethod - async def async_mac_from_device(self) -> str | None: - """Try to fetch the mac address of the TV.""" - @abstractmethod async def async_get_app_list(self) -> dict[str, str] | None: """Get installed app list.""" @@ -169,10 +165,6 @@ class SamsungTVLegacyBridge(SamsungTVBridge): } self._remote: Remote | None = None - async def async_mac_from_device(self) -> None: - """Try to fetch the mac address of the TV.""" - return None - async def async_get_app_list(self) -> dict[str, str]: """Get installed app list.""" return {} @@ -304,11 +296,6 @@ class SamsungTVWSBridge(SamsungTVBridge): self._remote: SamsungTVWSAsyncRemote | None = None self._remote_lock = asyncio.Lock() - async def async_mac_from_device(self) -> str | None: - """Try to fetch the mac address of the TV.""" - info = await self.async_device_info() - return mac_from_device_info(info) if info else None - async def async_get_app_list(self) -> dict[str, str] | None: """Get installed app list.""" if self._app_list is None: diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 47446e9103c..ba9e050d9ce 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -42,6 +42,7 @@ async def test_entry_diagnostics( "ip_address": "test", "mac": "aa:bb:cc:dd:ee:ff", "method": "websocket", + "model": "82GXARRS", "name": "fake", "port": 8002, "token": REDACTED, diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index bc815b838ed..14462b1aaf3 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,5 +1,6 @@ """Tests for the Samsung TV Integration.""" -from unittest.mock import patch +from copy import deepcopy +from unittest.mock import Mock, patch import pytest @@ -132,3 +133,18 @@ async def test_setup_duplicate_entries(hass: HomeAssistant) -> None: assert len(hass.states.async_all("media_player")) == 1 await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) assert len(hass.states.async_all("media_player")) == 1 + + +@pytest.mark.usefixtures("remotews") +async def test_setup_h_j_model( + hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test Samsung TV integration is setup.""" + device_info = deepcopy(rest_api.rest_device_info.return_value) + device_info["device"]["modelName"] = "UE48JU6400" + rest_api.rest_device_info.return_value = device_info + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + assert state + assert "H and J series use an encrypted protocol" in caplog.text From f09e288feec277411fb71d19077cb90ce8e5233e Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Fri, 4 Mar 2022 17:07:01 -0500 Subject: [PATCH 0242/1054] Bump sleepiq library version (#67659) --- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 93cd1be3204..24a3fc2f463 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.1.0"], + "requirements": ["asyncsleepiq==1.1.2"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 4fc42ec3c37..059645aecae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -329,7 +329,7 @@ async-upnp-client==0.25.0 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.1.0 +asyncsleepiq==1.1.2 # homeassistant.components.aten_pe atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c216e921e61..ff584c8a83c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -259,7 +259,7 @@ arcam-fmj==0.12.0 async-upnp-client==0.25.0 # homeassistant.components.sleepiq -asyncsleepiq==1.1.0 +asyncsleepiq==1.1.2 # homeassistant.components.aurora auroranoaa==0.0.2 From 1ebe82fc4bf9321ac4dd953c9e5382b408e00f77 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Mar 2022 23:17:43 +0100 Subject: [PATCH 0243/1054] Fix reload of media player groups (#67653) --- homeassistant/components/group/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index b36d4f1033f..8e595d75db6 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -59,11 +59,12 @@ SERVICE_SET = "set" SERVICE_REMOVE = "remove" PLATFORMS = [ - Platform.LIGHT, - Platform.COVER, - Platform.NOTIFY, - Platform.FAN, Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.MEDIA_PLAYER, + Platform.NOTIFY, ] REG_KEY = f"{DOMAIN}_registry" From e4221336dcbce1ed1042b237e551265381f9790b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Mar 2022 12:30:40 -1000 Subject: [PATCH 0244/1054] Handle elkm1 login case with username and insecure login (#67602) --- homeassistant/components/elkm1/__init__.py | 22 ++++++++++--------- homeassistant/components/elkm1/config_flow.py | 4 +--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 04a26f2822b..6791c2ec1bb 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -279,9 +279,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: keypad.add_callback(_element_changed) try: - if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME]) - ): + if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT): return False except asyncio.TimeoutError as exc: raise ConfigEntryNotReady(f"Timed out connecting to {conf[CONF_HOST]}") from exc @@ -334,7 +332,6 @@ async def async_wait_for_elk_to_sync( elk: elkm1.Elk, login_timeout: int, sync_timeout: int, - password_auth: bool, ) -> bool: """Wait until the elk has finished sync. Can fail login or timeout.""" @@ -354,18 +351,23 @@ async def async_wait_for_elk_to_sync( login_event.set() sync_event.set() + def first_response(*args, **kwargs): + _LOGGER.debug("ElkM1 received first response (VN)") + login_event.set() + def sync_complete(): sync_event.set() success = True elk.add_handler("login", login_status) + # VN is the first command sent for panel, when we get + # it back we now we are logged in either with or without a password + elk.add_handler("VN", first_response) elk.add_handler("sync_complete", sync_complete) - events = [] - if password_auth: - events.append(("login", login_event, login_timeout)) - events.append(("sync_complete", sync_event, sync_timeout)) - - for name, event, timeout in events: + for name, event, timeout in ( + ("login", login_event, login_timeout), + ("sync_complete", sync_event, sync_timeout), + ): _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) try: async with async_timeout.timeout(timeout): diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index a21cf186005..96f9fd5d078 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -81,9 +81,7 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid) - ): + if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT): raise InvalidAuth short_mac = _short_mac(mac) if mac else None From 53543f15a590156663038a946b497ef0a32e4879 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 23:38:28 +0100 Subject: [PATCH 0245/1054] Allign logic for Fritz sensors and binary_sensors (#67623) --- .../components/fritz/binary_sensor.py | 13 ++++++---- homeassistant/components/fritz/common.py | 25 ++++++++++++++++++ homeassistant/components/fritz/sensor.py | 26 +++---------------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index b416e0cfb11..db1aac99c47 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -1,6 +1,7 @@ """AVM FRITZ!Box connectivity sensor.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass import logging @@ -14,8 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .common import AvmWrapper, FritzBoxBaseEntity -from .const import DOMAIN, MeshRoles +from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) class FritzBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes Fritz sensor entity.""" - exclude_mesh_role: MeshRoles = MeshRoles.SLAVE + is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = ( @@ -45,7 +46,7 @@ SENSOR_TYPES: tuple[FritzBinarySensorEntityDescription, ...] = ( name="Firmware Update", device_class=BinarySensorDeviceClass.UPDATE, entity_category=EntityCategory.DIAGNOSTIC, - exclude_mesh_role=MeshRoles.NONE, + is_suitable=lambda info: True, ), ) @@ -57,10 +58,12 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box binary sensors") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] + connection_info = await avm_wrapper.async_get_connection_info() + entities = [ FritzBoxBinarySensor(avm_wrapper, entry.title, description) for description in SENSOR_TYPES - if (description.exclude_mesh_role != avm_wrapper.mesh_role) + if description.is_suitable(connection_info) ] async_add_entities(entities, True) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 2fc28433e56..4c307c126cd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -642,6 +642,22 @@ class AvmWrapper(FritzBoxTools): partial(self.get_wan_link_properties) ) + async def async_get_connection_info(self) -> ConnectionInfo: + """Return ConnectionInfo data.""" + + link_properties = await self.async_get_wan_link_properties() + connection_info = ConnectionInfo( + connection=link_properties.get("NewWANAccessType", "").lower(), + mesh_role=self.mesh_role, + wan_enabled=self.device_is_router, + ) + _LOGGER.debug( + "ConnectionInfo for FritzBox %s: %s", + self.host, + connection_info, + ) + return connection_info + async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" @@ -970,3 +986,12 @@ class FritzBoxBaseEntity: name=self._device_name, sw_version=self._avm_wrapper.current_firmware, ) + + +@dataclass +class ConnectionInfo: + """Fritz sensor connection information class.""" + + connection: str + mesh_role: MeshRoles + wan_enabled: bool diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index f01966d7114..9811adf6829 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -28,8 +28,8 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from .common import AvmWrapper, FritzBoxBaseEntity -from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION, MeshRoles +from .common import AvmWrapper, ConnectionInfo, FritzBoxBaseEntity +from .const import DOMAIN, DSL_CONNECTION, UPTIME_DEVIATION _LOGGER = logging.getLogger(__name__) @@ -134,15 +134,6 @@ def _retrieve_link_attenuation_received_state( return status.attenuation[1] / 10 # type: ignore[no-any-return] -@dataclass -class ConnectionInfo: - """Fritz sensor connection information class.""" - - connection: str - mesh_role: MeshRoles - wan_enabled: bool - - @dataclass class FritzRequireKeysMixin: """Fritz sensor data class.""" @@ -283,18 +274,7 @@ async def async_setup_entry( _LOGGER.debug("Setting up FRITZ!Box sensors") avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] - link_properties = await avm_wrapper.async_get_wan_link_properties() - connection_info = ConnectionInfo( - connection=link_properties.get("NewWANAccessType", "").lower(), - mesh_role=avm_wrapper.mesh_role, - wan_enabled=avm_wrapper.device_is_router, - ) - - _LOGGER.debug( - "ConnectionInfo for FritzBox %s: %s", - avm_wrapper.host, - connection_info, - ) + connection_info = await avm_wrapper.async_get_connection_info() entities = [ FritzBoxSensor(avm_wrapper, entry.title, description) From 777ae584d49e45a074f74cdff47f0706597907c8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 4 Mar 2022 23:43:33 +0100 Subject: [PATCH 0246/1054] Add unique_id to Fritz diagnostics (#67384) Co-authored-by: Paulus Schoutsen --- homeassistant/components/fritz/diagnostics.py | 3 +++ tests/components/fritz/test_diagnostics.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index fa4ff6a7db8..ed45295892b 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -22,6 +22,9 @@ async def async_get_config_entry_diagnostics( "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": { "model": avm_wrapper.model, + "unique_id": avm_wrapper.unique_id.replace( + avm_wrapper.unique_id[6:11], "XX:XX" + ), "current_firmware": avm_wrapper.current_firmware, "latest_firmware": avm_wrapper.latest_firmware, "update_available": avm_wrapper.update_available, diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index 892210d0844..a4b4942c375 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import MOCK_USER_DATA +from .const import MOCK_MESH_MASTER_MAC, MOCK_USER_DATA from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -69,6 +69,7 @@ async def test_entry_diagnostics( "latest_firmware": None, "mesh_role": "master", "model": "FRITZ!Box 7530 AX", + "unique_id": MOCK_MESH_MASTER_MAC.replace("6F:12", "XX:XX"), "update_available": False, "wan_link_properties": { "NewLayer1DownstreamMaxBitRate": 318557000, From acd906dfab0cd2a9b32eee5488967e74e8bea344 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 5 Mar 2022 00:19:17 +0000 Subject: [PATCH 0247/1054] [ci skip] Translation update --- .../dlna_dms/translations/zh-Hant.json | 2 +- .../components/fritz/translations/ca.json | 3 +- .../components/fritz/translations/de.json | 3 +- .../components/fritz/translations/el.json | 3 +- .../components/fritz/translations/et.json | 3 +- .../components/fritz/translations/id.json | 3 +- .../components/fritz/translations/it.json | 3 +- .../components/fritz/translations/no.json | 3 +- .../components/fritz/translations/pt-BR.json | 3 +- .../components/fritz/translations/ru.json | 3 +- .../components/fritz/translations/tr.json | 3 +- .../fritz/translations/zh-Hant.json | 3 +- .../components/moon/translations/bg.json | 13 ++++++++ .../components/moon/translations/ca.json | 13 ++++++++ .../components/moon/translations/de.json | 13 ++++++++ .../components/moon/translations/el.json | 13 ++++++++ .../components/moon/translations/es.json | 13 ++++++++ .../components/moon/translations/et.json | 13 ++++++++ .../components/moon/translations/id.json | 13 ++++++++ .../components/moon/translations/no.json | 13 ++++++++ .../components/moon/translations/pt-BR.json | 13 ++++++++ .../components/moon/translations/ru.json | 13 ++++++++ .../components/moon/translations/tr.json | 13 ++++++++ .../components/moon/translations/zh-Hant.json | 13 ++++++++ .../components/onewire/translations/bg.json | 12 ++++++++ .../components/onewire/translations/de.json | 26 ++++++++++++++++ .../components/onewire/translations/el.json | 26 ++++++++++++++++ .../components/onewire/translations/et.json | 26 ++++++++++++++++ .../components/onewire/translations/id.json | 26 ++++++++++++++++ .../components/onewire/translations/no.json | 26 ++++++++++++++++ .../components/onewire/translations/ru.json | 26 ++++++++++++++++ .../components/onewire/translations/tr.json | 30 +++++++++++++++++++ .../onewire/translations/zh-Hant.json | 26 ++++++++++++++++ .../rfxtrx/translations/zh-Hant.json | 2 +- .../components/sensibo/translations/bg.json | 8 ++++- .../components/sensibo/translations/id.json | 9 +++++- .../components/sensibo/translations/tr.json | 14 +++++++-- .../components/sleepiq/translations/tr.json | 10 ++++++- .../components/switch/translations/ca.json | 10 +++++++ .../components/switch/translations/de.json | 10 +++++++ .../components/switch/translations/en.json | 10 +++++++ .../components/switch/translations/it.json | 10 +++++++ .../components/tuya/translations/zh-Hant.json | 2 +- .../components/update/translations/bg.json | 3 ++ .../components/update/translations/et.json | 3 ++ .../components/update/translations/id.json | 3 ++ .../components/update/translations/no.json | 3 ++ .../components/update/translations/ru.json | 3 ++ .../components/update/translations/tr.json | 3 ++ .../update/translations/zh-Hant.json | 3 ++ 50 files changed, 502 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/moon/translations/bg.json create mode 100644 homeassistant/components/moon/translations/ca.json create mode 100644 homeassistant/components/moon/translations/de.json create mode 100644 homeassistant/components/moon/translations/el.json create mode 100644 homeassistant/components/moon/translations/es.json create mode 100644 homeassistant/components/moon/translations/et.json create mode 100644 homeassistant/components/moon/translations/id.json create mode 100644 homeassistant/components/moon/translations/no.json create mode 100644 homeassistant/components/moon/translations/pt-BR.json create mode 100644 homeassistant/components/moon/translations/ru.json create mode 100644 homeassistant/components/moon/translations/tr.json create mode 100644 homeassistant/components/moon/translations/zh-Hant.json create mode 100644 homeassistant/components/update/translations/bg.json create mode 100644 homeassistant/components/update/translations/et.json create mode 100644 homeassistant/components/update/translations/id.json create mode 100644 homeassistant/components/update/translations/no.json create mode 100644 homeassistant/components/update/translations/ru.json create mode 100644 homeassistant/components/update/translations/tr.json create mode 100644 homeassistant/components/update/translations/zh-Hant.json diff --git a/homeassistant/components/dlna_dms/translations/zh-Hant.json b/homeassistant/components/dlna_dms/translations/zh-Hant.json index 1dc212f19ee..404b9b29b9a 100644 --- a/homeassistant/components/dlna_dms/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dms/translations/zh-Hant.json @@ -16,7 +16,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", "title": "\u5df2\u767c\u73fe\u7684 DLNA DMA \u88dd\u7f6e" } } diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index 6df025c99bb..d39805a6302 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -10,7 +10,8 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3", "connection_error": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "upnp_not_configured": "Falta la configuraci\u00f3 UPnP al dispositiu." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index 3c391ba534c..ae36b3450ca 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -10,7 +10,8 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", "connection_error": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "upnp_not_configured": "Fehlende UPnP-Einstellungen auf dem Ger\u00e4t." }, "flow_title": "FRITZ!Box Tools: {name}", "step": { diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index bf49c098e1e..d514848040f 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -10,7 +10,8 @@ "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "upnp_not_configured": "\u039b\u03b5\u03af\u03c0\u03bf\u03c5\u03bd \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 UPnP \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 6562a89ab9a..2051e9f4e63 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -10,7 +10,8 @@ "already_in_progress": "Seadistamine on juba k\u00e4ivitatud", "cannot_connect": "\u00dchendamine nurjus", "connection_error": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamine nurjus" + "invalid_auth": "Tuvastamine nurjus", + "upnp_not_configured": "Puuduvad seadme UPnP-seaded." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json index a5ca1887256..816885b6391 100644 --- a/homeassistant/components/fritz/translations/id.json +++ b/homeassistant/components/fritz/translations/id.json @@ -10,7 +10,8 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", "connection_error": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid" + "invalid_auth": "Autentikasi tidak valid", + "upnp_not_configured": "Pengaturan UPnP pada perangkat tidak ada." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index 6e594556708..bf577223b6e 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -10,7 +10,8 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi", "connection_error": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida" + "invalid_auth": "Autenticazione non valida", + "upnp_not_configured": "Impostazioni UPnP mancanti sul dispositivo." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 7c7a9903b4a..6d6a805bab8 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -10,7 +10,8 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes", "connection_error": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning" + "invalid_auth": "Ugyldig godkjenning", + "upnp_not_configured": "Mangler UPnP-innstillinger p\u00e5 enheten." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/pt-BR.json b/homeassistant/components/fritz/translations/pt-BR.json index 7a27094b0c2..2170b34b1ee 100644 --- a/homeassistant/components/fritz/translations/pt-BR.json +++ b/homeassistant/components/fritz/translations/pt-BR.json @@ -10,7 +10,8 @@ "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "cannot_connect": "Falha ao conectar", "connection_error": "Falha ao conectar", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "upnp_not_configured": "Faltam configura\u00e7\u00f5es de UPnP no dispositivo." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index 414d7dbea6e..82530cca298 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -10,7 +10,8 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "upnp_not_configured": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UPnP \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index 29a200bea69..9a9cd3da6bf 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -10,7 +10,8 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131", "connection_error": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "upnp_not_configured": "Cihazda UPnP ayarlar\u0131 eksik." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index cb03aa8606c..7eeb3ae5e4b 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -10,7 +10,8 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_error": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "upnp_not_configured": "\u672a\u8a2d\u5b9a\u88dd\u7f6e UPnP \u8a2d\u5b9a\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/moon/translations/bg.json b/homeassistant/components/moon/translations/bg.json new file mode 100644 index 00000000000..71462a123f9 --- /dev/null +++ b/homeassistant/components/moon/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435\u0442\u043e?" + } + } + }, + "title": "\u041b\u0443\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ca.json b/homeassistant/components/moon/translations/ca.json new file mode 100644 index 00000000000..085de62df8c --- /dev/null +++ b/homeassistant/components/moon/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, + "title": "Lluna" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/de.json b/homeassistant/components/moon/translations/de.json new file mode 100644 index 00000000000..4d8d2d45284 --- /dev/null +++ b/homeassistant/components/moon/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + }, + "title": "Mond" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/el.json b/homeassistant/components/moon/translations/el.json new file mode 100644 index 00000000000..51cde4e9c65 --- /dev/null +++ b/homeassistant/components/moon/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + } + } + }, + "title": "\u03a6\u03b5\u03b3\u03b3\u03ac\u03c1\u03b9" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/es.json b/homeassistant/components/moon/translations/es.json new file mode 100644 index 00000000000..d23683ceb5d --- /dev/null +++ b/homeassistant/components/moon/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya configurado. Solo una configuraci\u00f3n es posible." + }, + "step": { + "user": { + "description": "\u00bfQuieres empezar la configuraci\u00f3n?" + } + } + }, + "title": "Luna" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/et.json b/homeassistant/components/moon/translations/et.json new file mode 100644 index 00000000000..28eccf25439 --- /dev/null +++ b/homeassistant/components/moon/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. Lubatud on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas alustada seadistamist?" + } + } + }, + "title": "Kuu" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/id.json b/homeassistant/components/moon/translations/id.json new file mode 100644 index 00000000000..42b208bfb7e --- /dev/null +++ b/homeassistant/components/moon/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + }, + "title": "Bulan" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/no.json b/homeassistant/components/moon/translations/no.json new file mode 100644 index 00000000000..c86dae4f615 --- /dev/null +++ b/homeassistant/components/moon/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + }, + "title": "M\u00e5ne" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pt-BR.json b/homeassistant/components/moon/translations/pt-BR.json new file mode 100644 index 00000000000..83022094ad0 --- /dev/null +++ b/homeassistant/components/moon/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Moon" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ru.json b/homeassistant/components/moon/translations/ru.json new file mode 100644 index 00000000000..90f93873205 --- /dev/null +++ b/homeassistant/components/moon/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "title": "\u041b\u0443\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/tr.json b/homeassistant/components/moon/translations/tr.json new file mode 100644 index 00000000000..0abcd94692c --- /dev/null +++ b/homeassistant/components/moon/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "title": "Ay" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/zh-Hant.json b/homeassistant/components/moon/translations/zh-Hant.json new file mode 100644 index 00000000000..c84da0f79f2 --- /dev/null +++ b/homeassistant/components/moon/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "title": "\u6708\u76f8" +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/bg.json b/homeassistant/components/onewire/translations/bg.json index 353a6523eed..e45a2db7197 100644 --- a/homeassistant/components/onewire/translations/bg.json +++ b/homeassistant/components/onewire/translations/bg.json @@ -19,5 +19,17 @@ } } } + }, + "options": { + "error": { + "device_not_selected": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435" + }, + "step": { + "device_selection": { + "data": { + "device_selection": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u043e\u0438\u0442\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index 2b0630db22c..a6347b12d75 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -22,5 +22,31 @@ "title": "1-Wire einrichten" } } + }, + "options": { + "error": { + "device_not_selected": "Zu konfigurierende Ger\u00e4te ausw\u00e4hlen" + }, + "step": { + "ack_no_options": { + "description": "Es gibt keine Optionen f\u00fcr die SysBus-Implementierung", + "title": "OneWire SysBus-Optionen" + }, + "configure_device": { + "data": { + "precision": "Sensorgenauigkeit" + }, + "description": "Sensorgenauigkeit f\u00fcr {sensor_id} ausw\u00e4hlen", + "title": "OneWire-Sensorpr\u00e4zision" + }, + "device_selection": { + "data": { + "clear_device_options": "Alle Ger\u00e4tekonfigurationen l\u00f6schen", + "device_selection": "Zu konfigurierende Ger\u00e4te ausw\u00e4hlen" + }, + "description": "W\u00e4hle die zu verarbeitenden Konfigurationsschritte aus", + "title": "OneWire-Ger\u00e4teoptionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json index 35b39838f5d..3d358decfe5 100644 --- a/homeassistant/components/onewire/translations/el.json +++ b/homeassistant/components/onewire/translations/el.json @@ -22,5 +22,31 @@ "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" + }, + "step": { + "ack_no_options": { + "description": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03bb\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 SysBus", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 SysBus OneWire" + }, + "configure_device": { + "data": { + "precision": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03b9\u03b1 {sensor_id}", + "title": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "\u0394\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae \u03cc\u03bb\u03c9\u03bd \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "device_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b8\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/et.json b/homeassistant/components/onewire/translations/et.json index 175c26cebb5..bb00ce8fbbc 100644 --- a/homeassistant/components/onewire/translations/et.json +++ b/homeassistant/components/onewire/translations/et.json @@ -22,5 +22,31 @@ "title": "Seadista 1-wire sidumine" } } + }, + "options": { + "error": { + "device_not_selected": "Vali seadistatav seade" + }, + "step": { + "ack_no_options": { + "description": "SysBusi rakendamiseks pole v\u00f5imalusi", + "title": "OneWire SysBusi valikud" + }, + "configure_device": { + "data": { + "precision": "Anduri t\u00e4psus" + }, + "description": "Vali {sensor_id} anduri t\u00e4psus.", + "title": "OneWire anduri t\u00e4psus" + }, + "device_selection": { + "data": { + "clear_device_options": "Eemalda k\u00f5ik seadmete s\u00e4tted", + "device_selection": "Vali seadistatav seade" + }, + "description": "Vali milliseid konfiguratsioonietappe t\u00f6\u00f6delda", + "title": "OneWire'i seadme valikud" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/id.json b/homeassistant/components/onewire/translations/id.json index 5de8e2eee3e..a1cde1b35bd 100644 --- a/homeassistant/components/onewire/translations/id.json +++ b/homeassistant/components/onewire/translations/id.json @@ -22,5 +22,31 @@ "title": "Siapkan 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Pilih perangkat untuk dikonfigurasi" + }, + "step": { + "ack_no_options": { + "description": "Tidak ada opsi untuk implementasi SysBus", + "title": "Opsi OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "Presisi Sensor" + }, + "description": "Pilih presisi sensor untuk {sensor_id}", + "title": "Presisi Sensor OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Hapus semua konfigurasi perangkat", + "device_selection": "Pilih perangkat untuk dikonfigurasi" + }, + "description": "Pilih langkah konfigurasi apa yang akan diproses", + "title": "Opsi Perangkat OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/no.json b/homeassistant/components/onewire/translations/no.json index b126349fb81..62bd0ac4634 100644 --- a/homeassistant/components/onewire/translations/no.json +++ b/homeassistant/components/onewire/translations/no.json @@ -22,5 +22,31 @@ "title": "Sett opp 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Velg enheter som skal konfigureres" + }, + "step": { + "ack_no_options": { + "description": "Det er ingen alternativer for SysBus-implementeringen", + "title": "OneWire SysBus-alternativer" + }, + "configure_device": { + "data": { + "precision": "Sensorpresisjon" + }, + "description": "Velg sensorpresisjon for {sensor_id}", + "title": "OneWire-sensorpresisjon" + }, + "device_selection": { + "data": { + "clear_device_options": "Fjern alle enhetskonfigurasjoner", + "device_selection": "Velg enheter som skal konfigureres" + }, + "description": "Velg hvilke konfigurasjonstrinn som skal behandles", + "title": "OneWire-enhetsalternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ru.json b/homeassistant/components/onewire/translations/ru.json index 4459607604b..4bc24f85f19 100644 --- a/homeassistant/components/onewire/translations/ru.json +++ b/homeassistant/components/onewire/translations/ru.json @@ -22,5 +22,31 @@ "title": "1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "step": { + "ack_no_options": { + "description": "\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 SysBus \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u0443\u044e \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430 {sensor_id}.", + "title": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430 OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0432\u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "device_selection": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u043a\u0430\u043a\u0438\u0435 \u0448\u0430\u0433\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index e3ed5f7ce45..6fea23cfc76 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -22,5 +22,35 @@ "title": "1-Wire'\u0131 kurun" } } + }, + "options": { + "error": { + "device_not_selected": "Yap\u0131land\u0131r\u0131lacak cihazlar\u0131 se\u00e7in" + }, + "step": { + "ack_no_options": { + "data": { + "one": "Bo\u015f", + "other": "Bo\u015f" + }, + "description": "SysBus uygulamas\u0131 i\u00e7in se\u00e7enek yok", + "title": "OneWire SysBus Se\u00e7enekleri" + }, + "configure_device": { + "data": { + "precision": "Sens\u00f6r Hassasiyeti" + }, + "description": "{sensor_id} i\u00e7in sens\u00f6r hassasiyetini se\u00e7in", + "title": "OneWire Sens\u00f6r Hassasiyeti" + }, + "device_selection": { + "data": { + "clear_device_options": "T\u00fcm cihaz yap\u0131land\u0131rmalar\u0131n\u0131 temizle", + "device_selection": "Yap\u0131land\u0131r\u0131lacak cihazlar\u0131 se\u00e7in" + }, + "description": "Hangi yap\u0131land\u0131rma ad\u0131mlar\u0131n\u0131n i\u015flenece\u011fini se\u00e7in", + "title": "OneWire Cihaz Se\u00e7enekleri" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index f9ee1b5e2c2..9b46a737939 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -22,5 +22,31 @@ "title": "\u8a2d\u5b9a 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + }, + "step": { + "ack_no_options": { + "description": "SysBus implementation \u6c92\u6709\u8a2d\u5b9a\u9078\u9805", + "title": "OneWire SysBus \u9078\u9805" + }, + "configure_device": { + "data": { + "precision": "\u611f\u6e2c\u5668\u7cbe\u6e96\u5ea6" + }, + "description": "\u8a2d\u5b9a {sensor_id} \u50b3\u611f\u5668\u7cbe\u6e96\u5ea6", + "title": "OneWire \u611f\u6e2c\u5668\u7cbe\u6e96\u5ea6" + }, + "device_selection": { + "data": { + "clear_device_options": "\u6e05\u9664\u6240\u6709\u88dd\u7f6e\u8a2d\u5b9a", + "device_selection": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u8a2d\u5b9a\u6b65\u9a5f\u9032\u884c", + "title": "OneWire \u88dd\u7f6e\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 8f4c9f2aa1a..c86701185cf 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -59,7 +59,7 @@ "data": { "automatic_add": "\u958b\u555f\u81ea\u52d5\u65b0\u589e", "debug": "\u958b\u555f\u9664\u932f", - "device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", + "device": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e", "protocols": "\u901a\u8a0a\u5354\u5b9a", "remove_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u522a\u9664" diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index ab5250b55f8..e600bafdb4a 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index 157bb18a674..479edabe69b 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", + "incorrect_api_key": "Kunci API tidak valid untuk akun yang dipilih", "invalid_auth": "Autentikasi tidak valid", "no_devices": "Tidak ada perangkat yang ditemukan", "no_username": "Tidak bisa mendapatkan nama pengguna" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + } + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index eb5e2eb4129..b08f683b200 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "incorrect_api_key": "Se\u00e7ilen hesap i\u00e7in ge\u00e7ersiz API anahtar\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_devices": "Hi\u00e7bir cihaz ke\u015ffedilmedi", + "no_username": "Kullan\u0131c\u0131 ad\u0131 al\u0131namad\u0131" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/sleepiq/translations/tr.json b/homeassistant/components/sleepiq/translations/tr.json index 153aa4126b0..abc741a6e17 100644 --- a/homeassistant/components/sleepiq/translations/tr.json +++ b/homeassistant/components/sleepiq/translations/tr.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "SleepIQ entegrasyonunun {username} hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/switch/translations/ca.json b/homeassistant/components/switch/translations/ca.json index c2406b7873e..49667b9f60a 100644 --- a/homeassistant/components/switch/translations/ca.json +++ b/homeassistant/components/switch/translations/ca.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitat d'interruptor" + }, + "description": "Selecciona l'interruptor del llum." + } + } + }, "device_automation": { "action_type": { "toggle": "Commuta {entity_name}", diff --git a/homeassistant/components/switch/translations/de.json b/homeassistant/components/switch/translations/de.json index 2b02f1304d2..12784fab206 100644 --- a/homeassistant/components/switch/translations/de.json +++ b/homeassistant/components/switch/translations/de.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch-Entit\u00e4t" + }, + "description": "W\u00e4hle den Schalter f\u00fcr den Lichtschalter aus." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name} umschalten", diff --git a/homeassistant/components/switch/translations/en.json b/homeassistant/components/switch/translations/en.json index 35f658e7e1f..ae04ed42520 100644 --- a/homeassistant/components/switch/translations/en.json +++ b/homeassistant/components/switch/translations/en.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch entity" + }, + "description": "Select the switch for the light switch." + } + } + }, "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", diff --git a/homeassistant/components/switch/translations/it.json b/homeassistant/components/switch/translations/it.json index 59a533c2a09..b71a46e48be 100644 --- a/homeassistant/components/switch/translations/it.json +++ b/homeassistant/components/switch/translations/it.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entit\u00e0 dell'interruttore" + }, + "description": "Seleziona l'interruttore per l'interruttore della luce." + } + } + }, "device_automation": { "action_type": { "toggle": "Attiva/Disattiva {entity_name}", diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index fe4d2ed2770..10247767161 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -71,7 +71,7 @@ "init": { "data": { "discovery_interval": "\u641c\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", - "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", + "list_devices": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" }, diff --git a/homeassistant/components/update/translations/bg.json b/homeassistant/components/update/translations/bg.json new file mode 100644 index 00000000000..661fdd4f830 --- /dev/null +++ b/homeassistant/components/update/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/et.json b/homeassistant/components/update/translations/et.json new file mode 100644 index 00000000000..7db9a98a507 --- /dev/null +++ b/homeassistant/components/update/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Uuenda" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/id.json b/homeassistant/components/update/translations/id.json new file mode 100644 index 00000000000..70f495575fa --- /dev/null +++ b/homeassistant/components/update/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Versi Baru" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/no.json b/homeassistant/components/update/translations/no.json new file mode 100644 index 00000000000..e98d60ab4fc --- /dev/null +++ b/homeassistant/components/update/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Oppdater" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/ru.json b/homeassistant/components/update/translations/ru.json new file mode 100644 index 00000000000..a2ee79efd15 --- /dev/null +++ b/homeassistant/components/update/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/tr.json b/homeassistant/components/update/translations/tr.json new file mode 100644 index 00000000000..30c3c90c437 --- /dev/null +++ b/homeassistant/components/update/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "G\u00fcncelle" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/zh-Hant.json b/homeassistant/components/update/translations/zh-Hant.json new file mode 100644 index 00000000000..46cdfb48f90 --- /dev/null +++ b/homeassistant/components/update/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u66f4\u65b0" +} \ No newline at end of file From c5f7e7d1b07d3a017eff521986f8bfa4fac8e151 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 5 Mar 2022 01:37:27 +0100 Subject: [PATCH 0248/1054] Refactor run app in SamsungTV (#67616) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 22 ++++++---- .../components/samsungtv/media_player.py | 18 +++++--- .../components/samsungtv/test_media_player.py | 42 ++++++++++++------- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 210bdd87cb3..fc699909e75 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -11,6 +11,7 @@ from samsungctl import Remote from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.async_rest import SamsungTVAsyncRest +from samsungtvws.command import SamsungTVCommand from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import WebSocketException @@ -128,7 +129,7 @@ class SamsungTVBridge(ABC): """Tells if the TV is on.""" @abstractmethod - async def async_send_key(self, key: str, key_type: str | None = None) -> None: + async def async_send_key(self, key: str) -> None: """Send a key to the tv and handles exceptions.""" @abstractmethod @@ -237,7 +238,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): pass return self._remote - async def async_send_key(self, key: str, key_type: str | None = None) -> None: + async def async_send_key(self, key: str) -> None: """Send the key using legacy protocol.""" await self.hass.async_add_executor_job(self._send_key, key) @@ -388,22 +389,25 @@ class SamsungTVWSBridge(SamsungTVBridge): return None - async def async_send_key(self, key: str, key_type: str | None = None) -> None: + async def async_launch_app(self, app_id: str) -> None: + """Send the launch_app command using websocket protocol.""" + await self._async_send_command(ChannelEmitCommand.launch_app(app_id)) + + async def async_send_key(self, key: str) -> None: """Send the key using websocket protocol.""" if key == "KEY_POWEROFF": key = "KEY_POWER" + await self._async_send_command(SendRemoteKey.click(key)) + + async def _async_send_command(self, command: SamsungTVCommand) -> None: + """Send the commands using websocket protocol.""" try: # recreate connection if connection was dead retry_count = 1 for _ in range(retry_count + 1): try: if remote := await self._async_get_remote(): - if key_type == "run_app": - await remote.send_command( - ChannelEmitCommand.launch_app(key) - ) - else: - await remote.send_command(SendRemoteKey.click(key)) + await remote.send_command(command) break except ( BrokenPipeError, diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index cb857a96afb..894e64dff17 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -173,12 +173,20 @@ class SamsungTVDevice(MediaPlayerEntity): if self._app_list is not None: self._attr_source_list.extend(self._app_list) - async def _async_send_key(self, key: str, key_type: str | None = None) -> None: + async def _async_launch_app(self, app_id: str) -> None: + """Send launch_app to the tv.""" + if self._power_off_in_progress(): + LOGGER.info("TV is powering off, not sending launch_app command") + return + assert isinstance(self._bridge, SamsungTVWSBridge) + await self._bridge.async_launch_app(app_id) + + async def _async_send_key(self, key: str) -> None: """Send a key to the tv and handles exceptions.""" if self._power_off_in_progress() and key != "KEY_POWEROFF": - LOGGER.info("TV is powering off, not sending command: %s", key) + LOGGER.info("TV is powering off, not sending key: %s", key) return - await self._bridge.async_send_key(key, key_type) + await self._bridge.async_send_key(key) def _power_off_in_progress(self) -> bool: return ( @@ -248,7 +256,7 @@ class SamsungTVDevice(MediaPlayerEntity): ) -> None: """Support changing a channel.""" if media_type == MEDIA_TYPE_APP: - await self._async_send_key(media_id, "run_app") + await self._async_launch_app(media_id) return if media_type != MEDIA_TYPE_CHANNEL: @@ -284,7 +292,7 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_select_source(self, source: str) -> None: """Select input source.""" if self._app_list and source in self._app_list: - await self._async_send_key(self._app_list[source], "run_app") + await self._async_launch_app(self._app_list[source]) return if source in SOURCES: diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2969f2ba2d0..913e11e237c 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -624,29 +624,41 @@ async def test_device_class(hass: HomeAssistant) -> None: assert state.attributes[ATTR_DEVICE_CLASS] is MediaPlayerDeviceClass.TV.value -async def test_turn_off_websocket(hass: HomeAssistant, remotews: Mock) -> None: +async def test_turn_off_websocket( + hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture +) -> None: """Test for turn_off.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[OSError("Boom"), DEFAULT_MOCK], ): await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command.reset_mock() - assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - # key called - assert remotews.send_command.call_count == 1 - command = remotews.send_command.call_args_list[0].args[0] - assert isinstance(command, SendRemoteKey) - assert command.params["DataOfCmd"] == "KEY_POWER" + remotews.send_command.reset_mock() - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - # key not called - assert remotews.send_command.call_count == 1 + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key called + assert remotews.send_command.call_count == 1 + command = remotews.send_command.call_args_list[0].args[0] + assert isinstance(command, SendRemoteKey) + assert command.params["DataOfCmd"] == "KEY_POWER" + + # commands not sent : power off in progress + remotews.send_command.reset_mock() + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert "TV is powering off, not sending key: KEY_VOLUP" in caplog.text + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, + True, + ) + assert "TV is powering off, not sending launch_app command" in caplog.text + remotews.send_command.assert_not_called() async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: From cdb463ea55dadf51f78d2f351cc1311459bb6007 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Mar 2022 08:37:58 +0100 Subject: [PATCH 0249/1054] Refactor android_ip_webcam (#67664) * Add native camera platform * Remove double mjpeg platform * Fix docstring * Remove not needed update --- .../components/android_ip_webcam/__init__.py | 30 ++++++++++--- .../components/android_ip_webcam/camera.py | 44 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/camera.py diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 67bb00f441d..cc24e6f4182 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,4 +1,6 @@ """Support for Android IP Webcam.""" +from __future__ import annotations + import asyncio from datetime import timedelta @@ -10,7 +12,6 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, - CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, @@ -194,9 +195,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_ipcamera(cam_config): """Set up an IP camera.""" host = cam_config[CONF_HOST] - username = cam_config.get(CONF_USERNAME) - password = cam_config.get(CONF_PASSWORD) - name = cam_config[CONF_NAME] + username: str | None = cam_config.get(CONF_USERNAME) + password: str | None = cam_config.get(CONF_PASSWORD) + name: str = cam_config[CONF_NAME] interval = cam_config[CONF_SCAN_INTERVAL] switches = cam_config.get(CONF_SWITCHES) sensors = cam_config.get(CONF_SENSORS) @@ -238,17 +239,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: webcams[host] = cam mjpeg_camera = { - CONF_PLATFORM: "mjpeg", CONF_MJPEG_URL: cam.mjpeg_url, CONF_STILL_IMAGE_URL: cam.image_url, - CONF_NAME: name, } if username and password: mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password}) + # Remove incorrect config entry setup via mjpeg platform discovery. + mjpeg_config_entry = next( + ( + config_entry + for config_entry in hass.config_entries.async_entries("mjpeg") + if all( + config_entry.options.get(key) == val + for key, val in mjpeg_camera.items() + ) + ), + None, + ) + if mjpeg_config_entry: + await hass.config_entries.async_remove(mjpeg_config_entry.entry_id) + + mjpeg_camera[CONF_NAME] = name + hass.async_create_task( discovery.async_load_platform( - hass, Platform.CAMERA, "mjpeg", mjpeg_camera, config + hass, Platform.CAMERA, DOMAIN, mjpeg_camera, config ) ) diff --git a/homeassistant/components/android_ip_webcam/camera.py b/homeassistant/components/android_ip_webcam/camera.py new file mode 100644 index 00000000000..de1223c7f5f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/camera.py @@ -0,0 +1,44 @@ +"""Support for Android IP Webcam Cameras.""" +from __future__ import annotations + +from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging +from homeassistant.const import HTTP_BASIC_AUTHENTICATION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the IP Webcam camera.""" + if discovery_info is None: + return + + filter_urllib3_logging() + async_add_entities([IPWebcamCamera(**discovery_info)]) + + +class IPWebcamCamera(MjpegCamera): + """Representation of a IP Webcam camera.""" + + def __init__( + self, + name: str, + mjpeg_url: str, + still_image_url: str, + username: str | None = None, + password: str = "", + ) -> None: + """Initialize the camera.""" + super().__init__( + name=name, + mjpeg_url=mjpeg_url, + still_image_url=still_image_url, + authentication=HTTP_BASIC_AUTHENTICATION, + username=username, + password=password, + ) From 9632cbeffa8440f68784cc05b63dac873ecb66f0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 5 Mar 2022 03:00:31 -0500 Subject: [PATCH 0250/1054] Improve zwave_js custom triggers and services (#67461) * Improve zwave_js custom triggers and services * Switch from pop to get * Support string boolean values * refactor and add coverage * comments and additional assertions --- homeassistant/components/zwave_js/api.py | 2 +- .../components/zwave_js/config_validation.py | 41 +++++++++ homeassistant/components/zwave_js/const.py | 27 ------ .../components/zwave_js/device_action.py | 2 +- .../components/zwave_js/device_condition.py | 2 +- .../components/zwave_js/device_trigger.py | 9 +- homeassistant/components/zwave_js/helpers.py | 44 +++++++++- homeassistant/components/zwave_js/services.py | 88 ++++++++----------- .../components/zwave_js/triggers/event.py | 27 +++--- .../zwave_js/triggers/value_updated.py | 44 ++++------ .../zwave_js/test_config_validation.py | 26 ++++++ tests/components/zwave_js/test_services.py | 18 +++- tests/components/zwave_js/test_trigger.py | 27 ++++++ 13 files changed, 222 insertions(+), 135 deletions(-) create mode 100644 homeassistant/components/zwave_js/config_validation.py create mode 100644 tests/components/zwave_js/test_config_validation.py diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 4e68cc2e2dd..0e947de982b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -60,8 +60,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .config_validation import BITMASK_SCHEMA from .const import ( - BITMASK_SCHEMA, CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN, diff --git a/homeassistant/components/zwave_js/config_validation.py b/homeassistant/components/zwave_js/config_validation.py new file mode 100644 index 00000000000..9fc502bdafb --- /dev/null +++ b/homeassistant/components/zwave_js/config_validation.py @@ -0,0 +1,41 @@ +"""Config validation for the Z-Wave JS integration.""" +from typing import Any + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +# Validates that a bitmask is provided in hex form and converts it to decimal +# int equivalent since that's what the library uses +BITMASK_SCHEMA = vol.All( + cv.string, + vol.Lower, + vol.Match( + r"^(0x)?[0-9a-f]+$", + msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)", + ), + lambda value: int(value, 16), +) + + +def boolean(value: Any) -> bool: + """Validate and coerce a boolean value.""" + if isinstance(value, bool): + return value + if isinstance(value, str): + value = value.lower().strip() + if value in ("true", "yes", "on", "enable"): + return True + if value in ("false", "no", "off", "disable"): + return False + raise vol.Invalid(f"invalid boolean value {value}") + + +VALUE_SCHEMA = vol.Any( + boolean, + vol.Coerce(int), + vol.Coerce(float), + BITMASK_SCHEMA, + cv.string, + dict, +) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 8f6fada2106..d6d63487b8a 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -1,10 +1,6 @@ """Constants for the Z-Wave JS integration.""" import logging -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - CONF_ADDON_DEVICE = "device" CONF_ADDON_EMULATE_HARDWARE = "emulate_hardware" CONF_ADDON_LOG_LEVEL = "log_level" @@ -117,26 +113,3 @@ ENTITY_DESC_KEY_TEMPERATURE = "temperature" ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature" ENTITY_DESC_KEY_MEASUREMENT = "measurement" ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" - -# Schema Constants - -# Validates that a bitmask is provided in hex form and converts it to decimal -# int equivalent since that's what the library uses -BITMASK_SCHEMA = vol.All( - cv.string, - vol.Lower, - vol.Match( - r"^(0x)?[0-9a-f]+$", - msg="Must provide an integer (e.g. 255) or a bitmask in hex form (e.g. 0xff)", - ), - lambda value: int(value, 16), -) - -VALUE_SCHEMA = vol.Any( - bool, - vol.Coerce(int), - vol.Coerce(float), - BITMASK_SCHEMA, - cv.string, - dict, -) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index b81d675e6fd..7e5e8c6c78d 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -29,6 +29,7 @@ from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from .config_validation import VALUE_SCHEMA from .const import ( ATTR_COMMAND_CLASS, ATTR_CONFIG_PARAMETER, @@ -48,7 +49,6 @@ from .const import ( SERVICE_SET_CONFIG_PARAMETER, SERVICE_SET_LOCK_USERCODE, SERVICE_SET_VALUE, - VALUE_SCHEMA, ) from .device_automation_helpers import ( CONF_SUBTYPE, diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 8bb151199d7..c70371d6f8a 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -16,6 +16,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from .config_validation import VALUE_SCHEMA from .const import ( ATTR_COMMAND_CLASS, ATTR_ENDPOINT, @@ -23,7 +24,6 @@ from .const import ( ATTR_PROPERTY_KEY, ATTR_VALUE, DOMAIN, - VALUE_SCHEMA, ) from .device_automation_helpers import ( CONF_SUBTYPE, diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 0615668cccd..89379f9a953 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -32,6 +32,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.typing import ConfigType from . import trigger +from .config_validation import VALUE_SCHEMA from .const import ( ATTR_COMMAND_CLASS, ATTR_DATA_TYPE, @@ -80,14 +81,6 @@ CONFIG_PARAMETER_VALUE_UPDATED = f"{VALUE_UPDATED_PLATFORM_TYPE}.config_paramete VALUE_VALUE_UPDATED = f"{VALUE_UPDATED_PLATFORM_TYPE}.value" NODE_STATUS = "state.node_status" -VALUE_SCHEMA = vol.Any( - bool, - vol.Coerce(int), - vol.Coerce(float), - cv.boolean, - cv.string, -) - NOTIFICATION_EVENT_CC_MAPPINGS = ( (ENTRY_CONTROL_NOTIFICATION, CommandClass.ENTRY_CONTROL), diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 05df480a487..a84ddee300f 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -14,9 +14,16 @@ from zwave_js_server.model.value import ( get_value_id, ) +from homeassistant.components.group import expand_entity_ids from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_TYPE, __version__ as HA_VERSION +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + CONF_TYPE, + __version__ as HA_VERSION, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -30,6 +37,7 @@ from .const import ( CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN, + LOGGER, ) @@ -221,6 +229,40 @@ def async_get_nodes_from_area_id( return nodes +@callback +def async_get_nodes_from_targets( + hass: HomeAssistant, + val: dict[str, Any], + ent_reg: er.EntityRegistry | None = None, + dev_reg: dr.DeviceRegistry | None = None, +) -> set[ZwaveNode]: + """ + Get nodes for all targets. + + Supports entity_id with group expansion, area_id, and device_id. + """ + nodes: set[ZwaveNode] = set() + # Convert all entity IDs to nodes + for entity_id in expand_entity_ids(hass, val.get(ATTR_ENTITY_ID, [])): + try: + nodes.add(async_get_node_from_entity_id(hass, entity_id, ent_reg, dev_reg)) + except ValueError as err: + LOGGER.warning(err.args[0]) + + # Convert all area IDs to nodes + for area_id in val.get(ATTR_AREA_ID, []): + nodes.update(async_get_nodes_from_area_id(hass, area_id, ent_reg, dev_reg)) + + # Convert all device IDs to nodes + for device_id in val.get(ATTR_DEVICE_ID, []): + try: + nodes.add(async_get_node_from_device_id(hass, device_id, dev_reg)) + except ValueError as err: + LOGGER.warning(err.args[0]) + + return nodes + + def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveValue: """Get a Z-Wave JS Value from a config.""" endpoint = None diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 767516cc17c..2d31bed108f 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -25,11 +25,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from . import const -from .helpers import ( - async_get_node_from_device_id, - async_get_node_from_entity_id, - async_get_nodes_from_area_id, -) +from .config_validation import BITMASK_SCHEMA, VALUE_SCHEMA +from .helpers import async_get_nodes_from_targets _LOGGER = logging.getLogger(__name__) @@ -80,38 +77,16 @@ class ZWaveServices: @callback def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]: """Get nodes set from service data.""" - nodes: set[ZwaveNode] = set() - # Convert all entity IDs to nodes - for entity_id in expand_entity_ids(self._hass, val.pop(ATTR_ENTITY_ID, [])): - try: - nodes.add( - async_get_node_from_entity_id( - self._hass, entity_id, self._ent_reg, self._dev_reg - ) - ) - except ValueError as err: - const.LOGGER.warning(err.args[0]) + val[const.ATTR_NODES] = async_get_nodes_from_targets( + self._hass, val, self._ent_reg, self._dev_reg + ) + return val - # Convert all area IDs to nodes - for area_id in val.pop(ATTR_AREA_ID, []): - nodes.update( - async_get_nodes_from_area_id( - self._hass, area_id, self._ent_reg, self._dev_reg - ) - ) - - # Convert all device IDs to nodes - for device_id in val.pop(ATTR_DEVICE_ID, []): - try: - nodes.add( - async_get_node_from_device_id( - self._hass, device_id, self._dev_reg - ) - ) - except ValueError as err: - const.LOGGER.warning(err.args[0]) - - val[const.ATTR_NODES] = nodes + @callback + def has_at_least_one_node(val: dict[str, Any]) -> dict[str, Any]: + """Validate that at least one node is specified.""" + if not val.get(const.ATTR_NODES): + raise vol.Invalid(f"No {const.DOMAIN} nodes found for given targets") return val @callback @@ -120,6 +95,9 @@ class ZWaveServices: nodes: set[ZwaveNode] = val[const.ATTR_NODES] broadcast: bool = val[const.ATTR_BROADCAST] + if not broadcast: + has_at_least_one_node(val) + # User must specify a node if they are attempting a broadcast and have more # than one zwave-js network. if ( @@ -150,12 +128,20 @@ class ZWaveServices: def validate_entities(val: dict[str, Any]) -> dict[str, Any]: """Validate entities exist and are from the zwave_js platform.""" val[ATTR_ENTITY_ID] = expand_entity_ids(self._hass, val[ATTR_ENTITY_ID]) + invalid_entities = [] for entity_id in val[ATTR_ENTITY_ID]: entry = self._ent_reg.async_get(entity_id) if entry is None or entry.platform != const.DOMAIN: - raise vol.Invalid( - f"Entity {entity_id} is not a valid {const.DOMAIN} entity." + const.LOGGER.info( + "Entity %s is not a valid %s entity.", entity_id, const.DOMAIN ) + invalid_entities.append(entity_id) + + # Remove invalid entities + val[ATTR_ENTITY_ID] = list(set(val[ATTR_ENTITY_ID]) - set(invalid_entities)) + + if not val[ATTR_ENTITY_ID]: + raise vol.Invalid(f"No {const.DOMAIN} entities found in service call") return val @@ -177,10 +163,10 @@ class ZWaveServices: vol.Coerce(int), cv.string ), vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA + vol.Coerce(int), BITMASK_SCHEMA ), vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA, cv.string + vol.Coerce(int), BITMASK_SCHEMA, cv.string ), }, cv.has_at_least_one_key( @@ -188,6 +174,7 @@ class ZWaveServices: ), parameter_name_does_not_need_bitmask, get_nodes_from_service_data, + has_at_least_one_node, ), ), ) @@ -211,10 +198,8 @@ class ZWaveServices: vol.Coerce(int), { vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA, cv.string - ): vol.Any( - vol.Coerce(int), const.BITMASK_SCHEMA, cv.string - ) + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string) }, ), }, @@ -222,6 +207,7 @@ class ZWaveServices: ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID ), get_nodes_from_service_data, + has_at_least_one_node, ), ), ) @@ -265,16 +251,15 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): const.VALUE_SCHEMA, + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean, - vol.Optional(const.ATTR_OPTIONS): { - cv.string: const.VALUE_SCHEMA - }, + vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA}, }, cv.has_at_least_one_key( ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID ), get_nodes_from_service_data, + has_at_least_one_node, ), ), ) @@ -302,10 +287,8 @@ class ZWaveServices: vol.Coerce(int), str ), vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): const.VALUE_SCHEMA, - vol.Optional(const.ATTR_OPTIONS): { - cv.string: const.VALUE_SCHEMA - }, + vol.Required(const.ATTR_VALUE): VALUE_SCHEMA, + vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA}, }, vol.Any( cv.has_at_least_one_key( @@ -338,6 +321,7 @@ class ZWaveServices: ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID ), get_nodes_from_service_data, + has_at_least_one_node, ), ), ) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 110cd21294f..fd46c89832b 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -20,13 +20,13 @@ from homeassistant.components.zwave_js.const import ( ATTR_EVENT_DATA, ATTR_EVENT_SOURCE, ATTR_NODE_ID, + ATTR_NODES, ATTR_PARTIAL_DICT_MATCH, DATA_CLIENT, DOMAIN, ) from homeassistant.components.zwave_js.helpers import ( - async_get_node_from_device_id, - async_get_node_from_entity_id, + async_get_nodes_from_targets, get_device_id, get_home_and_node_id_from_device_entry, ) @@ -111,6 +111,13 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) + if config[ATTR_EVENT_SOURCE] == "node": + config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) + if not config[ATTR_NODES]: + raise vol.Invalid( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) + if ATTR_CONFIG_ENTRY_ID not in config: return config @@ -133,21 +140,7 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = set() - if ATTR_DEVICE_ID in config: - nodes.update( - { - async_get_node_from_device_id(hass, device_id) - for device_id in config[ATTR_DEVICE_ID] - } - ) - if ATTR_ENTITY_ID in config: - nodes.update( - { - async_get_node_from_entity_id(hass, entity_id) - for entity_id in config[ATTR_ENTITY_ID] - } - ) + nodes: set[Node] = config.get(ATTR_NODES, {}) event_source = config[ATTR_EVENT_SOURCE] event_name = config[ATTR_EVENT] diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 71223c4ef1e..8a0b287c26b 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -20,6 +20,7 @@ from homeassistant.components.zwave_js.const import ( ATTR_CURRENT_VALUE_RAW, ATTR_ENDPOINT, ATTR_NODE_ID, + ATTR_NODES, ATTR_PREVIOUS_VALUE, ATTR_PREVIOUS_VALUE_RAW, ATTR_PROPERTY, @@ -29,8 +30,7 @@ from homeassistant.components.zwave_js.const import ( DOMAIN, ) from homeassistant.components.zwave_js.helpers import ( - async_get_node_from_device_id, - async_get_node_from_entity_id, + async_get_nodes_from_targets, get_device_id, ) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL @@ -38,20 +38,14 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType +from ..config_validation import VALUE_SCHEMA + # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" ATTR_FROM = "from" ATTR_TO = "to" -VALUE_SCHEMA = vol.Any( - bool, - vol.Coerce(int), - vol.Coerce(float), - cv.boolean, - cv.string, -) - TRIGGER_SCHEMA = vol.All( cv.TRIGGER_BASE_SCHEMA.extend( { @@ -76,6 +70,20 @@ TRIGGER_SCHEMA = vol.All( ) +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) + if not config[ATTR_NODES]: + raise vol.Invalid( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) + return config + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -85,21 +93,7 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = set() - if ATTR_DEVICE_ID in config: - nodes.update( - { - async_get_node_from_device_id(hass, device_id) - for device_id in config.get(ATTR_DEVICE_ID, []) - } - ) - if ATTR_ENTITY_ID in config: - nodes.update( - { - async_get_node_from_entity_id(hass, entity_id) - for entity_id in config.get(ATTR_ENTITY_ID, []) - } - ) + nodes: set[Node] = config[ATTR_NODES] from_value = config[ATTR_FROM] to_value = config[ATTR_TO] diff --git a/tests/components/zwave_js/test_config_validation.py b/tests/components/zwave_js/test_config_validation.py new file mode 100644 index 00000000000..5a390ff0290 --- /dev/null +++ b/tests/components/zwave_js/test_config_validation.py @@ -0,0 +1,26 @@ +"""Test the Z-Wave JS config validation helpers.""" +import pytest +import voluptuous as vol + +from homeassistant.components.zwave_js.config_validation import boolean + + +def test_boolean_validation(): + """Test boolean config validator.""" + # test bool + assert boolean(True) + assert not boolean(False) + # test strings + assert boolean("TRUE") + assert not boolean("FALSE") + assert boolean("ON") + assert not boolean("NO") + # ensure 1's and 0's don't get converted to bool + with pytest.raises(vol.Invalid): + boolean("1") + with pytest.raises(vol.Invalid): + boolean("0") + with pytest.raises(vol.Invalid): + boolean(1) + with pytest.raises(vol.Invalid): + boolean(0) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 571190bd35c..09189ad9230 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -498,6 +498,20 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): client.async_send_command.reset_mock() + # Test setting config parameter with no valid nodes raises Exception + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: "sensor.fake", + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_PARAMETER_BITMASK: 1, + ATTR_CONFIG_VALUE: 1, + }, + blocking=True, + ) + async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration): """Test the bulk_set_partial_config_parameters service.""" @@ -1345,8 +1359,8 @@ async def test_multicast_set_value( diff_network_node.client.driver.controller.home_id.return_value = "diff_home_id" with pytest.raises(vol.MultipleInvalid), patch( - "homeassistant.components.zwave_js.services.async_get_node_from_device_id", - return_value=diff_network_node, + "homeassistant.components.zwave_js.helpers.async_get_node_from_device_id", + side_effect=(climate_danfoss_lc_13, diff_network_node), ): await hass.services.async_call( DOMAIN, diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 5dbeff87a54..45de09e8b17 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -1,6 +1,8 @@ """The tests for Z-Wave JS automation triggers.""" from unittest.mock import AsyncMock, patch +import pytest +import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.event import Event from zwave_js_server.model.node import Node @@ -708,3 +710,28 @@ async def test_async_validate_trigger_config(hass): mock_platform.async_validate_trigger_config.return_value = {} await async_validate_trigger_config(hass, {}) mock_platform.async_validate_trigger_config.assert_awaited() + + +async def test_invalid_trigger_configs(hass): + """Test invalid trigger configs.""" + with pytest.raises(vol.Invalid): + await async_validate_trigger_config( + hass, + { + "platform": f"{DOMAIN}.event", + "entity_id": "fake.entity", + "event_source": "node", + "event": "value updated", + }, + ) + + with pytest.raises(vol.Invalid): + await async_validate_trigger_config( + hass, + { + "platform": f"{DOMAIN}.value_updated", + "entity_id": "fake.entity", + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + ) From 848406091527363cc548b91b534727b9d6af2418 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sat, 5 Mar 2022 01:10:39 -0700 Subject: [PATCH 0251/1054] Add Hyperion device configuration URL (#67495) * Adding device configuration url * bump version --- homeassistant/components/hyperion/camera.py | 1 + homeassistant/components/hyperion/light.py | 1 + homeassistant/components/hyperion/manifest.json | 2 +- homeassistant/components/hyperion/switch.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 4b6492559ea..a9d78a256d6 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -254,6 +254,7 @@ class HyperionCamera(Camera): manufacturer=HYPERION_MANUFACTURER_NAME, model=HYPERION_MODEL_NAME, name=self._instance_name, + configuration_url=self._client.remote_url, ) diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 753e41018d9..8b809bdec8a 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -244,6 +244,7 @@ class HyperionBaseLight(LightEntity): manufacturer=HYPERION_MANUFACTURER_NAME, model=HYPERION_MODEL_NAME, name=self._instance_name, + configuration_url=self._client.remote_url, ) def _get_option(self, key: str) -> Any: diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 8a886053361..223c001a53c 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -5,7 +5,7 @@ "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": ["hyperion-py==0.7.4"], + "requirements": ["hyperion-py==0.7.5"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index ac853a32909..bf4958d845c 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -191,6 +191,7 @@ class HyperionComponentSwitch(SwitchEntity): manufacturer=HYPERION_MANUFACTURER_NAME, model=HYPERION_MODEL_NAME, name=self._instance_name, + configuration_url=self._client.remote_url, ) async def _async_send_set_component(self, value: bool) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 059645aecae..cc5ecc31845 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ huisbaasje-client==0.1.0 hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.7.4 +hyperion-py==0.7.5 # homeassistant.components.iammeter iammeter==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff584c8a83c..9fba86e717c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -578,7 +578,7 @@ huawei-lte-api==1.4.18 huisbaasje-client==0.1.0 # homeassistant.components.hyperion -hyperion-py==0.7.4 +hyperion-py==0.7.5 # homeassistant.components.iaqualink iaqualink==0.4.1 From ca32c38859f5a4f27fca006be7c6e5aae9c0e342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 5 Mar 2022 21:13:30 +0200 Subject: [PATCH 0252/1054] Upgrade bandit to 1.7.4 (#67669) No new issues flagged. https://github.com/PyCQA/bandit/releases/tag/1.7.1 https://github.com/PyCQA/bandit/releases/tag/1.7.2 https://github.com/PyCQA/bandit/releases/tag/1.7.3 https://github.com/PyCQA/bandit/releases/tag/1.7.4 --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cdd28cd6e9..66e794f95fa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit - rev: 1.7.0 + rev: 1.7.4 hooks: - id: bandit args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ca7828267b6..d296c2be72d 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,6 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -bandit==1.7.0 +bandit==1.7.4 black==22.1.0 codespell==2.1.0 flake8-comprehensions==3.7.0 From af7670a5a53c0e3dca25d606cd6856e1fd255b8d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Mar 2022 22:37:44 +0100 Subject: [PATCH 0253/1054] Add base entity for Sensibo (#67696) --- .coveragerc | 1 + homeassistant/components/sensibo/climate.py | 57 ++++----------- homeassistant/components/sensibo/entity.py | 77 +++++++++++++++++++++ homeassistant/components/sensibo/number.py | 39 ++--------- 4 files changed, 96 insertions(+), 78 deletions(-) create mode 100644 homeassistant/components/sensibo/entity.py diff --git a/.coveragerc b/.coveragerc index 4144b38cc0f..178c130ac13 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1015,6 +1015,7 @@ omit = homeassistant/components/sensibo/climate.py homeassistant/components/sensibo/coordinator.py homeassistant/components/sensibo/diagnostics.py + homeassistant/components/sensibo/entity.py homeassistant/components/sensibo/number.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index eeb904cf6a8..cd383367dc4 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,11 +1,6 @@ """Support for Sensibo wifi-enabled home thermostats.""" from __future__ import annotations -import asyncio - -from aiohttp.client_exceptions import ClientConnectionError -import async_timeout -from pysensibo.exceptions import AuthenticationError, SensiboError import voluptuous as vol from homeassistant.components.climate import ( @@ -36,15 +31,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.temperature import convert as convert_temperature -from .const import ALL, DOMAIN, LOGGER, TIMEOUT +from .const import ALL, DOMAIN, LOGGER from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboBaseEntity SERVICE_ASSUME_STATE = "assume_state" @@ -126,17 +119,14 @@ async def async_setup_entry( ) -class SensiboClimate(CoordinatorEntity, ClimateEntity): +class SensiboClimate(SensiboBaseEntity, ClimateEntity): """Representation of a Sensibo device.""" - coordinator: SensiboDataUpdateCoordinator - def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str ) -> None: """Initiate SensiboClimate.""" - super().__init__(coordinator) - self._client = coordinator.client + super().__init__(coordinator, device_id) self._attr_unique_id = device_id self._attr_name = coordinator.data[device_id]["name"] self._attr_temperature_unit = ( @@ -146,17 +136,6 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): ) self._attr_supported_features = self.get_features() self._attr_precision = PRECISION_TENTHS - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, - name=coordinator.data[device_id]["name"], - connections={(CONNECTION_NETWORK_MAC, coordinator.data[device_id]["mac"])}, - manufacturer="Sensibo", - configuration_url="https://home.sensibo.com/", - model=coordinator.data[device_id]["model"], - sw_version=coordinator.data[device_id]["fw_ver"], - hw_version=coordinator.data[device_id]["fw_type"], - suggested_area=coordinator.data[device_id]["name"], - ) def get_features(self) -> int: """Get supported features.""" @@ -309,26 +288,14 @@ class SensiboClimate(CoordinatorEntity, ClimateEntity): self, name: str, value: str | int | bool, assumed_state: bool = False ) -> None: """Set AC state.""" - result = {} - try: - async with async_timeout.timeout(TIMEOUT): - result = await self._client.async_set_ac_state_property( - self.unique_id, - name, - value, - self.coordinator.data[self.unique_id]["ac_states"], - assumed_state, - ) - except ( - ClientConnectionError, - asyncio.TimeoutError, - AuthenticationError, - SensiboError, - ) as err: - raise HomeAssistantError( - f"Failed to set AC state for device {self.name} to Sensibo servers: {err}" - ) from err - LOGGER.debug("Result: %s", result) + params = { + "name": name, + "value": value, + "ac_states": self.coordinator.data[self.unique_id]["ac_states"], + "assumed_state": assumed_state, + } + result = await self.async_send_command("set_ac_state", params) + if result["result"]["status"] == "Success": self.coordinator.data[self.unique_id][AC_STATE_TO_DATA[name]] = value self.async_write_ha_state() diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py new file mode 100644 index 00000000000..22d29d37ead --- /dev/null +++ b/homeassistant/components/sensibo/entity.py @@ -0,0 +1,77 @@ +"""Base entity for Sensibo integration.""" +from __future__ import annotations + +from typing import Any + +import async_timeout + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT +from .coordinator import SensiboDataUpdateCoordinator + + +class SensiboBaseEntity(CoordinatorEntity): + """Representation of a Sensibo numbers.""" + + coordinator: SensiboDataUpdateCoordinator + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator) + self._device_id = device_id + self._client = coordinator.client + device = coordinator.data[device_id] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device["id"])}, + name=device["name"], + connections={(CONNECTION_NETWORK_MAC, device["mac"])}, + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=device["model"], + sw_version=device["fw_ver"], + hw_version=device["fw_type"], + suggested_area=device["name"], + ) + + async def async_send_command( + self, command: str, params: dict[str, Any] + ) -> dict[str, Any]: + """Send command to Sensibo api.""" + try: + async with async_timeout.timeout(TIMEOUT): + result = await self.async_send_api_call(command, params) + except SENSIBO_ERRORS as err: + raise HomeAssistantError( + f"Failed to send command {command} for device {self.name} to Sensibo servers: {err}" + ) from err + + LOGGER.debug("Result: %s", result) + return result + + async def async_send_api_call( + self, command: str, params: dict[str, Any] + ) -> dict[str, Any]: + """Send api call.""" + result: dict[str, Any] = {"status": None} + if command == "set_calibration": + result = await self._client.async_set_calibration( + self._device_id, + params["data"], + ) + if command == "set_ac_state": + result = await self._client.async_set_ac_state_property( + self._device_id, + params["name"], + params["value"], + params["ac_states"], + params["assumed_state"], + ) + return result diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 9e531249bf7..c775b8e6ffa 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -3,19 +3,16 @@ from __future__ import annotations from dataclasses import dataclass -import async_timeout - from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT +from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboBaseEntity @dataclass @@ -73,10 +70,9 @@ async def async_setup_entry( ) -class SensiboNumber(CoordinatorEntity, NumberEntity): +class SensiboNumber(SensiboBaseEntity, NumberEntity): """Representation of a Sensibo numbers.""" - coordinator: SensiboDataUpdateCoordinator entity_description: SensiboNumberEntityDescription def __init__( @@ -86,25 +82,12 @@ class SensiboNumber(CoordinatorEntity, NumberEntity): entity_description: SensiboNumberEntityDescription, ) -> None: """Initiate Sensibo Number.""" - super().__init__(coordinator) + super().__init__(coordinator, device_id) self.entity_description = entity_description - self._device_id = device_id - self._client = coordinator.client self._attr_unique_id = f"{device_id}-{entity_description.key}" self._attr_name = ( f"{coordinator.data[device_id]['name']} {entity_description.name}" ) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, - name=coordinator.data[device_id]["name"], - connections={(CONNECTION_NETWORK_MAC, coordinator.data[device_id]["mac"])}, - manufacturer="Sensibo", - configuration_url="https://home.sensibo.com/", - model=coordinator.data[device_id]["model"], - sw_version=coordinator.data[device_id]["fw_ver"], - hw_version=coordinator.data[device_id]["fw_type"], - suggested_area=coordinator.data[device_id]["name"], - ) @property def value(self) -> float: @@ -114,17 +97,7 @@ class SensiboNumber(CoordinatorEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set value for calibration.""" data = {self.entity_description.remote_key: value} - try: - async with async_timeout.timeout(TIMEOUT): - result = await self._client.async_set_calibration( - self._device_id, - data, - ) - except SENSIBO_ERRORS as err: - raise HomeAssistantError( - f"Failed to set calibration for device {self.name} to Sensibo servers: {err}" - ) from err - LOGGER.debug("Result: %s", result) + result = await self.async_send_command("set_calibration", {"data": data}) if result["status"] == "success": self.coordinator.data[self._device_id][self.entity_description.key] = value self.async_write_ha_state() From a8baebee8d4b05c779377e7e8de64751fb9793f3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Mar 2022 22:42:03 +0100 Subject: [PATCH 0254/1054] Return None for fields not reported in Sensibo (#67693) --- homeassistant/components/sensibo/climate.py | 4 ++-- homeassistant/components/sensibo/coordinator.py | 8 ++++---- homeassistant/components/sensibo/number.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index cd383367dc4..70b828c7d17 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -146,7 +146,7 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): return features @property - def current_humidity(self) -> int: + def current_humidity(self) -> int | None: """Return the current humidity.""" return self.coordinator.data[self.unique_id]["humidity"] @@ -168,7 +168,7 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): ] @property - def current_temperature(self) -> float: + def current_temperature(self) -> float | None: """Return the current temperature.""" return convert_temperature( self.coordinator.data[self.unique_id]["temp"], diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 926232ef159..9694d6db316 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -52,8 +52,8 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): unique_id = dev["id"] mac = dev["macAddress"] name = dev["room"]["name"] - temperature = dev["measurements"].get("temperature", 0.0) - humidity = dev["measurements"].get("humidity", 0) + temperature = dev["measurements"].get("temperature") + humidity = dev["measurements"].get("humidity") ac_states = dev["acState"] target_temperature = ac_states.get("targetTemperature") hvac_mode = ac_states.get("mode") @@ -95,8 +95,8 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): fw_type = dev["firmwareType"] model = dev["productModel"] - calibration_temp = dev["sensorsCalibration"].get("temperature", 0.0) - calibration_hum = dev["sensorsCalibration"].get("humidity", 0.0) + calibration_temp = dev["sensorsCalibration"].get("temperature") + calibration_hum = dev["sensorsCalibration"].get("humidity") device_data[unique_id] = { "id": unique_id, diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index c775b8e6ffa..ec7a150ebe9 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -90,7 +90,7 @@ class SensiboNumber(SensiboBaseEntity, NumberEntity): ) @property - def value(self) -> float: + def value(self) -> float | None: """Return the value from coordinator data.""" return self.coordinator.data[self._device_id][self.entity_description.key] From cc9fd2bcba1c34e633473ac664b87408b7bbbe49 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Sat, 5 Mar 2022 16:45:56 -0500 Subject: [PATCH 0255/1054] Add firmness number entity to SleepIQ (#65841) --- homeassistant/components/sleepiq/__init__.py | 1 + homeassistant/components/sleepiq/const.py | 9 +-- homeassistant/components/sleepiq/number.py | 55 ++++++++++++++++ tests/components/sleepiq/test_number.py | 68 ++++++++++++++++++++ 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/sleepiq/number.py create mode 100644 tests/components/sleepiq/test_number.py diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index da3f38fe560..17370d1b463 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -30,6 +30,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index c1c28a7b5a8..2aabf29ef54 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -2,18 +2,19 @@ DATA_SLEEPIQ = "data_sleepiq" DOMAIN = "sleepiq" -SLEEPYQ_INVALID_CREDENTIALS_MESSAGE = "username or password" BED = "bed" +FIRMNESS = "firmness" ICON_EMPTY = "mdi:bed-empty" ICON_OCCUPIED = "mdi:bed" IS_IN_BED = "is_in_bed" -SLEEP_NUMBER = "sleep_number" PRESSURE = "pressure" +SLEEP_NUMBER = "sleep_number" SENSOR_TYPES = { - SLEEP_NUMBER: "SleepNumber", - IS_IN_BED: "Is In Bed", + FIRMNESS: "Firmness", PRESSURE: "Pressure", + IS_IN_BED: "Is In Bed", + SLEEP_NUMBER: "SleepNumber", } LEFT = "left" diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py new file mode 100644 index 00000000000..39140b51a5b --- /dev/null +++ b/homeassistant/components/sleepiq/number.py @@ -0,0 +1,55 @@ +"""Support for SleepIQ SleepNumber firmness number entities.""" +from asyncsleepiq import SleepIQBed, SleepIQSleeper + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, FIRMNESS +from .coordinator import SleepIQData +from .entity import SleepIQSleeperEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ bed sensors.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepNumberFirmnessEntity(data.data_coordinator, bed, sleeper) + for bed in data.client.beds.values() + for sleeper in bed.sleepers + ) + + +class SleepNumberFirmnessEntity(SleepIQSleeperEntity, NumberEntity): + """Representation of an SleepIQ Entity with CoordinatorEntity.""" + + _attr_icon = "mdi:bed" + _attr_max_value: float = 100 + _attr_min_value: float = 5 + _attr_step: float = 5 + + def __init__( + self, + coordinator: DataUpdateCoordinator, + bed: SleepIQBed, + sleeper: SleepIQSleeper, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, bed, sleeper, FIRMNESS) + + @callback + def _async_update_attrs(self) -> None: + """Update sensor attributes.""" + self._attr_value = float(self.sleeper.sleep_number) + + async def async_set_value(self, value: float) -> None: + """Set the firmness value.""" + await self.sleeper.set_sleepnumber(int(value)) + self._attr_value = value + self.async_write_ha_state() diff --git a/tests/components/sleepiq/test_number.py b/tests/components/sleepiq/test_number.py new file mode 100644 index 00000000000..f00bef2b4cb --- /dev/null +++ b/tests/components/sleepiq/test_number.py @@ -0,0 +1,68 @@ +"""The tests for SleepIQ number platform.""" +from homeassistant.components.number import DOMAIN +from homeassistant.components.number.const import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPER_R_NAME, + SLEEPER_R_NAME_LOWER, + setup_platform, +) + + +async def test_firmness(hass, mock_asyncsleepiq): + """Test the SleepIQ firmness number values for a bed with two sides.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get( + f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness" + ) + assert state.state == "40.0" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_L_NAME} Firmness" + ) + + entry = entity_registry.async_get( + f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_firmness" + + state = hass.states.get( + f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness" + ) + assert state.state == "80.0" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} {SLEEPER_R_NAME} Firmness" + ) + + entry = entity_registry.async_get( + f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_firmness" + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_once() + mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_with(42) From 2474d84e352363bf12446977aeee41c555da6b9e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Mar 2022 22:59:15 +0100 Subject: [PATCH 0256/1054] Implement measurement sensor for Sensibo (#66949) --- .../components/sensibo/coordinator.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 9694d6db316..eaee8e23c1a 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -1,6 +1,7 @@ """DataUpdateCoordinator for the Sensibo integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -17,6 +18,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT +@dataclass +class MotionSensor: + """Dataclass for motionsensors.""" + + id: str + alive: bool | None = None + fw_ver: str | None = None + fw_type: str | None = None + is_main_sensor: bool | None = None + battery_voltage: int | None = None + humidity: int | None = None + temperature: float | None = None + model: str | None = None + + class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" @@ -98,6 +114,28 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): calibration_temp = dev["sensorsCalibration"].get("temperature") calibration_hum = dev["sensorsCalibration"].get("humidity") + # Sky plus supports functionality to use motion sensor as sensor for temp and humidity + if main_sensor := dev["mainMeasurementsSensor"]: + measurements = main_sensor["measurements"] + temperature = measurements.get("temperature") + humidity = measurements.get("humidity") + + motion_sensors = [ + MotionSensor( + id=motionsensor["id"], + alive=motionsensor["connectionStatus"].get("isAlive"), + fw_ver=motionsensor.get("firmwareVersion"), + fw_type=motionsensor.get("firmwareType"), + is_main_sensor=motionsensor.get("isMainSensor"), + battery_voltage=motionsensor["measurements"].get("batteryVoltage"), + humidity=motionsensor["measurements"].get("humidity"), + temperature=motionsensor["measurements"].get("temperature"), + model=motionsensor.get("productModel"), + ) + for motionsensor in dev["motionSensors"] + if dev["motionSensors"] + ] + device_data[unique_id] = { "id": unique_id, "mac": mac, @@ -126,5 +164,6 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): "calibration_temp": calibration_temp, "calibration_hum": calibration_hum, "full_capabilities": capabilities, + "motion_sensors": motion_sensors, } return device_data From 3d26f6b85d0a86c5a0cd80ca7c638842df4cf6b3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 5 Mar 2022 23:01:53 +0100 Subject: [PATCH 0257/1054] Quicker update on hvac mode change in Sensibo (#67692) --- homeassistant/components/sensibo/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 70b828c7d17..7c3282478ab 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -268,6 +268,7 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): await self._async_set_ac_state_property("on", True) await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) + await self.coordinator.async_request_refresh() async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" From f5d25eaf8f1269bd8cd7f11c903507d373491f1c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 6 Mar 2022 00:18:25 +0000 Subject: [PATCH 0258/1054] [ci skip] Translation update --- .../components/fritz/translations/nl.json | 3 ++- .../components/moon/translations/nl.json | 13 ++++++++++ .../components/onewire/translations/nl.json | 26 +++++++++++++++++++ .../components/sensibo/translations/nl.json | 9 ++++++- .../components/switch/translations/et.json | 10 +++++++ .../components/switch/translations/nl.json | 10 +++++++ .../components/switch/translations/pt-BR.json | 10 +++++++ .../components/switch/translations/ru.json | 10 +++++++ .../switch/translations/zh-Hant.json | 10 +++++++ .../components/update/translations/nl.json | 3 +++ 10 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/moon/translations/nl.json create mode 100644 homeassistant/components/update/translations/nl.json diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index c62a3b7044e..b5577f11a0d 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -10,7 +10,8 @@ "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken", "connection_error": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "upnp_not_configured": "Ontbrekende UPnP instellingen op apparaat." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/moon/translations/nl.json b/homeassistant/components/moon/translations/nl.json new file mode 100644 index 00000000000..ebcef695e04 --- /dev/null +++ b/homeassistant/components/moon/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u beginnen met instellen?" + } + } + }, + "title": "Maan" +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index 77ac79c1597..19dadbafe16 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -22,5 +22,31 @@ "title": "Stel 1-Wire in" } } + }, + "options": { + "error": { + "device_not_selected": "Selecteer apparaten om te configureren" + }, + "step": { + "ack_no_options": { + "description": "Er zijn geen opties voor de SysBus-implementatie", + "title": "OneWire SysBus-opties" + }, + "configure_device": { + "data": { + "precision": "Sensor nauwkeurigheid" + }, + "description": "Selecteer sensor nauwkeurigheid voor {sensor_id}", + "title": "OneWire sensor nauwkeurigheid" + }, + "device_selection": { + "data": { + "clear_device_options": "Wis alle apparaatconfiguraties", + "device_selection": "Selecteer apparaten om te configureren" + }, + "description": "Selecteer welke configuratiestappen moeten doorlopen", + "title": "OneWire-apparaatopties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index 8a3f935b983..cc6529c3540 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "cannot_connect": "Kan geen verbinding maken", + "incorrect_api_key": "Ongeldige API-sleutel voor geselecteerd account", "invalid_auth": "Ongeldige authenticatie", "no_devices": "Geen apparaten gevonden", "no_username": "Kan gebruikersnaam niet ophalen" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json index 394d908c9a1..e9c9928c89d 100644 --- a/homeassistant/components/switch/translations/et.json +++ b/homeassistant/components/switch/translations/et.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "L\u00fcliti olem" + }, + "description": "Vali valgusti l\u00fcliti" + } + } + }, "device_automation": { "action_type": { "toggle": "Muuda {entity_name} olekut", diff --git a/homeassistant/components/switch/translations/nl.json b/homeassistant/components/switch/translations/nl.json index 71d3b0f6b8e..87636c8a1f1 100644 --- a/homeassistant/components/switch/translations/nl.json +++ b/homeassistant/components/switch/translations/nl.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entiteit wijzigen" + }, + "description": "Kies de schakelaar voor de lichtschakelaar." + } + } + }, "device_automation": { "action_type": { "toggle": "Omschakelen {entity_name}", diff --git a/homeassistant/components/switch/translations/pt-BR.json b/homeassistant/components/switch/translations/pt-BR.json index cb73ce3c5cf..6f7f076332a 100644 --- a/homeassistant/components/switch/translations/pt-BR.json +++ b/homeassistant/components/switch/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entidade de switch" + }, + "description": "Selecione o switch para o interruptor de luz." + } + } + }, "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", diff --git a/homeassistant/components/switch/translations/ru.json b/homeassistant/components/switch/translations/ru.json index 801146b11d3..2602a1ac20b 100644 --- a/homeassistant/components/switch/translations/ru.json +++ b/homeassistant/components/switch/translations/ru.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u043a\u0430\u043a \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c", diff --git a/homeassistant/components/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json index 95f3ac4d641..631db326ae0 100644 --- a/homeassistant/components/switch/translations/zh-Hant.json +++ b/homeassistant/components/switch/translations/zh-Hant.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u958b\u95dc\u5be6\u9ad4" + }, + "description": "\u9078\u64c7\u6307\u5b9a\u70ba\u71c8\u5149\u4e4b\u958b\u95dc\u3002" + } + } + }, "device_automation": { "action_type": { "toggle": "\u5207\u63db{entity_name}", diff --git a/homeassistant/components/update/translations/nl.json b/homeassistant/components/update/translations/nl.json new file mode 100644 index 00000000000..95b82de3b4d --- /dev/null +++ b/homeassistant/components/update/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} \ No newline at end of file From 1358aed01641292805c94e0d3c50ec097df86746 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Mar 2022 06:27:06 +0100 Subject: [PATCH 0259/1054] Code cleanup yale_smart_alarm (#67701) --- .../components/yale_smart_alarm/__init__.py | 21 ++--- .../yale_smart_alarm/alarm_control_panel.py | 85 ++++++------------- .../yale_smart_alarm/config_flow.py | 7 +- .../components/yale_smart_alarm/const.py | 8 ++ .../yale_smart_alarm/coordinator.py | 13 +-- .../components/yale_smart_alarm/lock.py | 70 +++++---------- 6 files changed, 68 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 626c7d0b206..8712241a2aa 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -5,31 +5,25 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from .const import COORDINATOR, DOMAIN, LOGGER, PLATFORMS +from .const import COORDINATOR, DOMAIN, PLATFORMS from .coordinator import YaleDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yale from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - title = entry.title - - coordinator = YaleDataUpdateCoordinator(hass, entry=entry) + coordinator = YaleDataUpdateCoordinator(hass, entry) if not await hass.async_add_executor_job(coordinator.get_updates): raise ConfigEntryAuthFailed await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) - LOGGER.debug("Loaded entry for %s", title) - return True @@ -41,12 +35,7 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - title = entry.title - if unload_ok: + if await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) - LOGGER.debug("Unloaded entry for %s", title) - return unload_ok - + return True return False diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 0348676904e..7849f1a9db8 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -9,7 +9,6 @@ from yalesmartalarmclient.const import ( YALE_STATE_ARM_PARTIAL, YALE_STATE_DISARM, ) -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, @@ -39,6 +38,7 @@ from .const import ( MANUFACTURER, MODEL, STATE_MAP, + YALE_ALL_ERRORS, ) from .coordinator import YaleDataUpdateCoordinator @@ -103,81 +103,46 @@ class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): async def async_alarm_disarm(self, code=None) -> None: """Send disarm command.""" - if TYPE_CHECKING: - assert self.coordinator.yale, "Connection to API is missing" - - try: - alarm_state = await self.hass.async_add_executor_job( - self.coordinator.yale.disarm - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: - raise HomeAssistantError( - f"Could not verify disarmed for {self._attr_name}: {error}" - ) from error - - LOGGER.debug("Alarm disarmed: %s", alarm_state) - if alarm_state: - self.coordinator.data["alarm"] = YALE_STATE_DISARM - self.async_write_ha_state() - return - raise HomeAssistantError("Could not disarm, check system ready for disarming.") + return await self.async_set_alarm(YALE_STATE_DISARM, code) async def async_alarm_arm_home(self, code=None) -> None: """Send arm home command.""" - if TYPE_CHECKING: - assert self.coordinator.yale, "Connection to API is missing" - - try: - alarm_state = await self.hass.async_add_executor_job( - self.coordinator.yale.arm_partial - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: - raise HomeAssistantError( - f"Could not verify armed home for {self._attr_name}: {error}" - ) from error - - LOGGER.debug("Alarm armed home: %s", alarm_state) - if alarm_state: - self.coordinator.data["alarm"] = YALE_STATE_ARM_PARTIAL - self.async_write_ha_state() - return - raise HomeAssistantError("Could not arm home, check system ready for arming.") + return await self.async_set_alarm(YALE_STATE_ARM_PARTIAL, code) async def async_alarm_arm_away(self, code=None) -> None: """Send arm away command.""" + return await self.async_set_alarm(YALE_STATE_ARM_FULL, code) + + async def async_set_alarm(self, command: str, code: str | None = None) -> None: + """Set alarm.""" if TYPE_CHECKING: assert self.coordinator.yale, "Connection to API is missing" try: - alarm_state = await self.hass.async_add_executor_job( - self.coordinator.yale.arm_full - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: + if command == YALE_STATE_ARM_FULL: + alarm_state = await self.hass.async_add_executor_job( + self.coordinator.yale.arm_full + ) + if command == YALE_STATE_ARM_PARTIAL: + alarm_state = await self.hass.async_add_executor_job( + self.coordinator.yale.arm_partial + ) + if command == YALE_STATE_DISARM: + alarm_state = await self.hass.async_add_executor_job( + self.coordinator.yale.disarm + ) + except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not verify armed away for {self._attr_name}: {error}" + f"Could not set alarm for {self._attr_name}: {error}" ) from error - LOGGER.debug("Alarm armed away: %s", alarm_state) if alarm_state: - self.coordinator.data["alarm"] = YALE_STATE_ARM_FULL + self.coordinator.data["alarm"] = command self.async_write_ha_state() return - raise HomeAssistantError("Could not arm away, check system ready for arming.") + raise HomeAssistantError( + "Could not change alarm check system ready for arming." + ) @property def available(self) -> bool: diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index 62daa639f50..fe8b1c5b2fb 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -5,7 +5,7 @@ from typing import Any import voluptuous as vol from yalesmartalarmclient.client import YaleSmartAlarmClient -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError +from yalesmartalarmclient.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -21,6 +21,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, + YALE_BASE_ERRORS, ) DATA_SCHEMA = vol.Schema( @@ -81,7 +82,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): except AuthenticationError as error: LOGGER.error("Authentication failed. Check credentials %s", error) errors = {"base": "invalid_auth"} - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: LOGGER.error("Connection to API failed %s", error) errors = {"base": "cannot_connect"} @@ -124,7 +125,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): except AuthenticationError as error: LOGGER.error("Authentication failed. Check credentials %s", error) errors = {"base": "invalid_auth"} - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: LOGGER.error("Connection to API failed %s", error) errors = {"base": "cannot_connect"} diff --git a/homeassistant/components/yale_smart_alarm/const.py b/homeassistant/components/yale_smart_alarm/const.py index 0628e6aceb4..e506a2c70d6 100644 --- a/homeassistant/components/yale_smart_alarm/const.py +++ b/homeassistant/components/yale_smart_alarm/const.py @@ -6,6 +6,7 @@ from yalesmartalarmclient.client import ( YALE_STATE_ARM_PARTIAL, YALE_STATE_DISARM, ) +from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -40,3 +41,10 @@ STATE_MAP = { YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY, } + +YALE_BASE_ERRORS = ( + ConnectionError, + TimeoutError, + UnknownError, +) +YALE_ALL_ERRORS = (*YALE_BASE_ERRORS, AuthenticationError) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 2d476f920f9..879b68bfde1 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -2,9 +2,10 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from yalesmartalarmclient.client import YaleSmartAlarmClient -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError +from yalesmartalarmclient.exceptions import AuthenticationError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -12,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, YALE_BASE_ERRORS class YaleDataUpdateCoordinator(DataUpdateCoordinator): @@ -29,7 +30,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> dict[str, Any]: """Fetch data from Yale.""" updates = await self.hass.async_add_executor_job(self.get_updates) @@ -120,7 +121,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): "lock_map": _lock_map, } - def get_updates(self) -> dict: + def get_updates(self) -> dict[str, Any]: """Fetch data from Yale.""" if self.yale is None: @@ -130,7 +131,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): ) except AuthenticationError as error: raise ConfigEntryAuthFailed from error - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: raise UpdateFailed from error try: @@ -141,7 +142,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): except AuthenticationError as error: raise ConfigEntryAuthFailed from error - except (ConnectionError, TimeoutError, UnknownError) as error: + except YALE_BASE_ERRORS as error: raise UpdateFailed from error return { diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a7231d78dce..0ac16d23276 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError - from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, CONF_CODE @@ -17,7 +15,7 @@ from .const import ( COORDINATOR, DEFAULT_LOCK_CODE_DIGITS, DOMAIN, - LOGGER, + YALE_ALL_ERRORS, ) from .coordinator import YaleDataUpdateCoordinator from .entity import YaleEntity @@ -51,44 +49,15 @@ class YaleDoorlock(YaleEntity, LockEntity): async def async_unlock(self, **kwargs) -> None: """Send unlock command.""" - if TYPE_CHECKING: - assert self.coordinator.yale, "Connection to API is missing" - code = kwargs.get(ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE)) - - if not code: - raise HomeAssistantError( - f"No code provided, {self._attr_name} not unlocked" - ) - - try: - get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name - ) - lock_state = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.open_lock, - get_lock, - code, - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: - raise HomeAssistantError( - f"Could not verify unlocking for {self._attr_name}: {error}" - ) from error - - LOGGER.debug("Door unlock: %s", lock_state) - if lock_state: - self.coordinator.data["lock_map"][self._attr_unique_id] = "unlocked" - self.async_write_ha_state() - return - raise HomeAssistantError("Could not unlock, check system ready for unlocking") + return await self.async_set_lock("unlocked", code) async def async_lock(self, **kwargs) -> None: """Send lock command.""" + return await self.async_set_lock("locked", None) + + async def async_set_lock(self, command: str, code: str | None) -> None: + """Set lock.""" if TYPE_CHECKING: assert self.coordinator.yale, "Connection to API is missing" @@ -96,26 +65,25 @@ class YaleDoorlock(YaleEntity, LockEntity): get_lock = await self.hass.async_add_executor_job( self.coordinator.yale.lock_api.get, self._attr_name ) - lock_state = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.close_lock, - get_lock, - ) - except ( - AuthenticationError, - ConnectionError, - TimeoutError, - UnknownError, - ) as error: + if command == "locked": + lock_state = await self.hass.async_add_executor_job( + self.coordinator.yale.lock_api.close_lock, + get_lock, + ) + if command == "unlocked": + lock_state = await self.hass.async_add_executor_job( + self.coordinator.yale.lock_api.open_lock, get_lock, code + ) + except YALE_ALL_ERRORS as error: raise HomeAssistantError( - f"Could not verify unlocking for {self._attr_name}: {error}" + f"Could not set lock for {self._attr_name}: {error}" ) from error - LOGGER.debug("Door unlock: %s", lock_state) if lock_state: - self.coordinator.data["lock_map"][self._attr_unique_id] = "unlocked" + self.coordinator.data["lock_map"][self._attr_unique_id] = command self.async_write_ha_state() return - raise HomeAssistantError("Could not unlock, check system ready for unlocking") + raise HomeAssistantError("Could set lock, check system ready for lock.") @property def is_locked(self) -> bool | None: From 26c5dca45d9b3dee002dfe1549780747e5007e06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Mar 2022 22:02:22 -1000 Subject: [PATCH 0260/1054] Ensure elkm1 can be manually configured when discovered instance is not used (#67712) --- homeassistant/components/elkm1/config_flow.py | 14 +- tests/components/elkm1/test_config_flow.py | 153 +++++++++++++++++- 2 files changed, 161 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 96f9fd5d078..b8cd89edae4 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -225,7 +225,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(user_input, self.unique_id) except asyncio.TimeoutError: - return {CONF_HOST: "cannot_connect"}, None + return {"base": "cannot_connect"}, None except InvalidAuth: return {CONF_PASSWORD: "invalid_auth"}, None except Exception: # pylint: disable=broad-except @@ -285,9 +285,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if device := await async_discover_device( self.hass, user_input[CONF_ADDRESS] ): - await self.async_set_unique_id(dr.format_mac(device.mac_address)) + await self.async_set_unique_id( + dr.format_mac(device.mac_address), raise_on_progress=False + ) self._abort_if_unique_id_configured() - user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}" + # Ignore the port from discovery since its always going to be + # 2601 if secure is turned on even though they may want insecure + user_input[CONF_ADDRESS] = device.ip_address errors, result = await self._async_create_or_error(user_input, False) if not errors: return result @@ -322,7 +326,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if is_ip_address(host) and ( device := await async_discover_device(self.hass, host) ): - await self.async_set_unique_id(dr.format_mac(device.mac_address)) + await self.async_set_unique_id( + dr.format_mac(device.mac_address), raise_on_progress=False + ) self._abort_if_unique_id_configured() return (await self._async_create_or_error(user_input, True))[1] diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 49402d7b4d5..183ab90086c 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -73,6 +73,155 @@ async def test_form_user_with_secure_elk_no_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_user_with_insecure_elk_skip_discovery(hass): + """Test we can setup a insecure elk with skipping discovery.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_insecure_elk_no_discovery(hass): + """Test we can setup a insecure elk.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_insecure_elk_times_out(hass): + """Test we can setup a insecure elk that times out.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=False) + + with patch( + "homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", + 0, + ), patch( + "homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0 + ), _patch_discovery(), _patch_elk( + elk=mocked_elk + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass): """Test we abort when we try to configure the same ip.""" config_entry = MockConfigEntry( @@ -262,7 +411,7 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, - "host": "elks://127.0.0.1:2601", + "host": "elks://127.0.0.1", "password": "test-password", "prefix": "", "username": "test-username", @@ -434,7 +583,7 @@ async def test_form_cannot_connect(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {CONF_HOST: "cannot_connect"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_unknown_exception(hass): From 23b218bc4444c9774279c74252653d9d1e10be3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Mar 2022 22:02:45 -1000 Subject: [PATCH 0261/1054] Add missing disconnect in elkm1 config flow validation (#67716) --- homeassistant/components/elkm1/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index b8cd89edae4..d68fce268a2 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -81,8 +81,11 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT): - raise InvalidAuth + try: + if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT): + raise InvalidAuth + finally: + elk.disconnect() short_mac = _short_mac(mac) if mac else None if prefix and prefix != short_mac: From 6c41786be46fcad78e053e1457042bb90d051f06 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 6 Mar 2022 19:03:52 +1100 Subject: [PATCH 0262/1054] Update aiolifx dependency to resolve log flood (#67721) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index b034745ee31..9251bcb1f50 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.7.0", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"], "homekit": { "models": ["LIFX"] }, diff --git a/requirements_all.txt b/requirements_all.txt index cc5ecc31845..0857f1f43c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aiokafka==0.6.0 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.7.0 +aiolifx==0.7.1 # homeassistant.components.lifx aiolifx_effects==0.2.2 From e8c05298ba0d812f6ca210e778e9adf8dc9dc58c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 6 Mar 2022 10:29:20 +0100 Subject: [PATCH 0263/1054] Remove rfxtrx signal repetition (#67675) --- homeassistant/components/rfxtrx/__init__.py | 6 +-- .../components/rfxtrx/config_flow.py | 20 --------- homeassistant/components/rfxtrx/const.py | 1 - homeassistant/components/rfxtrx/cover.py | 12 +----- homeassistant/components/rfxtrx/light.py | 10 +---- homeassistant/components/rfxtrx/siren.py | 21 +++------ homeassistant/components/rfxtrx/strings.json | 1 - homeassistant/components/rfxtrx/switch.py | 6 +-- tests/components/rfxtrx/test_config_flow.py | 12 +----- tests/components/rfxtrx/test_cover.py | 23 ++++------ tests/components/rfxtrx/test_device_action.py | 6 +-- .../components/rfxtrx/test_device_trigger.py | 6 +-- tests/components/rfxtrx/test_light.py | 35 +++------------ tests/components/rfxtrx/test_siren.py | 6 +-- tests/components/rfxtrx/test_switch.py | 43 ++++--------------- 15 files changed, 43 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 3587f673fd9..d2afda81f9f 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -48,7 +48,6 @@ from .const import ( DOMAIN = "rfxtrx" -DEFAULT_SIGNAL_REPETITIONS = 1 DEFAULT_OFF_DELAY = 2.0 SIGNAL_EVENT = f"{DOMAIN}_event" @@ -562,15 +561,12 @@ class RfxtrxCommandEntity(RfxtrxEntity): self, device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, - signal_repetitions: int = 1, event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - self.signal_repetitions = signal_repetitions self._state: bool | None = None async def _async_send(self, fun, *args): rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] - for _ in range(self.signal_repetitions): - await self.hass.async_add_executor_job(fun, rfx_object.transport, *args) + await self.hass.async_add_executor_job(fun, rfx_object.transport, *args) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index ea0f7478740..8fce56564a4 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -50,16 +50,12 @@ from .const import ( CONF_OFF_DELAY, CONF_PROTOCOLS, CONF_REPLACE_DEVICE, - CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_DEFAULT, CONST_VENETIAN_BLIND_MODE_EU, CONST_VENETIAN_BLIND_MODE_US, 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 CONF_EVENT_CODE = "event_code" CONF_MANUAL_PATH = "Enter Manually" @@ -210,7 +206,6 @@ class OptionsFlow(config_entries.OptionsFlow): devices = {} device = { CONF_DEVICE_ID: device_id, - CONF_SIGNAL_REPETITIONS: user_input.get(CONF_SIGNAL_REPETITIONS, 1), } devices[self._selected_device_event_code] = device @@ -252,21 +247,6 @@ class OptionsFlow(config_entries.OptionsFlow): } 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 diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 74fe1ae2790..529289090a6 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -2,7 +2,6 @@ CONF_DATA_BITS = "data_bits" CONF_AUTOMATIC_ADD = "automatic_add" -CONF_SIGNAL_REPETITIONS = "signal_repetitions" CONF_OFF_DELAY = "off_delay" CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode" CONF_PROTOCOLS = "protocols" diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 80b58b6f0c4..d350998e2a9 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -19,16 +19,10 @@ from homeassistant.const import STATE_OPEN from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ( - DEFAULT_SIGNAL_REPETITIONS, - DeviceTuple, - RfxtrxCommandEntity, - async_setup_platform_entry, -) +from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry from .const import ( COMMAND_OFF_LIST, COMMAND_ON_LIST, - CONF_SIGNAL_REPETITIONS, CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_EU, CONST_VENETIAN_BLIND_MODE_US, @@ -59,7 +53,6 @@ async def async_setup_entry( RfxtrxCover( event.device, device_id, - entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), event=event if auto else None, ) @@ -79,12 +72,11 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self, device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, - signal_repetitions: int, event: rfxtrxmod.RFXtrxEvent = None, venetian_blind_mode: bool | None = None, ) -> None: """Initialize the RFXtrx cover device.""" - super().__init__(device, device_id, signal_repetitions, event) + super().__init__(device, device_id, event) self._venetian_blind_mode = venetian_blind_mode async def async_added_to_hass(self): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 6baada1fb75..7212b65cd7e 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -15,13 +15,8 @@ from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ( - DEFAULT_SIGNAL_REPETITIONS, - DeviceTuple, - RfxtrxCommandEntity, - async_setup_platform_entry, -) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_SIGNAL_REPETITIONS +from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST _LOGGER = logging.getLogger(__name__) @@ -53,7 +48,6 @@ async def async_setup_entry( RfxtrxLight( event.device, device_id, - entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), event=event if auto else None, ) ] diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index 78c970c4934..9a4a475998d 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -20,12 +20,11 @@ from homeassistant.helpers.event import async_call_later from . import ( DEFAULT_OFF_DELAY, - DEFAULT_SIGNAL_REPETITIONS, DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry, ) -from .const import CONF_OFF_DELAY, CONF_SIGNAL_REPETITIONS +from .const import CONF_OFF_DELAY SUPPORT_RFXTRX = SUPPORT_TURN_ON | SUPPORT_TONES @@ -76,9 +75,6 @@ async def async_setup_entry( RfxtrxChime( event.device, device_id, - entity_info.get( - CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS - ), entity_info.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY), auto, ) @@ -92,9 +88,6 @@ async def async_setup_entry( RfxtrxSecurityPanic( event.device, device_id, - entity_info.get( - CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS - ), entity_info.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY), auto, ) @@ -138,11 +131,9 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): _device: rfxtrxmod.ChimeDevice - def __init__( - self, device, device_id, signal_repetitions=1, off_delay=None, event=None - ): + def __init__(self, device, device_id, off_delay=None, event=None): """Initialize the entity.""" - super().__init__(device, device_id, signal_repetitions, event) + super().__init__(device, device_id, event) self._attr_available_tones = list(self._device.COMMANDS.values()) self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TONES self._default_tone = next(iter(self._device.COMMANDS)) @@ -191,11 +182,9 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) _device: rfxtrxmod.SecurityDevice - def __init__( - self, device, device_id, signal_repetitions=1, off_delay=None, event=None - ): + def __init__(self, device, device_id, off_delay=None, event=None): """Initialize the entity.""" - super().__init__(device, device_id, signal_repetitions, event) + super().__init__(device, device_id, event) self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF self._on_value = get_first_key(self._device.STATUS, SECURITY_PANIC_ON) self._off_value = get_first_key(self._device.STATUS, SECURITY_PANIC_OFF) diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index f21905c1d21..4469fd59801 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -54,7 +54,6 @@ "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", "venetian_blind_mode": "Venetian blind mode", "replace_device": "Select device to replace" }, diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 8bc1fa42874..fc826b4ebe6 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -12,7 +12,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( - DEFAULT_SIGNAL_REPETITIONS, DOMAIN, DeviceTuple, RfxtrxCommandEntity, @@ -23,7 +22,6 @@ from .const import ( COMMAND_OFF_LIST, COMMAND_ON_LIST, CONF_DATA_BITS, - CONF_SIGNAL_REPETITIONS, DEVICE_PACKET_TYPE_LIGHTING4, ) @@ -59,7 +57,6 @@ async def async_setup_entry( RfxtrxSwitch( event.device, device_id, - entity_info.get(CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS), entity_info.get(CONF_DATA_BITS), entity_info.get(CONF_COMMAND_ON), entity_info.get(CONF_COMMAND_OFF), @@ -79,14 +76,13 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self, device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, - signal_repetitions: int = 1, data_bits: int | None = None, cmd_on: int | None = None, cmd_off: int | None = None, event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the RFXtrx switch.""" - super().__init__(device, device_id, signal_repetitions, event=event) + super().__init__(device, device_id, event=event) self._data_bits = data_bits self._cmd_on = cmd_on self._cmd_off = cmd_off diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 100b6d0e55b..a756bf26b9f 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -401,7 +401,7 @@ async def test_options_add_device(hass): assert result["step_id"] == "set_device_options" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"signal_repetitions": 5} + result["flow_id"], user_input={} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -411,7 +411,6 @@ async def test_options_add_device(hass): assert entry.data["automatic_add"] assert entry.data["devices"]["0b1100cd0213c7f230010f71"] - 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") @@ -431,7 +430,7 @@ async def test_options_add_duplicate_device(hass): "device": "/dev/tty123", "debug": False, "automatic_add": False, - "devices": {"0b1100cd0213c7f230010f71": {"signal_repetitions": 1}}, + "devices": {"0b1100cd0213c7f230010f71": {}}, }, unique_id=DOMAIN, ) @@ -626,11 +625,9 @@ async def test_options_replace_control_device(hass): "devices": { "0b1100100118cdea02010f70": { "device_id": ["11", "0", "118cdea:2"], - "signal_repetitions": 1, }, "0b1100101118cdea02010f70": { "device_id": ["11", "0", "1118cdea:2"], - "signal_repetitions": 1, }, }, }, @@ -751,7 +748,6 @@ async def test_options_add_and_configure_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "signal_repetitions": 5, "data_bits": 4, "off_delay": "abcdef", "command_on": "xyz", @@ -769,7 +765,6 @@ async def test_options_add_and_configure_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "signal_repetitions": 5, "data_bits": 4, "command_on": "0xE", "command_off": "0x7", @@ -784,7 +779,6 @@ async def test_options_add_and_configure_device(hass): assert entry.data["automatic_add"] assert entry.data["devices"]["0913000022670e013970"] - assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5 assert entry.data["devices"]["0913000022670e013970"]["off_delay"] == 9 state = hass.states.get("binary_sensor.pt2262_22670e") @@ -816,7 +810,6 @@ async def test_options_add_and_configure_device(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "signal_repetitions": 5, "data_bits": 4, "command_on": "0xE", "command_off": "0x7", @@ -828,7 +821,6 @@ async def test_options_add_and_configure_device(hass): await hass.async_block_till_done() assert entry.data["devices"]["0913000022670e013970"] - assert entry.data["devices"]["0913000022670e013970"]["signal_repetitions"] == 5 assert "delay_off" not in entry.data["devices"]["0913000022670e013970"] diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index fe7f49d728b..a05a456d221 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -12,9 +12,7 @@ from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_one_cover(hass, rfxtrx): """Test with 1 cover.""" - entry_data = create_rfx_test_cfg( - devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}} - ) + entry_data = create_rfx_test_cfg(devices={"0b1400cd0213c7f20d010f51": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) @@ -61,9 +59,7 @@ async def test_state_restore(hass, rfxtrx, state): mock_restore_cache(hass, [State(entity_id, state)]) - entry_data = create_rfx_test_cfg( - devices={"0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}} - ) + entry_data = create_rfx_test_cfg(devices={"0b1400cd0213c7f20d010f51": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) @@ -78,9 +74,9 @@ async def test_several_covers(hass, rfxtrx): """Test with 3 covers.""" entry_data = create_rfx_test_cfg( devices={ - "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}, - "0A1400ADF394AB010D0060": {"signal_repetitions": 1}, - "09190000009ba8010100": {"signal_repetitions": 1}, + "0b1400cd0213c7f20d010f51": {}, + "0A1400ADF394AB010D0060": {}, + "09190000009ba8010100": {}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -125,8 +121,8 @@ async def test_duplicate_cover(hass, rfxtrx): """Test with 2 duplicate covers.""" entry_data = create_rfx_test_cfg( devices={ - "0b1400cd0213c7f20d010f51": {"signal_repetitions": 1}, - "0b1400cd0213c7f20d010f50": {"signal_repetitions": 1}, + "0b1400cd0213c7f20d010f51": {}, + "0b1400cd0213c7f20d010f50": {}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -147,11 +143,10 @@ async def test_rfy_cover(hass, rfxtrx): entry_data = create_rfx_test_cfg( devices={ "071a000001020301": { - "signal_repetitions": 1, "venetian_blind_mode": "Unknown", }, - "071a000001020302": {"signal_repetitions": 1, "venetian_blind_mode": "US"}, - "071a000001020303": {"signal_repetitions": 1, "venetian_blind_mode": "EU"}, + "071a000001020302": {"venetian_blind_mode": "US"}, + "071a000001020303": {"venetian_blind_mode": "EU"}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index 54f6430fc1d..79211b73513 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -94,7 +94,7 @@ def _get_expected_actions(data): ) async def test_get_actions(hass, device_reg: DeviceRegistry, device, expected): """Test we get the expected actions from a rfxtrx.""" - await setup_entry(hass, {device.code: {"signal_repetitions": 1}}) + await setup_entry(hass, {device.code: {}}) device_entry = device_reg.async_get_device(device.device_identifiers, set()) assert device_entry @@ -137,7 +137,7 @@ async def test_action( ): """Test for actions.""" - await setup_entry(hass, {device.code: {"signal_repetitions": 1}}) + await setup_entry(hass, {device.code: {}}) device_entry = device_reg.async_get_device(device.device_identifiers, set()) assert device_entry @@ -172,7 +172,7 @@ async def test_invalid_action(hass, device_reg: DeviceRegistry): """Test for invalid actions.""" device = DEVICE_LIGHTING_1 - await setup_entry(hass, {device.code: {"signal_repetitions": 1}}) + await setup_entry(hass, {device.code: {}}) device_identifers: Any = device.device_identifiers device_entry = device_reg.async_get_device(device_identifers, set()) diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index eee437d495c..c53a7f530d8 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -85,7 +85,7 @@ async def setup_entry(hass, devices): ) async def test_get_triggers(hass, device_reg, event: EventTestData, expected): """Test we get the expected triggers from a rfxtrx.""" - await setup_entry(hass, {event.code: {"signal_repetitions": 1}}) + await setup_entry(hass, {event.code: {}}) device_entry = device_reg.async_get_device(event.device_identifiers, set()) @@ -112,7 +112,7 @@ async def test_get_triggers(hass, device_reg, event: EventTestData, expected): async def test_firing_event(hass, device_reg: DeviceRegistry, rfxtrx, event): """Test for turn_on and turn_off triggers firing.""" - await setup_entry(hass, {event.code: {"fire_event": True, "signal_repetitions": 1}}) + await setup_entry(hass, {event.code: {"fire_event": True}}) device_entry = device_reg.async_get_device(event.device_identifiers, set()) assert device_entry @@ -152,7 +152,7 @@ async def test_invalid_trigger(hass, device_reg: DeviceRegistry): """Test for invalid actions.""" event = EVENT_LIGHTING_1 - await setup_entry(hass, {event.code: {"fire_event": True, "signal_repetitions": 1}}) + await setup_entry(hass, {event.code: {"fire_event": True}}) device_identifers: Any = event.device_identifiers device_entry = device_reg.async_get_device(device_identifers, set()) diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 8b68dc7bf47..a6ff96662fb 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -14,9 +14,7 @@ from tests.components.rfxtrx.conftest import create_rfx_test_cfg async def test_one_light(hass, rfxtrx): """Test with 1 light.""" - entry_data = create_rfx_test_cfg( - devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}} - ) + entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210020f51": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) @@ -100,9 +98,7 @@ async def test_state_restore(hass, rfxtrx, state, brightness): hass, [State(entity_id, state, attributes={ATTR_BRIGHTNESS: brightness})] ) - entry_data = create_rfx_test_cfg( - devices={"0b1100cd0213c7f210020f51": {"signal_repetitions": 1}} - ) + entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210020f51": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) @@ -118,9 +114,9 @@ async def test_several_lights(hass, rfxtrx): """Test with 3 lights.""" entry_data = create_rfx_test_cfg( devices={ - "0b1100cd0213c7f230020f71": {"signal_repetitions": 1}, - "0b1100100118cdea02020f70": {"signal_repetitions": 1}, - "0b1100101118cdea02050f70": {"signal_repetitions": 1}, + "0b1100cd0213c7f230020f71": {}, + "0b1100100118cdea02020f70": {}, + "0b1100101118cdea02050f70": {}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -163,27 +159,6 @@ async def test_several_lights(hass, rfxtrx): assert state.attributes.get("brightness") == 255 -@pytest.mark.parametrize("repetitions", [1, 3]) -async def test_repetitions(hass, rfxtrx, repetitions): - """Test signal 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( - "light", "turn_on", {"entity_id": "light.ac_213c7f2_48"}, blocking=True - ) - await hass.async_block_till_done() - - assert rfxtrx.transport.send.call_count == repetitions - - async def test_discover_light(hass, rfxtrx_automatic): """Test with discovery of lights.""" rfxtrx = rfxtrx_automatic diff --git a/tests/components/rfxtrx/test_siren.py b/tests/components/rfxtrx/test_siren.py index 98859b84109..25098d33ccc 100644 --- a/tests/components/rfxtrx/test_siren.py +++ b/tests/components/rfxtrx/test_siren.py @@ -11,7 +11,7 @@ from tests.common import MockConfigEntry async def test_one_chime(hass, rfxtrx, timestep): """Test with 1 entity.""" entry_data = create_rfx_test_cfg( - devices={"0a16000000000000000000": {"signal_repetitions": 1, "off_delay": 2.0}} + devices={"0a16000000000000000000": {"off_delay": 2.0}} ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -66,9 +66,7 @@ async def test_one_chime(hass, rfxtrx, timestep): async def test_one_security1(hass, rfxtrx, timestep): """Test with 1 entity.""" - entry_data = create_rfx_test_cfg( - devices={"08200300a109000670": {"signal_repetitions": 1, "off_delay": 2.0}} - ) + entry_data = create_rfx_test_cfg(devices={"08200300a109000670": {"off_delay": 2.0}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index a4560934ee1..4da7f1d9881 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -17,9 +17,7 @@ EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" async def test_one_switch(hass, rfxtrx): """Test with 1 switch.""" - entry_data = create_rfx_test_cfg( - devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}} - ) + entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210010f51": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) @@ -57,7 +55,6 @@ async def test_one_pt2262_switch(hass, rfxtrx): entry_data = create_rfx_test_cfg( devices={ "0913000022670e013970": { - "signal_repetitions": 1, "data_bits": 4, "command_on": 0xE, "command_off": 0x7, @@ -104,9 +101,7 @@ async def test_state_restore(hass, rfxtrx, state): mock_restore_cache(hass, [State(entity_id, state)]) - entry_data = create_rfx_test_cfg( - devices={"0b1100cd0213c7f210010f51": {"signal_repetitions": 1}} - ) + entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210010f51": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) @@ -121,9 +116,9 @@ async def test_several_switches(hass, rfxtrx): """Test with 3 switches.""" entry_data = create_rfx_test_cfg( devices={ - "0b1100cd0213c7f230010f71": {"signal_repetitions": 1}, - "0b1100100118cdea02010f70": {"signal_repetitions": 1}, - "0b1100101118cdea02010f70": {"signal_repetitions": 1}, + "0b1100cd0213c7f230010f71": {}, + "0b1100100118cdea02010f70": {}, + "0b1100101118cdea02010f70": {}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -149,33 +144,12 @@ async def test_several_switches(hass, rfxtrx): assert state.attributes.get("friendly_name") == "AC 1118cdea:2" -@pytest.mark.parametrize("repetitions", [1, 3]) -async def test_repetitions(hass, rfxtrx, repetitions): - """Test signal 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( - "switch", "turn_on", {"entity_id": "switch.ac_213c7f2_48"}, blocking=True - ) - await hass.async_block_till_done() - - assert rfxtrx.transport.send.call_count == repetitions - - async def test_switch_events(hass, rfxtrx): """Event test with 2 switches.""" entry_data = create_rfx_test_cfg( devices={ - "0b1100cd0213c7f205010f51": {"signal_repetitions": 1}, - "0b1100cd0213c7f210010f51": {"signal_repetitions": 1}, + "0b1100cd0213c7f205010f51": {}, + "0b1100cd0213c7f210010f51": {}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -231,7 +205,6 @@ async def test_pt2262_switch_events(hass, rfxtrx): entry_data = create_rfx_test_cfg( devices={ "0913000022670e013970": { - "signal_repetitions": 1, "data_bits": 4, "command_on": 0xE, "command_off": 0x7, @@ -299,7 +272,7 @@ async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic): async def test_unknown_event_code(hass, rfxtrx): """Test with 3 switches.""" - entry_data = create_rfx_test_cfg(devices={"1234567890": {"signal_repetitions": 1}}) + entry_data = create_rfx_test_cfg(devices={"1234567890": {}}) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) mock_entry.add_to_hass(hass) From 496287973d975258592b9a7d534037fbd69554fe Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 6 Mar 2022 05:34:47 -0800 Subject: [PATCH 0264/1054] Bump google-cloud-pubsub to 2.10.0 (#67724) --- homeassistant/components/google_pubsub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index cd68e17b59d..f4f966538dd 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -2,7 +2,7 @@ "domain": "google_pubsub", "name": "Google Pub/Sub", "documentation": "https://www.home-assistant.io/integrations/google_pubsub", - "requirements": ["google-cloud-pubsub==2.9.0"], + "requirements": ["google-cloud-pubsub==2.10.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 0857f1f43c1..572de0d6aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -716,7 +716,7 @@ goodwe==0.2.15 google-api-python-client==2.38.0 # homeassistant.components.google_pubsub -google-cloud-pubsub==2.9.0 +google-cloud-pubsub==2.10.0 # homeassistant.components.google_cloud google-cloud-texttospeech==2.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fba86e717c..30099b962b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -496,7 +496,7 @@ goodwe==0.2.15 google-api-python-client==2.38.0 # homeassistant.components.google_pubsub -google-cloud-pubsub==2.9.0 +google-cloud-pubsub==2.10.0 # homeassistant.components.nest google-nest-sdm==1.7.1 From 69c58a9ce6cd94b4d7c98b7c34ca71ff836e2e7d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 6 Mar 2022 05:35:16 -0800 Subject: [PATCH 0265/1054] Bump google-nest-sdm to 1.8.0 (#67723) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 6968b401561..f59a8e6ac31 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.7.1"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.8.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 572de0d6aec..54f8b2752bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,7 +722,7 @@ google-cloud-pubsub==2.10.0 google-cloud-texttospeech==2.10.0 # homeassistant.components.nest -google-nest-sdm==1.7.1 +google-nest-sdm==1.8.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30099b962b9..49b19a2fc31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -499,7 +499,7 @@ google-api-python-client==2.38.0 google-cloud-pubsub==2.10.0 # homeassistant.components.nest -google-nest-sdm==1.7.1 +google-nest-sdm==1.8.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From b6c962726ada4bcc89cbef3dc20c5be7adacd70d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 6 Mar 2022 16:45:41 +0000 Subject: [PATCH 0266/1054] Fix regression with homekit_controller + Aqara motion/vibration sensors (#67740) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index dfd45991b3f..9ca447ad2fe 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.15"], + "requirements": ["aiohomekit==0.7.16"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 54f8b2752bf..3a7e9bccf0d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.15 +aiohomekit==0.7.16 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49b19a2fc31..5642323b5d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aioguardian==2021.11.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.15 +aiohomekit==0.7.16 # homeassistant.components.emulated_hue # homeassistant.components.http From 208e8b16dbb20fe4944ff5c8b39b8d254069830a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 6 Mar 2022 21:16:09 +0100 Subject: [PATCH 0267/1054] Add problem sensor to yale_smart_alarm (#67699) --- .../yale_smart_alarm/alarm_control_panel.py | 13 +--- .../yale_smart_alarm/binary_sensor.py | 72 +++++++++++++++++-- .../yale_smart_alarm/coordinator.py | 10 ++- .../components/yale_smart_alarm/entity.py | 22 +++++- 4 files changed, 96 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 7849f1a9db8..ad38f30991b 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -23,10 +23,8 @@ from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( CONF_AREA_ID, @@ -35,12 +33,11 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, - MANUFACTURER, - MODEL, STATE_MAP, YALE_ALL_ERRORS, ) from .coordinator import YaleDataUpdateCoordinator +from .entity import YaleAlarmEntity PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { @@ -81,7 +78,7 @@ async def async_setup_entry( ) -class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): +class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): """Represent a Yale Smart Alarm.""" coordinator: YaleDataUpdateCoordinator @@ -94,12 +91,6 @@ class YaleAlarmDevice(CoordinatorEntity, AlarmControlPanelEntity): super().__init__(coordinator) self._attr_name = coordinator.entry.data[CONF_NAME] self._attr_unique_id = coordinator.entry.entry_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.entry.data[CONF_USERNAME])}, - manufacturer=MANUFACTURER, - model=MODEL, - name=self._attr_name, - ) async def async_alarm_disarm(self, code=None) -> None: """Send disarm command.""" diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index b017c4e33e3..42f8b446abc 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -4,14 +4,44 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import COORDINATOR, DOMAIN from .coordinator import YaleDataUpdateCoordinator -from .entity import YaleEntity +from .entity import YaleAlarmEntity, YaleEntity + +SENSOR_TYPES = ( + BinarySensorEntityDescription( + key="acfail", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Power Loss", + ), + BinarySensorEntityDescription( + key="battery", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Battery", + ), + BinarySensorEntityDescription( + key="tamper", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Tamper", + ), + BinarySensorEntityDescription( + key="jam", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Jam", + ), +) async def async_setup_entry( @@ -22,14 +52,17 @@ async def async_setup_entry( coordinator: YaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ COORDINATOR ] + sensors: list[YaleDoorSensor | YaleProblemSensor] = [] + for data in coordinator.data["door_windows"]: + sensors.append(YaleDoorSensor(coordinator, data)) + for description in SENSOR_TYPES: + sensors.append(YaleProblemSensor(coordinator, description)) - async_add_entities( - YaleBinarySensor(coordinator, data) for data in coordinator.data["door_windows"] - ) + async_add_entities(sensors) -class YaleBinarySensor(YaleEntity, BinarySensorEntity): - """Representation of a Yale binary sensor.""" +class YaleDoorSensor(YaleEntity, BinarySensorEntity): + """Representation of a Yale door sensor.""" _attr_device_class = BinarySensorDeviceClass.DOOR @@ -37,3 +70,30 @@ class YaleBinarySensor(YaleEntity, BinarySensorEntity): def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self.coordinator.data["sensor_map"][self._attr_unique_id] == "open" + + +class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): + """Representation of a Yale problem sensor.""" + + entity_description: BinarySensorEntityDescription + + def __init__( + self, + coordinator: YaleDataUpdateCoordinator, + entity_description: BinarySensorEntityDescription, + ) -> None: + """Initiate Yale Problem Sensor.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_name = ( + f"{coordinator.entry.data[CONF_NAME]} {entity_description.name}" + ) + self._attr_unique_id = f"{coordinator.entry.entry_id}-{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return ( + self.coordinator.data["status"][self.entity_description.key] + != "main.normal" + ) diff --git a/homeassistant/components/yale_smart_alarm/coordinator.py b/homeassistant/components/yale_smart_alarm/coordinator.py index 879b68bfde1..1a350d1db98 100644 --- a/homeassistant/components/yale_smart_alarm/coordinator.py +++ b/homeassistant/components/yale_smart_alarm/coordinator.py @@ -119,6 +119,7 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): "online": updates["online"], "sensor_map": _sensor_map, "lock_map": _lock_map, + "panel_info": updates["panel_info"], } def get_updates(self) -> dict[str, Any]: @@ -136,9 +137,11 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): try: arm_status = self.yale.get_armed_status() - cycle = self.yale.get_cycle() - status = self.yale.get_status() - online = self.yale.get_online() + data = self.yale.get_all() + cycle = data["CYCLE"] + status = data["STATUS"] + online = data["ONLINE"] + panel_info = data["PANEL INFO"] except AuthenticationError as error: raise ConfigEntryAuthFailed from error @@ -150,4 +153,5 @@ class YaleDataUpdateCoordinator(DataUpdateCoordinator): "cycle": cycle, "status": status, "online": online, + "panel_info": panel_info, } diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index 318989a018c..065d1f4fecb 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -1,6 +1,7 @@ """Base class for yale_smart_alarm entity.""" -from homeassistant.const import CONF_USERNAME +from homeassistant.const import CONF_NAME, CONF_USERNAME +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -25,3 +26,22 @@ class YaleEntity(CoordinatorEntity, Entity): identifiers={(DOMAIN, data["address"])}, via_device=(DOMAIN, self.coordinator.entry.data[CONF_USERNAME]), ) + + +class YaleAlarmEntity(CoordinatorEntity, Entity): + """Base implementation for Yale Alarm device.""" + + coordinator: YaleDataUpdateCoordinator + + def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: + """Initialize an Yale device.""" + super().__init__(coordinator) + panel_info = coordinator.data["panel_info"] + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.entry.data[CONF_USERNAME])}, + manufacturer=MANUFACTURER, + model=MODEL, + name=coordinator.entry.data[CONF_NAME], + connections={(CONNECTION_NETWORK_MAC, panel_info["mac"])}, + sw_version=panel_info["version"], + ) From cdaa7bb60caab055f1182fb43c839ac50166941b Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sun, 6 Mar 2022 15:57:18 -0500 Subject: [PATCH 0268/1054] Bump greeclimate to 1.1.0 (#67763) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index c3b4f1f028a..cd826f3291d 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==1.0.2"], + "requirements": ["greeclimate==1.1.0"], "codeowners": ["@cmroche"], "iot_class": "local_polling", "loggers": ["greeclimate"] diff --git a/requirements_all.txt b/requirements_all.txt index 3a7e9bccf0d..38f866acf8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -737,7 +737,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.0.2 +greeclimate==1.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5642323b5d4..5a75bf20ffe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -505,7 +505,7 @@ google-nest-sdm==1.8.0 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==1.0.2 +greeclimate==1.1.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 58321c50e1130ccc17af945540f2fc431881e892 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Mar 2022 16:13:39 -0800 Subject: [PATCH 0269/1054] Fix scaffold (#67769) --- script/scaffold/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 019ed33fad1..a73165de770 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -50,7 +50,7 @@ class Info: """Update the integration manifest.""" print(f"Updating {self.domain} manifest: {kwargs}") self.manifest_path.write_text( - json.dumps({**self.manifest(), **kwargs}, indent=2 + "\n") + json.dumps({**self.manifest(), **kwargs}, indent=2) + "\n" ) @property From 2957f4ce85055e26a5768847e56284ff84b9f543 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 7 Mar 2022 00:18:40 +0000 Subject: [PATCH 0270/1054] [ci skip] Translation update --- .../bmw_connected_drive/translations/pl.json | 2 +- .../components/coinbase/translations/pl.json | 2 +- .../components/deconz/translations/pl.json | 2 +- .../components/fritz/translations/hu.json | 3 +- .../components/fritz/translations/pl.json | 3 +- .../components/hangouts/translations/el.json | 1 + .../components/moon/translations/he.json | 13 ++++ .../components/moon/translations/hu.json | 13 ++++ .../components/moon/translations/pl.json | 13 ++++ .../components/netatmo/translations/pl.json | 8 +-- .../components/onewire/translations/hu.json | 30 ++++++++++ .../components/onewire/translations/pl.json | 26 ++++++++ .../pvpc_hourly_pricing/translations/pl.json | 4 +- .../components/sensibo/translations/he.json | 11 +++- .../components/sensibo/translations/hu.json | 9 ++- .../components/sensibo/translations/pl.json | 9 ++- .../components/sensor/translations/el.json | 1 + .../components/sleepiq/translations/he.json | 9 ++- .../components/subaru/translations/pl.json | 2 +- .../components/switch/translations/el.json | 10 ++++ .../components/switch/translations/he.json | 10 ++++ .../components/switch/translations/hu.json | 10 ++++ .../components/switch/translations/pl.json | 10 ++++ .../components/switch/translations/tr.json | 10 ++++ .../components/tuya/translations/he.json | 11 +++- .../tuya/translations/select.he.json | 59 ++++++++++++++++++- .../tuya/translations/sensor.he.json | 15 ++++- .../unifiprotect/translations/pl.json | 2 +- .../components/update/translations/hu.json | 3 + .../components/update/translations/pl.json | 3 + 30 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/moon/translations/he.json create mode 100644 homeassistant/components/moon/translations/hu.json create mode 100644 homeassistant/components/moon/translations/pl.json create mode 100644 homeassistant/components/update/translations/hu.json create mode 100644 homeassistant/components/update/translations/pl.json diff --git a/homeassistant/components/bmw_connected_drive/translations/pl.json b/homeassistant/components/bmw_connected_drive/translations/pl.json index 70467c6f9b9..146916edd5b 100644 --- a/homeassistant/components/bmw_connected_drive/translations/pl.json +++ b/homeassistant/components/bmw_connected_drive/translations/pl.json @@ -21,7 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Tylko odczyt (tylko czujniki i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", + "read_only": "Tylko odczyt (tylko sensory i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", "use_location": "U\u017cyj lokalizacji Home Assistant do sondowania lokalizacji samochodu (wymagane w przypadku pojazd\u00f3w innych ni\u017c i3/i8 wyprodukowanych przed 7/2014)" } } diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index e93e71d9e26..14432be5928 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -35,7 +35,7 @@ "init": { "data": { "account_balance_currencies": "Salda portfela do zg\u0142oszenia.", - "exchange_base": "Waluta bazowa dla czujnik\u00f3w kurs\u00f3w walut.", + "exchange_base": "Waluta bazowa dla sensor\u00f3w kurs\u00f3w walut.", "exchange_rate_currencies": "Kursy walut do zg\u0142oszenia." }, "description": "Dostosuj opcje Coinbase" diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 72d62aff959..945830a5734 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -97,7 +97,7 @@ "step": { "deconz_devices": { "data": { - "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", + "allow_clip_sensor": "Zezwalaj na sensory deCONZ CLIP", "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ", "allow_new_devices": "Zezwalaj na automatyczne dodawanie nowych urz\u0105dze\u0144" }, diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index ef8d8b2b32c..81d3bb19e27 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -10,7 +10,8 @@ "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "connection_error": "Nem siker\u00fclt csatlakozni", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "upnp_not_configured": "Hi\u00e1nyz\u00f3 UPnP-be\u00e1ll\u00edt\u00e1sok az eszk\u00f6z\u00f6n." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index b87f201f70a..7ec1f539aab 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -10,7 +10,8 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie", + "upnp_not_configured": "Brak ustawie\u0144 UPnP w urz\u0105dzeniu." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/hangouts/translations/el.json b/homeassistant/components/hangouts/translations/el.json index c4e46912a51..4b453c5bbaa 100644 --- a/homeassistant/components/hangouts/translations/el.json +++ b/homeassistant/components/hangouts/translations/el.json @@ -14,6 +14,7 @@ "data": { "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, + "description": "\u039a\u03b5\u03bd\u03cc", "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, "user": { diff --git a/homeassistant/components/moon/translations/he.json b/homeassistant/components/moon/translations/he.json new file mode 100644 index 00000000000..e5602d2dc15 --- /dev/null +++ b/homeassistant/components/moon/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + }, + "title": "\u05d9\u05e8\u05d7" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/hu.json b/homeassistant/components/moon/translations/hu.json new file mode 100644 index 00000000000..8c6a9f42071 --- /dev/null +++ b/homeassistant/components/moon/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "title": "Hold" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pl.json b/homeassistant/components/moon/translations/pl.json new file mode 100644 index 00000000000..fe01b71dadf --- /dev/null +++ b/homeassistant/components/moon/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + }, + "title": "Ksi\u0119\u017cyc" +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 4b7ac6490cc..39137e1ae83 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -54,16 +54,16 @@ "mode": "Obliczenia", "show_on_map": "Poka\u017c na mapie" }, - "description": "Skonfiguruj publiczny czujnik pogody dla obszaru.", - "title": "Netatmo publiczny czujnik pogody" + "description": "Skonfiguruj publiczny sensor pogody dla obszaru.", + "title": "Publiczny sensor pogody Netatmo" }, "public_weather_areas": { "data": { "new_area": "Nazwa obszaru", "weather_areas": "Obszary pogodowe" }, - "description": "Skonfiguruj publiczne czujniki pogody.", - "title": "Netatmo publiczny czujnik pogody" + "description": "Skonfiguruj publiczne sensory pogody.", + "title": "Publiczny sensor pogody Netatmo" } } } diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index 4d53659788d..7034f2eaa29 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -22,5 +22,35 @@ "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa" } } + }, + "options": { + "error": { + "device_not_selected": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket" + }, + "step": { + "ack_no_options": { + "data": { + "one": "\u00dcres", + "other": "\u00dcres" + }, + "description": "A SysBus implement\u00e1ci\u00f3j\u00e1nak nincsenek opci\u00f3i.", + "title": "OneWire SysBus opci\u00f3k" + }, + "configure_device": { + "data": { + "precision": "\u00c9rz\u00e9kel\u0151 pontoss\u00e1ga" + }, + "description": "V\u00e1lassza ki az \u00e9rz\u00e9kel\u0151 pontoss\u00e1g\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {sensor_id}", + "title": "OneWire \u00e9rz\u00e9kel\u0151 pontoss\u00e1ga" + }, + "device_selection": { + "data": { + "clear_device_options": "Az \u00f6sszes eszk\u00f6zkonfigur\u00e1ci\u00f3 t\u00f6rl\u00e9se", + "device_selection": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket" + }, + "description": "V\u00e1lassza ki a konfigur\u00e1ci\u00f3s l\u00e9p\u00e9seket", + "title": "OneWire eszk\u00f6z be\u00e1ll\u00edt\u00e1sai" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pl.json b/homeassistant/components/onewire/translations/pl.json index e1200b968b8..fe0e1d54eb5 100644 --- a/homeassistant/components/onewire/translations/pl.json +++ b/homeassistant/components/onewire/translations/pl.json @@ -22,5 +22,31 @@ "title": "Konfiguracja 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Wybierz urz\u0105dzenia do skonfigurowania" + }, + "step": { + "ack_no_options": { + "description": "Nie ma opcji dla implementacji magistrali SysBus", + "title": "Opcje magistrali OneWire SysBus" + }, + "configure_device": { + "data": { + "precision": "Dok\u0142adno\u015b\u0107 sensora" + }, + "description": "Wybierz dok\u0142adno\u015b\u0107 dla sensora {sensor_id}", + "title": "Dok\u0142adno\u015b\u0107 sensora OneWire" + }, + "device_selection": { + "data": { + "clear_device_options": "Wyczy\u015b\u0107 wszystkie konfiguracje urz\u0105dze\u0144", + "device_selection": "Wybierz urz\u0105dzenia do skonfigurowania" + }, + "description": "Wybierz, kt\u00f3re kroki konfiguracji maj\u0105 by\u0107 przetworzone", + "title": "Opcje urz\u0105dze\u0144 OneWire" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json index d689fd9383a..052cf66c1a6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json @@ -11,7 +11,7 @@ "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" }, - "description": "Ten czujnik u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", + "description": "Ten sensor u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", "title": "Konfiguracja sensora" } } @@ -24,7 +24,7 @@ "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" }, - "description": "Ten czujnik u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", + "description": "Ten sensor u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", "title": "Konfiguracja sensora" } } diff --git a/homeassistant/components/sensibo/translations/he.json b/homeassistant/components/sensibo/translations/he.json index 403b3c7e2ea..3486bd3c646 100644 --- a/homeassistant/components/sensibo/translations/he.json +++ b/homeassistant/components/sensibo/translations/he.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 55fd361aacc..802b718cc82 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "incorrect_api_key": "\u00c9rv\u00e9nytelen API-kulcs a megadott fi\u00f3khoz", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "no_devices": "Nincs \u00e9szlelt eszk\u00f6z", "no_username": "Nem siker\u00fclt beolvasni a felhaszn\u00e1l\u00f3nevet" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index fcbf9a20a6e..022aaf52039 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "incorrect_api_key": "Nieprawid\u0142owy klucz API dla wybranego konta", "invalid_auth": "Niepoprawne uwierzytelnienie", "no_devices": "Nie wykryto urz\u0105dze\u0144", "no_username": "Nie uda\u0142o si\u0119 uzyska\u0107 nazwy u\u017cytkownika" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + } + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index a3bf2cf6df9..dc15aec106a 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -5,6 +5,7 @@ "is_battery_level": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2 {entity_name}", "is_carbon_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", "is_carbon_monoxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03bc\u03bf\u03bd\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03ac\u03bd\u03b8\u03c1\u03b1\u03ba\u03b1 \u03c4\u03bf\u03c5 {entity_name}", + "is_current": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03c1\u03b5\u03cd\u03bc\u03b1 \u03b3\u03b9\u03b1 {entity_name}", "is_energy": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 {entity_name}", "is_frequency": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 {entity_name}", "is_gas": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03ad\u03c1\u03b9\u03bf {entity_name}", diff --git a/homeassistant/components/sleepiq/translations/he.json b/homeassistant/components/sleepiq/translations/he.json index 49f37a267d0..33d3b9421f1 100644 --- a/homeassistant/components/sleepiq/translations/he.json +++ b/homeassistant/components/sleepiq/translations/he.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/subaru/translations/pl.json b/homeassistant/components/subaru/translations/pl.json index 8bc9976fa2e..b0d491d475e 100644 --- a/homeassistant/components/subaru/translations/pl.json +++ b/homeassistant/components/subaru/translations/pl.json @@ -35,7 +35,7 @@ "data": { "update_enabled": "W\u0142\u0105cz odpytywanie pojazdu" }, - "description": "Po w\u0142\u0105czeniu, odpytywanie pojazdu b\u0119dzie co 2 godziny wysy\u0142a\u0107 zdalne polecenie do pojazdu w celu uzyskania nowych danych z czujnika. Bez odpytywania pojazdu, nowe dane z czujnika s\u0105 odbierane tylko wtedy, gdy pojazd automatycznie przesy\u0142a dane (zwykle po wy\u0142\u0105czeniu silnika).", + "description": "Po w\u0142\u0105czeniu, odpytywanie pojazdu b\u0119dzie co 2 godziny wysy\u0142a\u0107 zdalne polecenie do pojazdu w celu uzyskania nowych danych z sensora. Bez odpytywania pojazdu, nowe dane z sensora s\u0105 odbierane tylko wtedy, gdy pojazd automatycznie przesy\u0142a dane (zwykle po wy\u0142\u0105czeniu silnika).", "title": "Opcje Subaru Starlink" } } diff --git a/homeassistant/components/switch/translations/el.json b/homeassistant/components/switch/translations/el.json index 2c2903fdd6d..2067276af42 100644 --- a/homeassistant/components/switch/translations/el.json +++ b/homeassistant/components/switch/translations/el.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c6\u03ce\u03c4\u03c9\u03bd." + } + } + }, "device_automation": { "action_type": { "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}", diff --git a/homeassistant/components/switch/translations/he.json b/homeassistant/components/switch/translations/he.json index ea387cf7c67..c3d76bb6f9f 100644 --- a/homeassistant/components/switch/translations/he.json +++ b/homeassistant/components/switch/translations/he.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d2 \u05e2\u05d1\u05d5\u05e8 \u05de\u05ea\u05d2 \u05d4\u05d0\u05d5\u05e8." + } + } + }, "device_automation": { "action_type": { "toggle": "\u05d4\u05d7\u05dc\u05e4\u05ea \u05de\u05e6\u05d1 {entity_name}", diff --git a/homeassistant/components/switch/translations/hu.json b/homeassistant/components/switch/translations/hu.json index 4b1dc0712bd..423a48f345d 100644 --- a/homeassistant/components/switch/translations/hu.json +++ b/homeassistant/components/switch/translations/hu.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Kapcsol\u00f3 entit\u00e1s" + }, + "description": "V\u00e1lassza ki a kapcsol\u00f3t a vil\u00e1g\u00edt\u00e1shoz." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name} kapcsol\u00e1sa", diff --git a/homeassistant/components/switch/translations/pl.json b/homeassistant/components/switch/translations/pl.json index 9132a6c6b9e..51ecfc44c5d 100644 --- a/homeassistant/components/switch/translations/pl.json +++ b/homeassistant/components/switch/translations/pl.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Encja prze\u0142\u0105cznika" + }, + "description": "Wybierz prze\u0142\u0105cznik do w\u0142\u0105cznika \u015bwiat\u0142a." + } + } + }, "device_automation": { "action_type": { "toggle": "prze\u0142\u0105cz {entity_name}", diff --git a/homeassistant/components/switch/translations/tr.json b/homeassistant/components/switch/translations/tr.json index 421dca0df67..2a98d2e972f 100644 --- a/homeassistant/components/switch/translations/tr.json +++ b/homeassistant/components/switch/translations/tr.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Varl\u0131\u011f\u0131 de\u011fi\u015ftir" + }, + "description": "I\u015f\u0131k anahtar\u0131 i\u00e7in anahtar\u0131 se\u00e7in." + } + } + }, "device_automation": { "action_type": { "toggle": "{entity_name} de\u011fi\u015ftir", diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 87168fcce41..02e2dc94776 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -6,7 +6,8 @@ "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "login_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea ({code}): {msg}" }, "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea Tuya", "step": { @@ -52,9 +53,15 @@ "brightness_range_mode": "\u05d8\u05d5\u05d5\u05d7 \u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df", "max_kelvin": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e6\u05d1\u05e2 \u05de\u05e8\u05d1\u05d9\u05ea \u05d4\u05e0\u05ea\u05de\u05db\u05ea \u05d1\u05e7\u05dc\u05d5\u05d5\u05d9\u05df", "min_kelvin": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e6\u05d1\u05e2 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9\u05ea \u05d4\u05e0\u05ea\u05de\u05db\u05ea \u05d1\u05e7\u05dc\u05d5\u05d5\u05d9\u05df", + "set_temp_divided": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e2\u05e8\u05da \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05de\u05d7\u05d5\u05dc\u05e7 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 '\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea'", "support_color": "\u05db\u05e4\u05d4 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05e6\u05d1\u05e2", + "temp_step_override": "\u05e9\u05dc\u05d1 \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05d4\u05d9\u05e2\u05d3", "unit_of_measurement": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df" - } + }, + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df \u05d8\u05d5\u05d9\u05d4" + }, + "init": { + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05d5\u05d9\u05d4" } } } diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json index eacaecacb07..f31e63515c8 100644 --- a/homeassistant/components/tuya/translations/select.he.json +++ b/homeassistant/components/tuya/translations/select.he.json @@ -1,10 +1,12 @@ { "state": { "tuya__basic_anti_flickr": { + "0": "\u05de\u05d5\u05e9\u05d1\u05ea", "1": "50 \u05d4\u05e8\u05e5", "2": "60 \u05d4\u05e8\u05e5" }, "tuya__basic_nightvision": { + "0": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", "1": "\u05db\u05d1\u05d5\u05d9", "2": "\u05de\u05d5\u05e4\u05e2\u05dc" }, @@ -25,6 +27,10 @@ "back": "\u05d7\u05d6\u05d5\u05e8", "forward": "\u05e7\u05d3\u05d9\u05de\u05d4" }, + "tuya__decibel_sensitivity": { + "0": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05e0\u05de\u05d5\u05db\u05d4", + "1": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05d2\u05d1\u05d5\u05d4\u05d4" + }, "tuya__fan_angle": { "30": "30\u00b0", "60": "60\u00b0", @@ -60,17 +66,68 @@ "sleep": "\u05e9\u05d9\u05e0\u05d4", "work": "\u05e2\u05d1\u05d5\u05d3\u05d4" }, + "tuya__ipc_work_mode": { + "0": "\u05de\u05e6\u05d1 \u05e6\u05e8\u05d9\u05db\u05ea \u05d7\u05e9\u05de\u05dc \u05e0\u05de\u05d5\u05db\u05d4", + "1": "\u05de\u05e6\u05d1 \u05e2\u05d1\u05d5\u05d3\u05d4 \u05de\u05ea\u05de\u05e9\u05da" + }, "tuya__led_type": { + "halogen": "\u05d4\u05dc\u05d5\u05d2\u05df", + "incandescent": "\u05dc\u05d9\u05d1\u05d5\u05df", "led": "\u05dc\u05d3" }, "tuya__light_mode": { - "none": "\u05db\u05d1\u05d5\u05d9" + "none": "\u05db\u05d1\u05d5\u05d9", + "pos": "\u05e6\u05d9\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05dd \u05de\u05ea\u05d2", + "relay": "\u05e6\u05d9\u05d5\u05df \u05de\u05e6\u05d1 \u05d4\u05e4\u05e2\u05dc\u05d4/\u05db\u05d9\u05d1\u05d5\u05d9 \u05e9\u05dc \u05de\u05ea\u05d2" + }, + "tuya__motion_sensitivity": { + "0": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05e0\u05de\u05d5\u05db\u05d4", + "1": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05d1\u05d9\u05e0\u05d5\u05e0\u05d9\u05ea", + "2": "\u05e8\u05d2\u05d9\u05e9\u05d5\u05ea \u05d2\u05d1\u05d5\u05d4\u05d4" + }, + "tuya__record_mode": { + "1": "\u05d4\u05e7\u05dc\u05d8 \u05d0\u05d9\u05e8\u05d5\u05e2\u05d9\u05dd \u05d1\u05dc\u05d1\u05d3", + "2": "\u05d4\u05e7\u05dc\u05d8\u05d4 \u05e8\u05e6\u05d9\u05e4\u05d4" }, "tuya__relay_status": { + "last": "\u05d6\u05db\u05d5\u05e8 \u05de\u05e6\u05d1 \u05d0\u05d7\u05e8\u05d5\u05df", + "memory": "\u05d6\u05db\u05d5\u05e8 \u05de\u05e6\u05d1 \u05d0\u05d7\u05e8\u05d5\u05df", "off": "\u05db\u05d1\u05d5\u05d9", "on": "\u05de\u05d5\u05e4\u05e2\u05dc", "power_off": "\u05db\u05d1\u05d5\u05d9", "power_on": "\u05de\u05d5\u05e4\u05e2\u05dc" + }, + "tuya__vacuum_cistern": { + "closed": "\u05e1\u05d2\u05d5\u05e8", + "high": "\u05d2\u05d1\u05d5\u05d4", + "low": "\u05e0\u05de\u05d5\u05da", + "middle": "\u05d0\u05de\u05e6\u05e2" + }, + "tuya__vacuum_collection": { + "large": "\u05d2\u05d3\u05d5\u05dc", + "middle": "\u05d0\u05de\u05e6\u05e2", + "small": "\u05e7\u05d8\u05df" + }, + "tuya__vacuum_mode": { + "bow": "\u05e7\u05e9\u05ea", + "chargego": "\u05d7\u05d5\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4", + "left_bow": "\u05e7\u05e9\u05ea \u05e9\u05de\u05d0\u05dc\u05d4", + "left_spiral": "\u05e1\u05e4\u05d9\u05e8\u05dc\u05d4 \u05e9\u05de\u05d0\u05dc\u05d4", + "mop": "\u05e1\u05d7\u05d1\u05d4", + "part": "\u05d7\u05dc\u05e7", + "partial_bow": "\u05e7\u05e9\u05ea \u05d7\u05dc\u05e7\u05d9\u05ea", + "pick_zone": "\u05d1\u05d7\u05e8 \u05d0\u05d6\u05d5\u05e8", + "point": "\u05e0\u05e7\u05d5\u05d3\u05d4", + "pose": "\u05e4\u05d5\u05d6\u05d4", + "random": "\u05d0\u05e7\u05e8\u05d0\u05d9", + "right_bow": "\u05e7\u05e9\u05ea \u05d9\u05de\u05d9\u05e0\u05d4", + "right_spiral": "\u05e1\u05e4\u05d9\u05e8\u05dc\u05d4 \u05d9\u05de\u05d9\u05e0\u05d4", + "single": "\u05d9\u05d7\u05d9\u05d3", + "smart": "\u05d7\u05db\u05dd", + "spiral": "\u05e1\u05e4\u05d9\u05e8\u05dc\u05d4", + "standby": "\u05d4\u05de\u05ea\u05e0\u05d4", + "wall_follow": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05e7\u05d9\u05e8", + "zone": "\u05d0\u05d6\u05d5\u05e8" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.he.json b/homeassistant/components/tuya/translations/sensor.he.json index 73e5af2fc79..95664a2bd66 100644 --- a/homeassistant/components/tuya/translations/sensor.he.json +++ b/homeassistant/components/tuya/translations/sensor.he.json @@ -2,7 +2,20 @@ "state": { "tuya__air_quality": { "good": "\u05d8\u05d5\u05d1", - "great": "\u05e0\u05d4\u05d3\u05e8" + "great": "\u05e0\u05d4\u05d3\u05e8", + "mild": "\u05de\u05ea\u05d5\u05df", + "severe": "\u05d7\u05de\u05d5\u05e8" + }, + "tuya__status": { + "boiling_temp": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e8\u05ea\u05d9\u05d7\u05d4", + "cooling": "\u05e7\u05d9\u05e8\u05d5\u05e8", + "heating": "\u05d7\u05d9\u05de\u05d5\u05dd", + "heating_temp": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05d7\u05d9\u05de\u05d5\u05dd", + "reserve_1": "\u05e9\u05de\u05d5\u05e8 1", + "reserve_2": "\u05e9\u05de\u05d5\u05e8 2", + "reserve_3": "\u05e9\u05de\u05d5\u05e8 3", + "standby": "\u05d4\u05de\u05ea\u05e0\u05d4", + "warm": "\u05e9\u05d9\u05de\u05d5\u05e8 \u05d7\u05d5\u05dd" } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index 72460919989..ef879805546 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -51,7 +51,7 @@ "disable_rtsp": "Wy\u0142\u0105cz strumie\u0144 RTSP", "override_connection_host": "Zast\u0105p host po\u0142\u0105czenia" }, - "description": "Opcja metryk w czasie rzeczywistym powinna by\u0107 w\u0142\u0105czona tylko wtedy, gdy w\u0142\u0105czono czujniki diagnostyczne i chcesz je aktualizowa\u0107 w czasie rzeczywistym. Je\u015bli nie s\u0105 w\u0142\u0105czone, b\u0119d\u0105 aktualizowane tylko raz na 15 minut.", + "description": "Opcja metryk w czasie rzeczywistym powinna by\u0107 w\u0142\u0105czona tylko wtedy, gdy w\u0142\u0105czono sensory diagnostyczne i chcesz je aktualizowa\u0107 w czasie rzeczywistym. Je\u015bli nie s\u0105 w\u0142\u0105czone, b\u0119d\u0105 aktualizowane tylko raz na 15 minut.", "title": "Opcje UniFi Protect" } } diff --git a/homeassistant/components/update/translations/hu.json b/homeassistant/components/update/translations/hu.json new file mode 100644 index 00000000000..1e2ec425a88 --- /dev/null +++ b/homeassistant/components/update/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "Friss\u00edt\u00e9s" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/pl.json b/homeassistant/components/update/translations/pl.json new file mode 100644 index 00000000000..eff0431a518 --- /dev/null +++ b/homeassistant/components/update/translations/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Aktualizacja" +} \ No newline at end of file From 74c339eb7b70737b5c6d5a1325294002fe979a2b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 7 Mar 2022 00:44:32 -0800 Subject: [PATCH 0271/1054] Simplify google calendar new calendar tracking (#67772) * Simplify google calendar new calendar tracking * Remove application of CONFIG_SCHEMA --- homeassistant/components/google/__init__.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 08f6bf247f4..be973ac9b45 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -33,7 +33,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert from .api import GoogleCalendarService @@ -51,7 +50,6 @@ CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_MAX_RESULTS = "max_results" CONF_CALENDAR_ACCESS = "calendar_access" -DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_OFFSET = "!!" EVENT_CALENDAR_ID = "calendar_id" @@ -106,7 +104,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( FeatureAccess ), @@ -279,7 +277,6 @@ def setup_services( hass: HomeAssistant, hass_config: ConfigType, config: ConfigType, - track_new_found_calendars: bool, calendar_service: GoogleCalendarService, ) -> None: """Set up the service listeners.""" @@ -306,7 +303,7 @@ def setup_services( """Scan for new calendars.""" calendars = calendar_service.list_calendars() for calendar in calendars: - calendar["track"] = track_new_found_calendars + calendar[CONF_TRACK] = config[CONF_TRACK_NEW] hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) hass.services.register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) @@ -371,13 +368,7 @@ def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) - hass.data[DOMAIN][DATA_CALENDARS] = calendars calendar_service = hass.data[DOMAIN][DATA_SERVICE] - track_new_found_calendars = convert( - config.get(CONF_TRACK_NEW), bool, DEFAULT_CONF_TRACK_NEW - ) - assert track_new_found_calendars is not None - setup_services( - hass, hass_config, config, track_new_found_calendars, calendar_service - ) + setup_services(hass, hass_config, config, calendar_service) for calendar in calendars.values(): discovery.load_platform(hass, Platform.CALENDAR, DOMAIN, calendar, hass_config) From 6b4e9374b76280e3a7a6b3c93e6f62d39cad984d Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Mon, 7 Mar 2022 09:14:05 +0000 Subject: [PATCH 0272/1054] Fix timezone for growatt lastdataupdate (#67684) * Added timezone for growatt lastdataupdate (#67646) * Growatt lastdataupdate set to local timezone --- homeassistant/components/growatt_server/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 67095492de7..db045242987 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -222,7 +222,7 @@ class GrowattData: date_now = dt.now().date() last_updated_time = dt.parse_time(str(sorted_keys[-1])) mix_detail["lastdataupdate"] = datetime.datetime.combine( - date_now, last_updated_time + date_now, last_updated_time, dt.DEFAULT_TIME_ZONE ) # Dashboard data is largely inaccurate for mix system but it is the only call with the ability to return the combined From d656acfa2c8d044a68dc05975f659032e3bda311 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:08:50 +0100 Subject: [PATCH 0273/1054] Bump async-upnp-client to 0.26.0 (#67760) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 76bf8ac051c..3560b8c61b8 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.25.0"], + "requirements": ["async-upnp-client==0.26.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 9f4d02d4462..c8d30dc9c08 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.25.0"], + "requirements": ["async-upnp-client==0.26.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index a4e02759989..3ed1bb6e934 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.25.0"], + "requirements": ["async-upnp-client==0.26.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 19b0e4529a4..4ebec2fae92 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.25.0"], + "requirements": ["async-upnp-client==0.26.0"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index b6defca2060..89f065d72e1 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.9", "async-upnp-client==0.25.0"], + "requirements": ["yeelight==0.7.9", "async-upnp-client==0.26.0"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 33fa40b1766..94d35e523c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.8 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.25.0 +async-upnp-client==0.26.0 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 38f866acf8e..2b06e14b277 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -323,7 +323,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.25.0 +async-upnp-client==0.26.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a75bf20ffe..dd38c057c22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -256,7 +256,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.25.0 +async-upnp-client==0.26.0 # homeassistant.components.sleepiq asyncsleepiq==1.1.2 From 9a6f5bbc0496fe6042b14d530ed60bb114287d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 7 Mar 2022 11:11:25 +0100 Subject: [PATCH 0274/1054] Update whoami URL (#67793) --- homeassistant/util/location.py | 4 ++-- tests/util/test_location.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 4e76fa32de3..e653b439e0e 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -14,8 +14,8 @@ import aiohttp from homeassistant.const import __version__ as HA_VERSION -WHOAMI_URL = "https://whoami.home-assistant.io/v1" -WHOAMI_URL_DEV = "https://whoami-v1-dev.home-assistant.workers.dev/v1" +WHOAMI_URL = "https://services.home-assistant.io/whoami/v1" +WHOAMI_URL_DEV = "https://services-dev.home-assistant.workers.dev/whoami/v1" # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 3dff36744ee..d8d86965733 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -74,7 +74,7 @@ def test_get_miles(): async def test_detect_location_info_whoami(aioclient_mock, session): - """Test detect location info using whoami.home-assistant.io.""" + """Test detect location info using services.home-assistant.io/whoami.""" aioclient_mock.get(location_util.WHOAMI_URL, text=load_fixture("whoami.json")) with patch("homeassistant.util.location.HA_VERSION", "1.0"): From 68f932f6c3ef26e7d6363a5c43ddb00b092dfdf7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Mar 2022 11:15:49 +0100 Subject: [PATCH 0275/1054] Bump samsungtvws to 2.2.0 (#67771) --- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a536169de90..bc3f3077365 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws[async]==2.1.0", + "samsungtvws[async]==2.2.0", "wakeonlan==2.0.1" ], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2b06e14b277..6f1004169ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.1.0 +samsungtvws[async]==2.2.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd38c057c22..411151b7d06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.1.0 +samsungtvws[async]==2.2.0 # homeassistant.components.dhcp scapy==2.4.5 From 6a92081e831262a431345a42f23181af9ecdd844 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 7 Mar 2022 05:43:21 -0500 Subject: [PATCH 0276/1054] Add strict typing to adguard (#67775) --- .strict-typing | 1 + homeassistant/components/adguard/config_flow.py | 2 +- homeassistant/components/adguard/switch.py | 5 +++-- mypy.ini | 11 +++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1213a8b73b7..6796c5f3866 100644 --- a/.strict-typing +++ b/.strict-typing @@ -32,6 +32,7 @@ homeassistant.components.abode.* homeassistant.components.acer_projector.* homeassistant.components.accuweather.* homeassistant.components.actiontec.* +homeassistant.components.adguard.* homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index c7cb261d7c8..bc9b11c9a72 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -27,7 +27,7 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - _hassio_discovery = None + _hassio_discovery: dict[str, Any] | None = None async def _show_setup_form( self, errors: dict[str, str] | None = None diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index f6c41e7f2e8..0cb7a48fee6 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError @@ -76,7 +77,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Return the state of the switch.""" return self._state - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: await self._adguard_turn_off() @@ -88,7 +89,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Turn off the switch.""" raise NotImplementedError() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: await self._adguard_turn_on() diff --git a/mypy.ini b/mypy.ini index 7a927a3c6a7..2bb0f5b5462 100644 --- a/mypy.ini +++ b/mypy.ini @@ -153,6 +153,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.adguard.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aftership.*] check_untyped_defs = true disallow_incomplete_defs = true From a9cc2d23221a58a3bdd59d1a21ae291369a3f152 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Mar 2022 13:05:04 +0100 Subject: [PATCH 0277/1054] Add config flow for cover, fan, light and media_player groups (#67660) * Add options flow support to HelperConfigFlowHandler * Add config flow for cover, fan, light and media_player groups * Update according to review comments * Update translation strings * Update translation strings * Copy schema before adding suggested values --- homeassistant/components/group/__init__.py | 28 +++ homeassistant/components/group/config_flow.py | 81 ++++++++ homeassistant/components/group/cover.py | 19 +- homeassistant/components/group/fan.py | 17 +- homeassistant/components/group/light.py | 19 +- homeassistant/components/group/manifest.json | 7 +- .../components/group/media_player.py | 19 +- homeassistant/components/group/strings.json | 62 ++++++ homeassistant/generated/config_flows.py | 1 + .../helpers/helper_config_entry_flow.py | 118 ++++++++++- tests/components/group/test_config_flow.py | 191 ++++++++++++++++++ 11 files changed, 546 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/group/config_flow.py create mode 100644 tests/components/group/test_config_flow.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 8e595d75db6..e5dbb3c3630 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -11,6 +11,7 @@ from typing import Any, Union, cast import voluptuous as vol from homeassistant import core as ha +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -58,6 +59,14 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" +PLATFORMS_CONFIG_ENTRY = [ + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.MEDIA_PLAYER, +] + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, @@ -218,6 +227,25 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: return groups +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + hass.config_entries.async_setup_platforms(entry, (entry.options["group_type"],)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options["group_type"],) + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all groups found defined in the configuration.""" if DOMAIN not in hass.data: diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py new file mode 100644 index 00000000000..82de2056da5 --- /dev/null +++ b/homeassistant/components/group/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Group integration.""" +from __future__ import annotations + +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITIES +from homeassistant.helpers import helper_config_entry_flow, selector + +from . import DOMAIN + + +def basic_group_options_schema(domain: str) -> vol.Schema: + """Generate options schema.""" + return vol.Schema( + { + vol.Required(CONF_ENTITIES): selector.selector( + {"entity": {"domain": domain, "multiple": True}} + ), + } + ) + + +def basic_group_config_schema(domain: str) -> vol.Schema: + """Generate config schema.""" + return vol.Schema({vol.Required("name"): selector.selector({"text": {}})}).extend( + basic_group_options_schema(domain).schema + ) + + +STEPS = { + "init": vol.Schema( + { + vol.Required("group_type"): selector.selector( + { + "select": { + "options": [ + "cover", + "fan", + "light", + "media_player", + ] + } + } + ) + } + ), + "cover": basic_group_config_schema("cover"), + "fan": basic_group_config_schema("fan"), + "light": basic_group_config_schema("light"), + "media_player": basic_group_config_schema("media_player"), + "cover_options": basic_group_options_schema("cover"), + "fan_options": basic_group_options_schema("fan"), + "light_options": basic_group_options_schema("light"), + "media_player_options": basic_group_options_schema("media_player"), +} + + +class GroupConfigFlowHandler( + helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN +): + """Handle a config or options flow for Switch Light.""" + + steps = STEPS + + def async_config_entry_title(self, user_input: dict[str, Any]) -> str: + """Return config entry title.""" + return cast(str, user_input["name"]) if "name" in user_input else "" + + @staticmethod + def async_initial_options_step(config_entry: ConfigEntry) -> str: + """Return initial options step.""" + return f"{config_entry.options['group_type']}_options" + + def async_next_step(self, step_id: str, user_input: dict[str, Any]) -> str | None: + """Return next step_id.""" + if step_id == "init": + return cast(str, user_input["group_type"]) + return None diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index a4c550b8119..851079ad5e4 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -22,6 +22,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -43,7 +44,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -85,6 +86,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [CoverGroup(config_entry.entry_id, config_entry.title, entity_id)] + ) + + class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 7920e0f5d20..f26b1bf57fb 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -25,6 +25,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -35,7 +36,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -78,6 +79,20 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entity_id)]) + + class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index ea74136b204..c51baf0ff66 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -36,6 +36,7 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -48,7 +49,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Event, HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -92,6 +93,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [LightGroup(config_entry.entry_id, config_entry.title, entity_id)] + ) + + FORWARDED_ATTRIBUTES = frozenset( { ATTR_BRIGHTNESS, diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 6d8fd446c27..9c97f318da3 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -2,7 +2,10 @@ "domain": "group", "name": "Group", "documentation": "https://www.home-assistant.io/integrations/group", - "codeowners": ["@home-assistant/core"], + "codeowners": [ + "@home-assistant/core" + ], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 509c0cb4083..e1c044fd3e3 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -32,6 +32,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_STEP, MediaPlayerEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -55,7 +56,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, EventType @@ -96,6 +97,22 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + + async_add_entities( + [MediaGroup(config_entry.entry_id, config_entry.title, entity_id)] + ) + + class MediaGroup(MediaPlayerEntity): """Representation of a Media Group.""" diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index e29407bf932..8f0531ac7e4 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -1,5 +1,67 @@ { "title": "Group", + "config": { + "step": { + "init": { + "description": "Select group type", + "data": { + "group_type": "Group type" + } + }, + "cover": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "cover_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + }, + "fan": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "fan_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + }, + "light": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "light_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + }, + "media_player": { + "description": "Select group options", + "data": { + "entities": "Group members", + "name": "Group name" + } + }, + "media_player_options": { + "description": "Select group options", + "data": { + "entities": "Group members" + } + } + } + }, "state": { "_": { "off": "[%key:common::state::off%]", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2ee6e235e91..379adad2b93 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -127,6 +127,7 @@ FLOWS = [ "google_travel_time", "gpslogger", "gree", + "group", "growatt_server", "guardian", "habitica", diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 82d10868d01..c632ad60eae 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -2,13 +2,20 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Awaitable, Callable +import copy +import types from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResult +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import ( + RESULT_TYPE_CREATE_ENTRY, + FlowResult, + UnknownHandler, +) class HelperCommonFlowHandler: @@ -16,19 +23,18 @@ class HelperCommonFlowHandler: def __init__( self, - handler: HelperConfigFlowHandler, + handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" self._handler = handler self._options = dict(config_entry.options) if config_entry is not None else {} - async def async_step(self, _user_input: dict[str, Any] | None = None) -> FlowResult: + async def async_step( + self, step_id: str, _user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a step.""" errors = None - step_id = ( - self._handler.cur_step["step_id"] if self._handler.cur_step else "init" - ) if _user_input is not None: errors = {} try: @@ -38,19 +44,28 @@ class HelperCommonFlowHandler: except vol.Invalid as exc: errors["base"] = str(exc) else: + self._options.update(user_input) if ( next_step_id := self._handler.async_next_step(step_id, user_input) ) is None: title = self._handler.async_config_entry_title(user_input) return self._handler.async_create_entry( - title=title, data=user_input + title=title, data=self._options ) return self._handler.async_show_form( step_id=next_step_id, data_schema=self._handler.steps[next_step_id] ) + schema = dict(self._handler.steps[step_id].schema) + for key in list(schema): + if key in self._options and isinstance(key, vol.Marker): + new_key = copy.copy(key) + new_key.description = {"suggested_value": self._options[key]} + val = schema.pop(key) + schema[new_key] = val + return self._handler.async_show_form( - step_id=step_id, data_schema=self._handler.steps[step_id], errors=errors + step_id=step_id, data_schema=vol.Schema(schema), errors=errors ) @@ -66,6 +81,29 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) + @callback + def _async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + if ( + cls.async_initial_options_step + is HelperConfigFlowHandler.async_initial_options_step + ): + raise UnknownHandler + + return HelperOptionsFlowHandler( + config_entry, + cls.steps, + cls.async_config_entry_title, + cls.async_initial_options_step, + cls.async_next_step, + cls.async_validate_input, + ) + + # Create an async_get_options_flow method + cls.async_get_options_flow = _async_get_options_flow # type: ignore[assignment] + # Create flow step methods for each step defined in the flow schema for step in cls.steps: setattr(cls, f"async_step_{step}", cls.async_step) @@ -73,6 +111,17 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Initialize config flow.""" self._common_handler = HelperCommonFlowHandler(self, None) + @classmethod + @callback + def async_supports_options_flow( + cls, config_entry: config_entries.ConfigEntry + ) -> bool: + """Return options flow support for this handler.""" + return ( + cls.async_initial_options_step + is not HelperConfigFlowHandler.async_initial_options_step + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -81,7 +130,8 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: """Handle a step.""" - result = await self._common_handler.async_step(user_input) + step_id = self.cur_step["step_id"] if self.cur_step else "init" + result = await self._common_handler.async_step(step_id, user_input) if result["type"] == RESULT_TYPE_CREATE_ENTRY: result["options"] = result["data"] result["data"] = {} @@ -97,9 +147,57 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Return next step_id, or None to finish the flow.""" return None + @staticmethod + @callback + def async_initial_options_step( + config_entry: config_entries.ConfigEntry, + ) -> str: + """Return initial step_id of options flow.""" + raise UnknownHandler + # pylint: disable-next=no-self-use async def async_validate_input( self, hass: HomeAssistant, step_id: str, user_input: dict[str, Any] ) -> dict[str, Any]: """Validate user input.""" return user_input + + +class HelperOptionsFlowHandler(config_entries.OptionsFlow): + """Handle an options flow for helper integrations.""" + + def __init__( + self, + config_entry: config_entries.ConfigEntry, + steps: dict[str, vol.Schema], + config_entry_title: Callable[[Any, dict[str, Any]], str], + initial_step: Callable[[config_entries.ConfigEntry], str], + next_step: Callable[[Any, str, dict[str, Any]], str | None], + validate: Callable[ + [Any, HomeAssistant, str, dict[str, Any]], Awaitable[dict[str, Any]] + ], + ) -> None: + """Initialize options flow.""" + self._common_handler = HelperCommonFlowHandler(self, config_entry) + self._config_entry = config_entry + self._initial_step = initial_step(config_entry) + self.async_config_entry_title = types.MethodType(config_entry_title, self) + self.async_next_step = types.MethodType(next_step, self) + self.async_validate_input = types.MethodType(validate, self) + self.steps = steps + for step in self.steps: + if step == "init": + continue + setattr(self, f"async_step_{step}", self.async_step) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + return await self.async_step(user_input) + + async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle a step.""" + # pylint: disable-next=unsubscriptable-object # self.cur_step is a dict + step_id = self.cur_step["step_id"] if self.cur_step else self._initial_step + return await self._common_handler.async_step(step_id, user_input) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py new file mode 100644 index 00000000000..cc97ff8c95f --- /dev/null +++ b/tests/components/group/test_config_flow.py @@ -0,0 +1,191 @@ +"""Test the Switch config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.group import DOMAIN, async_setup_entry +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +@pytest.mark.parametrize( + "group_type,group_state,member_state,member_attributes", + ( + ("cover", "open", "open", {}), + ("fan", "on", "on", {}), + ("light", "on", "on", {}), + ("media_player", "on", "on", {}), + ), +) +async def test_config_flow( + hass: HomeAssistant, group_type, group_state, member_state, member_attributes +) -> None: + """Test the config flow.""" + members = [f"{group_type}.one", f"{group_type}.two"] + for member in members: + hass.states.async_set(member, member_state, member_attributes) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"group_type": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + with patch( + "homeassistant.components.group.async_setup_entry", wraps=async_setup_entry + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Living Room", + "entities": members, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Living Room" + assert result["data"] == {} + assert result["options"] == { + "group_type": group_type, + "entities": members, + "name": "Living Room", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "group_type": group_type, + "name": "Living Room", + "entities": members, + } + + state = hass.states.get(f"{group_type}.living_room") + assert state.state == group_state + assert state.attributes["entity_id"] == members + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize( + "group_type,member_state", + (("cover", "open"), ("fan", "on"), ("light", "on"), ("media_player", "on")), +) +async def test_options(hass: HomeAssistant, group_type, member_state) -> None: + """Test reconfiguring.""" + members1 = [f"{group_type}.one", f"{group_type}.two"] + members2 = [f"{group_type}.four", f"{group_type}.five"] + + for member in members1: + hass.states.async_set(member, member_state, {}) + for member in members2: + hass.states.async_set(member, member_state, {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + assert get_suggested(result["data_schema"].schema, "group_type") is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"group_type": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + assert get_suggested(result["data_schema"].schema, "entities") is None + assert get_suggested(result["data_schema"].schema, "name") is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Bed Room", + "entities": members1, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "group_type": group_type, + "entities": members1, + "name": "Bed Room", + } + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == f"{group_type}_options" + assert get_suggested(result["data_schema"].schema, "entities") == members1 + assert "name" not in result["data_schema"].schema + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members2, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "group_type": group_type, + "entities": members2, + "name": "Bed Room", + } + assert config_entry.data == {} + assert config_entry.options == { + "group_type": group_type, + "entities": members2, + "name": "Bed Room", + } + assert config_entry.title == "Bed Room" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members2 + + # Check we don't get suggestions from another entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + assert get_suggested(result["data_schema"].schema, "group_type") is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"group_type": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + assert get_suggested(result["data_schema"].schema, "entities") is None + assert get_suggested(result["data_schema"].schema, "name") is None From 2f25d52f69be2ea249c21bf80fec9aca4a88a194 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Mon, 7 Mar 2022 14:13:51 +0100 Subject: [PATCH 0278/1054] Fix Jellyfin erroring on media items without a source (#67697) * Fix erroring on media items with a source * code style improvement --- homeassistant/components/jellyfin/media_source.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 22bb2b3937d..dbd79612378 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -324,6 +324,9 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" + if not media_item[ITEM_KEY_MEDIA_SOURCES]: + raise BrowseError("Unable to determine mime type for item without media source") + media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) From 98adeb607008a17ecff81d31b143c47fb8fecfeb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Mar 2022 15:38:33 +0100 Subject: [PATCH 0279/1054] Fix false positive MQTT climate deprecation warnings for defaults (#67661) Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/climate.py | 32 +++++-- tests/components/mqtt/test_climate.py | 113 +++++++++++++++++++++++ 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index e145edde7d7..94320cc5def 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -271,7 +271,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list, + vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( @@ -298,7 +298,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( ), vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, + vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together @@ -431,6 +431,12 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + self._send_if_off = True + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + self._hold_list = [] + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -499,6 +505,15 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates + # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 + if CONF_SEND_IF_OFF in config: + self._send_if_off = config[CONF_SEND_IF_OFF] + + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + if CONF_HOLD_LIST in config: + self._hold_list = config[CONF_HOLD_LIST] + def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -806,7 +821,9 @@ class MqttClimate(MqttEntity, ClimateEntity): ): presets.append(PRESET_AWAY) - presets.extend(self._config[CONF_HOLD_LIST]) + # AWAY and HOLD mode topics and templates are deprecated, + # support will be removed with release 2022.9 + presets.extend(self._hold_list) if presets: presets.insert(0, PRESET_NONE) @@ -847,10 +864,7 @@ class MqttClimate(MqttEntity, ClimateEntity): setattr(self, attr, temp) # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if ( - self._config[CONF_SEND_IF_OFF] - or self._current_operation != HVAC_MODE_OFF - ): + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) @@ -890,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( swing_mode ) @@ -903,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVAC_MODE_OFF: payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c3501267e12..93249e76875 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -333,6 +333,43 @@ async def test_set_fan_mode(hass, mqtt_mock): assert state.attributes.get("fan_mode") == "high" +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("fan-mode-topic", "low", 0, False)]), + ({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_fan_mode_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of fan mode if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for fan_mode should be sent when the device is turned on + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -385,6 +422,43 @@ async def test_set_swing(hass, mqtt_mock): assert state.attributes.get("swing_mode") == "on" +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("swing-mode-topic", "on", 0, False)]), + ({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_swing_mode_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of swing mode if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for swing_mode should be sent when the device is turned on + await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_target_temperature(hass, mqtt_mock): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -421,6 +495,45 @@ async def test_set_target_temperature(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() +# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +@pytest.mark.parametrize( + "send_if_off,assert_async_publish", + [ + ({}, [call("temperature-topic", "21.0", 0, False)]), + ({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]), + ({"send_if_off": False}, []), + ], +) +async def test_set_target_temperature_send_if_off( + hass, mqtt_mock, send_if_off, assert_async_publish +): + """Test setting of target temperature if the hvac is off.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[CLIMATE_DOMAIN].update(send_if_off) + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_CLIMATE) is not None + + # Turn on HVAC + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + # Updates for target temperature should be sent when the device is turned on + await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "temperature-topic", "16.0", 0, False + ) + + # Turn off HVAC + await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + # Updates for target temperature sent should be if SEND_IF_OFF is not set or is True + mqtt_mock.async_publish.reset_mock() + await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls(assert_async_publish) + + async def test_set_target_temperature_pessimistic(hass, mqtt_mock): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) From 405c2f9cf35d00a2ce82db1753540170e8d04c02 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Mar 2022 17:23:08 +0100 Subject: [PATCH 0280/1054] Fix internet access switch for old discovery (#67777) --- homeassistant/components/fritz/common.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 4c307c126cd..21039d45afa 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -392,6 +392,8 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): ) self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): + if info.ip_address: + info.wan_access = self._get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True self.send_signal_device_update(new_device) From 99300570f11a7b1d9238003038bdef45274407f0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Mar 2022 17:56:52 +0100 Subject: [PATCH 0281/1054] Update frontend to 20220301.1 (#67812) --- homeassistant/components/frontend/manifest.json | 5 +++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index cc118e23dc9..baf61343040 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220301.0" + "home-assistant-frontend==20220301.1" ], "dependencies": [ "api", @@ -13,7 +13,8 @@ "diagnostics", "http", "lovelace", - "onboarding", "search", + "onboarding", + "search", "system_log", "websocket_api" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 94d35e523c6..25ba71bf1ee 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 6f1004169ef..2b72daff8d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -803,7 +803,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 411151b7d06..7d06a913e9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.0 +home-assistant-frontend==20220301.1 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From d65b2b37dcd3fa68d364aa159cbe14123fc69184 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Mar 2022 18:05:10 +0100 Subject: [PATCH 0282/1054] Fix profile name update for Shelly Valve (#67778) --- homeassistant/components/shelly/climate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 2c81ecbe183..1e7ba2dd183 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -317,4 +317,14 @@ class BlockSleepingClimate( if self.device_block and self.block: _LOGGER.debug("Entity %s attached to blocks", self.name) + + assert self.block.channel + + self._preset_modes = [ + PRESET_NONE, + *self.wrapper.device.settings["thermostats"][int(self.block.channel)][ + "schedule_profile_names" + ], + ] + self.async_write_ha_state() From f268191985bdfcb0eeaff4036879218ed109c0cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Mar 2022 12:10:38 -0500 Subject: [PATCH 0283/1054] Handle fan_modes being set to None in homekit (#67790) --- .../components/homekit/type_thermostats.py | 25 ++-- .../homekit/test_type_thermostats.py | 127 ++++++++++++++++++ 2 files changed, 139 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 8c54896e85e..1e20d1bc710 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -285,20 +285,19 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_HUMIDITY, value=50 ) - fan_modes = self.fan_modes = { - fan_mode.lower(): fan_mode - for fan_mode in attributes.get(ATTR_FAN_MODES, []) - } + fan_modes = {} self.ordered_fan_speeds = [] - if ( - features & SUPPORT_FAN_MODE - and fan_modes - and PRE_DEFINED_FAN_MODES.intersection(fan_modes) - ): - self.ordered_fan_speeds = [ - speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes - ] - self.fan_chars.append(CHAR_ROTATION_SPEED) + + if features & SUPPORT_FAN_MODE: + fan_modes = { + fan_mode.lower(): fan_mode + for fan_mode in attributes.get(ATTR_FAN_MODES) or [] + } + if fan_modes and PRE_DEFINED_FAN_MODES.intersection(fan_modes): + self.ordered_fan_speeds = [ + speed for speed in ORDERED_FAN_SPEEDS if speed in fan_modes + ] + self.fan_chars.append(CHAR_ROTATION_SPEED) if FAN_AUTO in fan_modes and (FAN_ON in fan_modes or self.ordered_fan_speeds): self.fan_chars.append(CHAR_TARGET_FAN_STATE) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d1db618e7e4..5f002fbbf6c 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -15,6 +15,8 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, @@ -74,6 +76,7 @@ from homeassistant.components.homekit.type_thermostats import ( from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, @@ -2349,3 +2352,127 @@ async def test_thermostat_with_fan_modes_with_off(hass, hk_driver, events): assert len(call_set_fan_mode) == 2 assert call_set_fan_mode[-1].data[ATTR_ENTITY_ID] == entity_id assert call_set_fan_mode[-1].data[ATTR_FAN_MODE] == FAN_OFF + + +async def test_thermostat_with_fan_modes_set_to_none(hass, hk_driver, events): + """Test a thermostate with fan modes set to None.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: None, + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + + +async def test_thermostat_with_fan_modes_set_to_none_not_supported( + hass, hk_driver, events +): + """Test a thermostate with fan modes set to None and supported feature missing.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_SWING_MODE, + ATTR_FAN_MODES: None, + ATTR_SWING_MODES: [SWING_BOTH, SWING_OFF, SWING_HORIZONTAL], + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_SWING_MODE: SWING_BOTH, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.ordered_fan_speeds == [] + assert CHAR_ROTATION_SPEED not in acc.fan_chars + assert CHAR_TARGET_FAN_STATE not in acc.fan_chars + assert CHAR_SWING_MODE in acc.fan_chars + assert CHAR_CURRENT_FAN_STATE in acc.fan_chars + + +async def test_thermostat_with_supported_features_target_temp_but_fan_mode_set( + hass, hk_driver, events +): + """Test a thermostate with fan mode and supported feature missing.""" + entity_id = "climate.test" + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE, + ATTR_MIN_TEMP: 44.6, + ATTR_MAX_TEMP: 95, + ATTR_PRESET_MODES: ["home", "away"], + ATTR_TEMPERATURE: 67, + ATTR_TARGET_TEMP_HIGH: None, + ATTR_TARGET_TEMP_LOW: None, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_FAN_MODES: None, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, + ATTR_FAN_MODE: FAN_AUTO, + ATTR_PRESET_MODE: "home", + ATTR_FRIENDLY_NAME: "Rec Room", + ATTR_HVAC_MODES: [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.ordered_fan_speeds == [] + assert not acc.fan_chars From 48e6738367ecb30f3bf77f869d98fd7bc449aefb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Mar 2022 18:14:14 +0100 Subject: [PATCH 0284/1054] Catch Elgato connection errors (#67799) --- homeassistant/components/elgato/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 805a70613f9..f15ccc0a03d 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -1,13 +1,13 @@ """Support for Elgato Lights.""" from typing import NamedTuple -from elgato import Elgato, Info, State +from elgato import Elgato, ElgatoConnectionError, Info, State from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER, SCAN_INTERVAL @@ -31,12 +31,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, ) + async def _async_update_data() -> State: + """Fetch Elgato data.""" + try: + return await elgato.state() + except ElgatoConnectionError as err: + raise UpdateFailed(err) from err + coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator( hass, LOGGER, name=f"{DOMAIN}_{entry.data[CONF_HOST]}", update_interval=SCAN_INTERVAL, - update_method=elgato.state, + update_method=_async_update_data, ) await coordinator.async_config_entry_first_refresh() From 7041bc797a512aa3a76a35b3519f428844950622 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Mar 2022 18:55:12 +0100 Subject: [PATCH 0285/1054] Fix temperature stepping in Sensibo (#67737) Co-authored-by: Paulus Schoutsen --- homeassistant/components/sensibo/coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index eaee8e23c1a..4a03b532149 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -17,6 +17,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT +MAX_POSSIBLE_STEP = 1000 + @dataclass class MotionSensor: @@ -93,7 +95,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): .get("values", [0, 1]) ) if temperatures_list: - temperature_step = temperatures_list[1] - temperatures_list[0] + diff = MAX_POSSIBLE_STEP + for i in range(len(temperatures_list) - 1): + if temperatures_list[i + 1] - temperatures_list[i] < diff: + diff = temperatures_list[i + 1] - temperatures_list[i] + temperature_step = diff active_features = list(ac_states) full_features = set() From 9d42a425fc6d88482161d93f78c5125a3f6059eb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Mar 2022 18:58:29 +0100 Subject: [PATCH 0286/1054] Add config flow to Season (#67413) --- CODEOWNERS | 2 + homeassistant/components/season/__init__.py | 17 +++- .../components/season/config_flow.py | 48 ++++++++++ homeassistant/components/season/const.py | 14 +++ homeassistant/components/season/manifest.json | 5 +- homeassistant/components/season/sensor.py | 53 +++++------ homeassistant/components/season/strings.json | 14 +++ .../components/season/translations/en.json | 14 +++ .../season/translations/sensor.en.json | 6 -- homeassistant/generated/config_flows.py | 1 + tests/components/season/conftest.py | 30 +++++++ tests/components/season/test_config_flow.py | 76 ++++++++++++++++ tests/components/season/test_init.py | 55 ++++++++++++ tests/components/season/test_sensor.py | 88 ++++++++++++------- 14 files changed, 356 insertions(+), 67 deletions(-) create mode 100644 homeassistant/components/season/config_flow.py create mode 100644 homeassistant/components/season/const.py create mode 100644 homeassistant/components/season/strings.json create mode 100644 homeassistant/components/season/translations/en.json create mode 100644 tests/components/season/conftest.py create mode 100644 tests/components/season/test_config_flow.py create mode 100644 tests/components/season/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 1c725643535..79a9241955e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -859,6 +859,8 @@ homeassistant/components/script/* @home-assistant/core tests/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core tests/components/search/* @home-assistant/core +homeassistant/components/season/* @frenck +tests/components/season/* @frenck homeassistant/components/select/* @home-assistant/core tests/components/select/* @home-assistant/core homeassistant/components/sense/* @kbickar diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index 935270486df..6d4a2974522 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -1 +1,16 @@ -"""The season integration.""" +"""The Season integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/season/config_flow.py b/homeassistant/components/season/config_flow.py new file mode 100644 index 00000000000..854c0158439 --- /dev/null +++ b/homeassistant/components/season/config_flow.py @@ -0,0 +1,48 @@ +"""Config flow to configure the Season integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN, TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL + + +class SeasonConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Season.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_TYPE]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={CONF_TYPE: user_input[CONF_TYPE]}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In( + { + TYPE_ASTRONOMICAL: "Astronomical", + TYPE_METEOROLOGICAL: "Meteorological", + } + ) + }, + ), + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/season/const.py b/homeassistant/components/season/const.py new file mode 100644 index 00000000000..c27d4f5c40e --- /dev/null +++ b/homeassistant/components/season/const.py @@ -0,0 +1,14 @@ +"""Constants for the Season integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "season" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Season" + +TYPE_ASTRONOMICAL: Final = "astronomical" +TYPE_METEOROLOGICAL: Final = "meteorological" + +VALID_TYPES: Final = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 77059619940..7b6feeca8a4 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -3,8 +3,9 @@ "name": "Season", "documentation": "https://www.home-assistant.io/integrations/season", "requirements": ["ephem==4.1.2"], - "codeowners": [], + "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_polling", - "loggers": ["ephem"] + "loggers": ["ephem"], + "config_flow": true } diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 23b50c0939f..216475f0cdf 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -1,8 +1,7 @@ -"""Support for tracking which astronomical or meteorological season it is.""" +"""Support for Season sensors.""" from __future__ import annotations from datetime import date, datetime -import logging import ephem import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -18,9 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Season" +from .const import DEFAULT_NAME, DOMAIN, TYPE_ASTRONOMICAL, VALID_TYPES EQUATOR = "equator" @@ -32,11 +30,6 @@ STATE_SPRING = "spring" STATE_SUMMER = "summer" STATE_WINTER = "winter" -TYPE_ASTRONOMICAL = "astronomical" -TYPE_METEOROLOGICAL = "meteorological" - -VALID_TYPES = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] - HEMISPHERE_SEASON_SWAP = { STATE_WINTER: STATE_SUMMER, STATE_SPRING: STATE_AUTUMN, @@ -60,25 +53,35 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Display the current season.""" - _type: str = config[CONF_TYPE] - name: str = config[CONF_NAME] + """Set up the season sensor platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config entry.""" + hemisphere = EQUATOR if hass.config.latitude < 0: hemisphere = SOUTHERN elif hass.config.latitude > 0: hemisphere = NORTHERN - else: - hemisphere = EQUATOR - _LOGGER.debug(_type) - add_entities([Season(hemisphere, _type, name)], True) + async_add_entities([SeasonSensorEntity(entry, hemisphere)], True) def get_season( @@ -100,14 +103,13 @@ def get_season( autumn_start = spring_start.replace(month=9) winter_start = spring_start.replace(month=12) + season = STATE_WINTER if spring_start <= current_date < summer_start: season = STATE_SPRING elif summer_start <= current_date < autumn_start: season = STATE_SUMMER elif autumn_start <= current_date < winter_start: season = STATE_AUTUMN - elif winter_start <= current_date or spring_start > current_date: - season = STATE_WINTER # If user is located in the southern hemisphere swap the season if hemisphere == NORTHERN: @@ -115,16 +117,17 @@ def get_season( return HEMISPHERE_SEASON_SWAP.get(season) -class Season(SensorEntity): +class SeasonSensorEntity(SensorEntity): """Representation of the current season.""" _attr_device_class = "season__season" - def __init__(self, hemisphere: str, season_tracking_type: str, name: str) -> None: + def __init__(self, entry: ConfigEntry, hemisphere: str) -> None: """Initialize the season.""" - self._attr_name = name + self._attr_name = entry.title + self._attr_unique_id = entry.entry_id self.hemisphere = hemisphere - self.type = season_tracking_type + self.type = entry.data[CONF_TYPE] def update(self) -> None: """Update season.""" diff --git a/homeassistant/components/season/strings.json b/homeassistant/components/season/strings.json new file mode 100644 index 00000000000..c75c0f1c507 --- /dev/null +++ b/homeassistant/components/season/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "type": "Type of season definition" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/season/translations/en.json b/homeassistant/components/season/translations/en.json new file mode 100644 index 00000000000..1638f3c0a20 --- /dev/null +++ b/homeassistant/components/season/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "step": { + "user": { + "data": { + "type": "Type of season definition" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.en.json b/homeassistant/components/season/translations/sensor.en.json index 54e0ad8e98f..91c7ac12bfc 100644 --- a/homeassistant/components/season/translations/sensor.en.json +++ b/homeassistant/components/season/translations/sensor.en.json @@ -5,12 +5,6 @@ "spring": "Spring", "summer": "Summer", "winter": "Winter" - }, - "season__season__": { - "autumn": "Autumn", - "spring": "Spring", - "summer": "Summer", - "winter": "Winter" } } } \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 379adad2b93..968a52d8269 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -283,6 +283,7 @@ FLOWS = [ "ruckus_unleashed", "samsungtv", "screenlogic", + "season", "sense", "senseme", "sensibo", diff --git a/tests/components/season/conftest.py b/tests/components/season/conftest.py new file mode 100644 index 00000000000..40d95f3331b --- /dev/null +++ b/tests/components/season/conftest.py @@ -0,0 +1,30 @@ +"""Fixtures for Season integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL +from homeassistant.const import CONF_TYPE + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Season", + domain=DOMAIN, + data={CONF_TYPE: TYPE_ASTRONOMICAL}, + unique_id=TYPE_ASTRONOMICAL, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.season.async_setup_entry", return_value=True): + yield diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py new file mode 100644 index 00000000000..11ebea8f6d6 --- /dev/null +++ b/tests/components/season/test_config_flow.py @@ -0,0 +1,76 @@ +"""Tests for the Season config flow.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.season.const import ( + DOMAIN, + TYPE_ASTRONOMICAL, + TYPE_METEOROLOGICAL, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_TYPE: TYPE_ASTRONOMICAL}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Season" + assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data={CONF_TYPE: TYPE_ASTRONOMICAL} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_import_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_NAME: "My Seasons", CONF_TYPE: TYPE_METEOROLOGICAL}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "My Seasons" + assert result.get("data") == {CONF_TYPE: TYPE_METEOROLOGICAL} diff --git a/tests/components/season/test_init.py b/tests/components/season/test_init.py new file mode 100644 index 00000000000..94012ba16dd --- /dev/null +++ b/tests/components/season/test_init.py @@ -0,0 +1,55 @@ +"""Tests for the Season integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.season.const import DOMAIN, TYPE_ASTRONOMICAL +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Season configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_import_config( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test Season being set up from config via import.""" + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_NAME: "My Season", + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + + entry = config_entries[0] + assert entry.title == "My Season" + assert entry.unique_id == TYPE_ASTRONOMICAL + assert entry.data == {CONF_TYPE: TYPE_ASTRONOMICAL} diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 7e673832121..90d01106bba 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,19 +1,21 @@ -"""The tests for the Season sensor platform.""" +"""The tests for the Season integration.""" from datetime import datetime -from unittest.mock import patch +from freezegun import freeze_time import pytest +from homeassistant.components.season.const import TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL from homeassistant.components.season.sensor import ( STATE_AUTUMN, STATE_SPRING, STATE_SUMMER, STATE_WINTER, - TYPE_ASTRONOMICAL, - TYPE_METEOROLOGICAL, ) -from homeassistant.const import STATE_UNKNOWN -from homeassistant.setup import async_setup_component +from homeassistant.const import CONF_TYPE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry HEMISPHERE_NORTHERN = { "homeassistant": {"latitude": 48.864716, "longitude": 2.349014}, @@ -65,60 +67,80 @@ def idfn(val): @pytest.mark.parametrize("type,day,expected", NORTHERN_PARAMETERS, ids=idfn) -async def test_season_northern_hemisphere(hass, type, day, expected): +async def test_season_northern_hemisphere( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + type: str, + day: datetime, + expected: str, +) -> None: """Test that season should be summer.""" hass.config.latitude = HEMISPHERE_NORTHERN["homeassistant"]["latitude"] + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=type, data={CONF_TYPE: type} + ) - config = { - **HEMISPHERE_NORTHERN, - "sensor": {"platform": "season", "type": type}, - } - - with patch("homeassistant.components.season.sensor.utcnow", return_value=day): - assert await async_setup_component(hass, "sensor", config) + with freeze_time(day): + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.season") assert state assert state.state == expected + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.season") + assert entry + assert entry.unique_id == mock_config_entry.entry_id + @pytest.mark.parametrize("type,day,expected", SOUTHERN_PARAMETERS, ids=idfn) -async def test_season_southern_hemisphere(hass, type, day, expected): +async def test_season_southern_hemisphere( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + type: str, + day: datetime, + expected: str, +) -> None: """Test that season should be summer.""" hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"] + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=type, data={CONF_TYPE: type} + ) - config = { - **HEMISPHERE_SOUTHERN, - "sensor": {"platform": "season", "type": type}, - } - - with patch("homeassistant.components.season.sensor.utcnow", return_value=day): - assert await async_setup_component(hass, "sensor", config) + with freeze_time(day): + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.season") assert state assert state.state == expected + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.season") + assert entry + assert entry.unique_id == mock_config_entry.entry_id -async def test_season_equator(hass): + +async def test_season_equator( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: """Test that season should be unknown for equator.""" hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"] - day = datetime(2017, 9, 3, 0, 0) + mock_config_entry.add_to_hass(hass) - with patch("homeassistant.components.season.sensor.utcnow", return_value=day): - assert await async_setup_component(hass, "sensor", HEMISPHERE_EQUATOR) + with freeze_time(datetime(2017, 9, 3, 0, 0)): + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.season") assert state assert state.state == STATE_UNKNOWN - -async def test_setup_hemisphere_empty(hass): - """Test platform setup of missing latlong.""" - hass.config.latitude = None - assert await async_setup_component(hass, "sensor", HEMISPHERE_EMPTY) - await hass.async_block_till_done() - assert hass.config.as_dict()["latitude"] is None + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.season") + assert entry + assert entry.unique_id == mock_config_entry.entry_id From 63957787c4b9e818ce0e5e86f08111fb073bb2b6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Mar 2022 23:26:30 +0100 Subject: [PATCH 0287/1054] Update PyTurboJPEG to 1.6.6 (#67800) --- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 7fad2044b7c..aa4ca61e1d5 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,7 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], - "requirements": ["PyTurboJPEG==1.6.5"], + "requirements": ["PyTurboJPEG==1.6.6"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 1fe64defe36..d8fd035a5ab 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.5", "av==8.1.0"], + "requirements": ["PyTurboJPEG==1.6.6", "av==8.1.0"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 2b72daff8d8..f526d149138 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -41,7 +41,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.5 +PyTurboJPEG==1.6.6 # homeassistant.components.vicare PyViCare==2.16.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d06a913e9c..0f7db7496e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -34,7 +34,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.5 +PyTurboJPEG==1.6.6 # homeassistant.components.vicare PyViCare==2.16.1 From b8e4780aa12a3b587971472be0cb52809670c660 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 7 Mar 2022 23:27:37 +0100 Subject: [PATCH 0288/1054] Modify diagnostics for Sensibo (#67764) --- homeassistant/components/sensibo/climate.py | 77 ++++++++++++------- .../components/sensibo/coordinator.py | 20 ++++- .../components/sensibo/diagnostics.py | 20 ++++- homeassistant/components/sensibo/entity.py | 2 +- homeassistant/components/sensibo/number.py | 12 ++- 5 files changed, 93 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 7c3282478ab..669fa7a9543 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -102,7 +102,7 @@ async def async_setup_entry( entities = [ SensiboClimate(coordinator, device_id) - for device_id, device_data in coordinator.data.items() + for device_id, device_data in coordinator.data.parsed.items() # Remove none climate devices if device_data["hvac_modes"] and device_data["temp"] ] @@ -128,10 +128,10 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): """Initiate SensiboClimate.""" super().__init__(coordinator, device_id) self._attr_unique_id = device_id - self._attr_name = coordinator.data[device_id]["name"] + self._attr_name = coordinator.data.parsed[device_id]["name"] self._attr_temperature_unit = ( TEMP_CELSIUS - if coordinator.data[device_id]["temp_unit"] == "C" + if coordinator.data.parsed[device_id]["temp_unit"] == "C" else TEMP_FAHRENHEIT ) self._attr_supported_features = self.get_features() @@ -140,7 +140,7 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): def get_features(self) -> int: """Get supported features.""" features = 0 - for key in self.coordinator.data[self.unique_id]["full_features"]: + for key in self.coordinator.data.parsed[self.unique_id]["full_features"]: if key in FIELD_TO_FLAG: features |= FIELD_TO_FLAG[key] return features @@ -148,14 +148,14 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): @property def current_humidity(self) -> int | None: """Return the current humidity.""" - return self.coordinator.data[self.unique_id]["humidity"] + return self.coordinator.data.parsed[self.unique_id]["humidity"] @property def hvac_mode(self) -> str: """Return hvac operation.""" return ( - SENSIBO_TO_HA[self.coordinator.data[self.unique_id]["hvac_mode"]] - if self.coordinator.data[self.unique_id]["on"] + SENSIBO_TO_HA[self.coordinator.data.parsed[self.unique_id]["hvac_mode"]] + if self.coordinator.data.parsed[self.unique_id]["on"] else HVAC_MODE_OFF ) @@ -164,14 +164,14 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): """Return the list of available hvac operation modes.""" return [ SENSIBO_TO_HA[mode] - for mode in self.coordinator.data[self.unique_id]["hvac_modes"] + for mode in self.coordinator.data.parsed[self.unique_id]["hvac_modes"] ] @property def current_temperature(self) -> float | None: """Return the current temperature.""" return convert_temperature( - self.coordinator.data[self.unique_id]["temp"], + self.coordinator.data.parsed[self.unique_id]["temp"], TEMP_CELSIUS, self.temperature_unit, ) @@ -179,53 +179,56 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return self.coordinator.data[self.unique_id]["target_temp"] + return self.coordinator.data.parsed[self.unique_id]["target_temp"] @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" - return self.coordinator.data[self.unique_id]["temp_step"] + return self.coordinator.data.parsed[self.unique_id]["temp_step"] @property def fan_mode(self) -> str | None: """Return the fan setting.""" - return self.coordinator.data[self.unique_id]["fan_mode"] + return self.coordinator.data.parsed[self.unique_id]["fan_mode"] @property def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" - return self.coordinator.data[self.unique_id]["fan_modes"] + return self.coordinator.data.parsed[self.unique_id]["fan_modes"] @property def swing_mode(self) -> str | None: """Return the swing setting.""" - return self.coordinator.data[self.unique_id]["swing_mode"] + return self.coordinator.data.parsed[self.unique_id]["swing_mode"] @property def swing_modes(self) -> list[str] | None: """Return the list of available swing modes.""" - return self.coordinator.data[self.unique_id]["swing_modes"] + return self.coordinator.data.parsed[self.unique_id]["swing_modes"] @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.coordinator.data[self.unique_id]["temp_list"][0] + return self.coordinator.data.parsed[self.unique_id]["temp_list"][0] @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.coordinator.data[self.unique_id]["temp_list"][-1] + return self.coordinator.data.parsed[self.unique_id]["temp_list"][-1] @property def available(self) -> bool: """Return True if entity is available.""" - return self.coordinator.data[self.unique_id]["available"] and super().available + return ( + self.coordinator.data.parsed[self.unique_id]["available"] + and super().available + ) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" if ( "targetTemperature" - not in self.coordinator.data[self.unique_id]["active_features"] + not in self.coordinator.data.parsed[self.unique_id]["active_features"] ): raise HomeAssistantError( "Current mode doesn't support setting Target Temperature" @@ -237,13 +240,23 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): if temperature == self.target_temperature: return - if temperature not in self.coordinator.data[self.unique_id]["temp_list"]: + if temperature not in self.coordinator.data.parsed[self.unique_id]["temp_list"]: # Requested temperature is not supported. - if temperature > self.coordinator.data[self.unique_id]["temp_list"][-1]: - temperature = self.coordinator.data[self.unique_id]["temp_list"][-1] + if ( + temperature + > self.coordinator.data.parsed[self.unique_id]["temp_list"][-1] + ): + temperature = self.coordinator.data.parsed[self.unique_id]["temp_list"][ + -1 + ] - elif temperature < self.coordinator.data[self.unique_id]["temp_list"][0]: - temperature = self.coordinator.data[self.unique_id]["temp_list"][0] + elif ( + temperature + < self.coordinator.data.parsed[self.unique_id]["temp_list"][0] + ): + temperature = self.coordinator.data.parsed[self.unique_id]["temp_list"][ + 0 + ] else: return @@ -252,7 +265,10 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if "fanLevel" not in self.coordinator.data[self.unique_id]["active_features"]: + if ( + "fanLevel" + not in self.coordinator.data.parsed[self.unique_id]["active_features"] + ): raise HomeAssistantError("Current mode doesn't support setting Fanlevel") await self._async_set_ac_state_property("fanLevel", fan_mode) @@ -264,7 +280,7 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): return # Turn on if not currently on. - if not self.coordinator.data[self.unique_id]["on"]: + if not self.coordinator.data.parsed[self.unique_id]["on"]: await self._async_set_ac_state_property("on", True) await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) @@ -272,7 +288,10 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" - if "swing" not in self.coordinator.data[self.unique_id]["active_features"]: + if ( + "swing" + not in self.coordinator.data.parsed[self.unique_id]["active_features"] + ): raise HomeAssistantError("Current mode doesn't support setting Swing") await self._async_set_ac_state_property("swing", swing_mode) @@ -292,13 +311,13 @@ class SensiboClimate(SensiboBaseEntity, ClimateEntity): params = { "name": name, "value": value, - "ac_states": self.coordinator.data[self.unique_id]["ac_states"], + "ac_states": self.coordinator.data.parsed[self.unique_id]["ac_states"], "assumed_state": assumed_state, } result = await self.async_send_command("set_ac_state", params) if result["result"]["status"] == "Success": - self.coordinator.data[self.unique_id][AC_STATE_TO_DATA[name]] = value + self.coordinator.data.parsed[self.unique_id][AC_STATE_TO_DATA[name]] = value self.async_write_ha_state() return diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 4a03b532149..089cc0406bd 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -35,9 +35,19 @@ class MotionSensor: model: str | None = None +@dataclass +class SensiboData: + """Dataclass for Sensibo data.""" + + raw: dict + parsed: dict + + class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" + data: SensiboData + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize the Sensibo coordinator.""" self.client = SensiboClient( @@ -52,7 +62,7 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), ) - async def _async_update_data(self) -> dict[str, dict[str, Any]]: + async def _async_update_data(self) -> SensiboData: """Fetch data from Sensibo.""" devices = [] @@ -65,7 +75,10 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): except SensiboError as error: raise UpdateFailed from error - device_data: dict[str, dict[str, Any]] = {} + if not devices: + raise UpdateFailed("No devices found") + + device_data: dict[str, Any] = {} for dev in devices: unique_id = dev["id"] mac = dev["macAddress"] @@ -172,4 +185,5 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): "full_capabilities": capabilities, "motion_sensors": motion_sensors, } - return device_data + + return SensiboData(raw=data, parsed=device_data) diff --git a/homeassistant/components/sensibo/diagnostics.py b/homeassistant/components/sensibo/diagnostics.py index d3e2382c7a8..e4a4672bf64 100644 --- a/homeassistant/components/sensibo/diagnostics.py +++ b/homeassistant/components/sensibo/diagnostics.py @@ -3,16 +3,34 @@ from __future__ import annotations from typing import Any +from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator +TO_REDACT = { + "location", + "ssid", + "id", + "macAddress", + "parentDeviceUid", + "qrId", + "serial", + "uid", + "email", + "firstName", + "lastName", + "username", + "podUid", + "deviceUid", +} + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return coordinator.data + return async_redact_data(coordinator.data.raw, TO_REDACT) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 22d29d37ead..ff68b0ebfd1 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -28,7 +28,7 @@ class SensiboBaseEntity(CoordinatorEntity): super().__init__(coordinator) self._device_id = device_id self._client = coordinator.client - device = coordinator.data[device_id] + device = coordinator.data.parsed[device_id] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device["id"])}, name=device["name"], diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index ec7a150ebe9..2aa38a41a7b 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -64,7 +64,7 @@ async def async_setup_entry( async_add_entities( SensiboNumber(coordinator, device_id, description) - for device_id, device_data in coordinator.data.items() + for device_id, device_data in coordinator.data.parsed.items() for description in NUMBER_TYPES if device_data["hvac_modes"] and device_data["temp"] ) @@ -86,20 +86,24 @@ class SensiboNumber(SensiboBaseEntity, NumberEntity): self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" self._attr_name = ( - f"{coordinator.data[device_id]['name']} {entity_description.name}" + f"{coordinator.data.parsed[device_id]['name']} {entity_description.name}" ) @property def value(self) -> float | None: """Return the value from coordinator data.""" - return self.coordinator.data[self._device_id][self.entity_description.key] + return self.coordinator.data.parsed[self._device_id][ + self.entity_description.key + ] async def async_set_value(self, value: float) -> None: """Set value for calibration.""" data = {self.entity_description.remote_key: value} result = await self.async_send_command("set_calibration", {"data": data}) if result["status"] == "success": - self.coordinator.data[self._device_id][self.entity_description.key] = value + self.coordinator.data.parsed[self._device_id][ + self.entity_description.key + ] = value self.async_write_ha_state() return raise HomeAssistantError(f"Could not set calibration for device {self.name}") From d1ef92c17a0c83dd33222728484931c5bba43585 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Mar 2022 23:30:54 +0100 Subject: [PATCH 0289/1054] Bump samsungtvws to 2.3.0 (#67821) Co-authored-by: epenet --- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index bc3f3077365..93ae1c0d73a 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws[async]==2.2.0", + "samsungtvws[async]==2.3.0", "wakeonlan==2.0.1" ], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index f526d149138..848252818f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.2.0 +samsungtvws[async]==2.3.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f7db7496e0..a27eda2584a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1330,7 +1330,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.2.0 +samsungtvws[async]==2.3.0 # homeassistant.components.dhcp scapy==2.4.5 From c70bed86ff374b59dca22eec7094a61ec56bfbb9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Mar 2022 23:31:57 +0100 Subject: [PATCH 0290/1054] Adjust websocket bridge logging in SamsungTV (#67809) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 24 ++++++++++++------- .../components/samsungtv/test_media_player.py | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index fc699909e75..a63ca0f3355 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -435,7 +435,7 @@ class SamsungTVWSBridge(SamsungTVBridge): """Create or return a remote control instance.""" if self._remote is None or not self._remote.is_alive(): # We need to create a new instance to reconnect. - LOGGER.debug("Create SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host) + LOGGER.debug("Create SamsungTVWSBridge for %s", self.host) assert self.port self._remote = SamsungTVWSAsyncRemote( host=self.host, @@ -449,20 +449,24 @@ class SamsungTVWSBridge(SamsungTVBridge): # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket except ConnectionFailure as err: - LOGGER.debug("ConnectionFailure %s", err.__repr__()) + LOGGER.info( + "Failed to get remote for %s, re-authentication required: %s", + self.host, + err.__repr__(), + ) self._notify_reauth_callback() except (WebSocketException, AsyncioTimeoutError, OSError) as err: - LOGGER.debug("WebSocketException, OSError %s", err.__repr__()) + LOGGER.debug( + "Failed to get remote for %s: %s", self.host, err.__repr__() + ) self._remote = None else: - LOGGER.debug( - "Created SamsungTVWSBridge for %s (%s)", CONF_NAME, self.host - ) + LOGGER.debug("Created SamsungTVWSBridge for %s", self.host) if self._device_info is None: # Initialise device info on first connect await self.async_device_info() if self.token != self._remote.token: - LOGGER.debug( + LOGGER.info( "SamsungTVWSBridge has provided a new token %s", self._remote.token, ) @@ -477,5 +481,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # Close the current remote connection await self._remote.close() self._remote = None - except OSError: - LOGGER.debug("Could not establish connection") + except OSError as err: + LOGGER.debug( + "Error closing connection to %s: %s", self.host, err.__repr__() + ) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 913e11e237c..d07ec9404d6 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -695,7 +695,7 @@ async def test_turn_off_ws_os_error( assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) - assert "Could not establish connection" in caplog.text + assert "Error closing connection" in caplog.text async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: From ea82f2e293f43d3e5be103a64b68d088c4b65545 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Mon, 7 Mar 2022 15:16:43 -0800 Subject: [PATCH 0291/1054] Add Kaleidescape integration (#67711) --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/kaleidescape/__init__.py | 96 +++++++++ .../components/kaleidescape/config_flow.py | 112 +++++++++++ .../components/kaleidescape/const.py | 5 + .../components/kaleidescape/entity.py | 47 +++++ .../components/kaleidescape/manifest.json | 17 ++ .../components/kaleidescape/media_player.py | 158 +++++++++++++++ .../components/kaleidescape/strings.json | 27 +++ .../kaleidescape/translations/en.json | 27 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 6 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/kaleidescape/__init__.py | 18 ++ tests/components/kaleidescape/conftest.py | 73 +++++++ .../kaleidescape/test_config_flow.py | 130 +++++++++++++ tests/components/kaleidescape/test_init.py | 58 ++++++ .../kaleidescape/test_media_player.py | 183 ++++++++++++++++++ 20 files changed, 978 insertions(+) create mode 100644 homeassistant/components/kaleidescape/__init__.py create mode 100644 homeassistant/components/kaleidescape/config_flow.py create mode 100644 homeassistant/components/kaleidescape/const.py create mode 100644 homeassistant/components/kaleidescape/entity.py create mode 100644 homeassistant/components/kaleidescape/manifest.json create mode 100644 homeassistant/components/kaleidescape/media_player.py create mode 100644 homeassistant/components/kaleidescape/strings.json create mode 100644 homeassistant/components/kaleidescape/translations/en.json create mode 100644 tests/components/kaleidescape/__init__.py create mode 100644 tests/components/kaleidescape/conftest.py create mode 100644 tests/components/kaleidescape/test_config_flow.py create mode 100644 tests/components/kaleidescape/test_init.py create mode 100644 tests/components/kaleidescape/test_media_player.py diff --git a/.strict-typing b/.strict-typing index 6796c5f3866..4e5a2da5b79 100644 --- a/.strict-typing +++ b/.strict-typing @@ -117,6 +117,7 @@ homeassistant.components.isy994.* homeassistant.components.iqvia.* homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* +homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.lametric.* diff --git a/CODEOWNERS b/CODEOWNERS index 79a9241955e..bc84567a2b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -513,6 +513,8 @@ tests/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz tests/components/juicenet/* @jesserockz homeassistant/components/kaiterra/* @Michsior14 +homeassistant/components/kaleidescape/* @SteveEasley +tests/components/kaleidescape/* @SteveEasley homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel tests/components/keenetic_ndms2/* @foxel diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py new file mode 100644 index 00000000000..574e74a3e14 --- /dev/null +++ b/homeassistant/components/kaleidescape/__init__.py @@ -0,0 +1,96 @@ +"""The Kaleidescape integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import TYPE_CHECKING + +from kaleidescape import Device as KaleidescapeDevice, KaleidescapeError + +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError + +from .const import DOMAIN + +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import Event, HomeAssistant + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Kaleidescape from a config entry.""" + device = KaleidescapeDevice( + entry.data[CONF_HOST], timeout=5, reconnect=True, reconnect_delay=5 + ) + + try: + await device.connect() + except (KaleidescapeError, ConnectionError) as err: + await device.disconnect() + raise ConfigEntryNotReady( + f"Unable to connect to {entry.data[CONF_HOST]}: {err}" + ) from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + + async def disconnect(event: Event) -> None: + await device.disconnect() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) + ) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await hass.data[DOMAIN][entry.entry_id].disconnect() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +@dataclass +class KaleidescapeDeviceInfo: + """Metadata for a Kaleidescape device.""" + + host: str + serial: str + name: str + model: str + server_only: bool + + +class UnsupportedError(HomeAssistantError): + """Error for unsupported device types.""" + + +async def validate_host(host: str) -> KaleidescapeDeviceInfo: + """Validate device host.""" + device = KaleidescapeDevice(host) + + try: + await device.connect() + except (KaleidescapeError, ConnectionError): + await device.disconnect() + raise + + info = KaleidescapeDeviceInfo( + host=device.host, + serial=device.system.serial_number, + name=device.system.friendly_name, + model=device.system.type, + server_only=device.is_server_only, + ) + + await device.disconnect() + + return info diff --git a/homeassistant/components/kaleidescape/config_flow.py b/homeassistant/components/kaleidescape/config_flow.py new file mode 100644 index 00000000000..a6127e89a77 --- /dev/null +++ b/homeassistant/components/kaleidescape/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow for Kaleidescape.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, cast +from urllib.parse import urlparse + +import voluptuous as vol + +from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST + +from . import KaleidescapeDeviceInfo, UnsupportedError, validate_host +from .const import DEFAULT_HOST, DOMAIN, NAME as KALEIDESCAPE_NAME + +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + +ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_UNKNOWN = "unknown" +ERROR_UNSUPPORTED = "unsupported" + + +class KaleidescapeConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Kaleidescape integration.""" + + VERSION = 1 + + discovered_device: KaleidescapeDeviceInfo + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle user initiated device additions.""" + errors = {} + host = DEFAULT_HOST + + if user_input is not None: + host = user_input[CONF_HOST].strip() + + try: + info = await validate_host(host) + if info.server_only: + raise UnsupportedError + except ConnectionError: + errors["base"] = ERROR_CANNOT_CONNECT + except UnsupportedError: + errors["base"] = ERROR_UNSUPPORTED + else: + host = info.host + + await self.async_set_unique_id(info.serial, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + return self.async_create_entry( + title=f"{KALEIDESCAPE_NAME} ({info.name})", + data={CONF_HOST: host}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), + errors=errors, + ) + + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle discovered device.""" + host = cast(str, urlparse(discovery_info.ssdp_location).hostname) + serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + try: + self.discovered_device = await validate_host(host) + if self.discovered_device.server_only: + raise UnsupportedError + except ConnectionError: + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except UnsupportedError: + return self.async_abort(reason=ERROR_UNSUPPORTED) + + self.context.update( + { + "title_placeholders": { + "name": self.discovered_device.name, + "model": self.discovered_device.model, + } + } + ) + + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle addition of discovered device.""" + if user_input is None: + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={ + "name": self.discovered_device.name, + "model": self.discovered_device.model, + }, + errors={}, + ) + + return self.async_create_entry( + title=f"{KALEIDESCAPE_NAME} ({self.discovered_device.name})", + data={CONF_HOST: self.discovered_device.host}, + ) diff --git a/homeassistant/components/kaleidescape/const.py b/homeassistant/components/kaleidescape/const.py new file mode 100644 index 00000000000..dc4e0195977 --- /dev/null +++ b/homeassistant/components/kaleidescape/const.py @@ -0,0 +1,5 @@ +"""Constants for the Kaleidescape integration.""" + +NAME = "Kaleidescape" +DOMAIN = "kaleidescape" +DEFAULT_HOST = "my-kaleidescape.local" diff --git a/homeassistant/components/kaleidescape/entity.py b/homeassistant/components/kaleidescape/entity.py new file mode 100644 index 00000000000..9a5e62bca94 --- /dev/null +++ b/homeassistant/components/kaleidescape/entity.py @@ -0,0 +1,47 @@ +"""Base Entity for Kaleidescape.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN, NAME as KALEIDESCAPE_NAME + +if TYPE_CHECKING: + from kaleidescape import Device as KaleidescapeDevice + +_LOGGER = logging.getLogger(__name__) + + +class KaleidescapeEntity(Entity): + """Defines a base Kaleidescape entity.""" + + def __init__(self, device: KaleidescapeDevice) -> None: + """Initialize entity.""" + self._device = device + + self._attr_should_poll = False + self._attr_unique_id = device.serial_number + self._attr_name = f"{KALEIDESCAPE_NAME} {device.system.friendly_name}" + self._attr_device_info = DeviceInfo( + identifiers={(KALEIDESCAPE_DOMAIN, self._device.serial_number)}, + name=self.name, + model=self._device.system.type, + manufacturer=KALEIDESCAPE_NAME, + sw_version=f"{self._device.system.kos_version}", + suggested_area="Theater", + configuration_url=f"http://{self._device.host}", + ) + + async def async_added_to_hass(self) -> None: + """Register update listener.""" + + @callback + def _update(event: str) -> None: + """Handle device state changes.""" + self.async_write_ha_state() + + self.async_on_remove(self._device.dispatcher.connect(_update).disconnect) diff --git a/homeassistant/components/kaleidescape/manifest.json b/homeassistant/components/kaleidescape/manifest.json new file mode 100644 index 00000000000..88d5c7726f0 --- /dev/null +++ b/homeassistant/components/kaleidescape/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "kaleidescape", + "name": "Kaleidescape", + "config_flow": true, + "ssdp": [ + { + "manufacturer": "Kaleidescape, Inc.", + "deviceType": "schemas-upnp-org:device:Basic:1" + } + ], + "documentation": "https://www.home-assistant.io/integrations/kaleidescape", + "requirements": ["pykaleidescape==2022.2.6"], + "codeowners": [ + "@SteveEasley" + ], + "iot_class": "local_push" +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py new file mode 100644 index 00000000000..080db5524fe --- /dev/null +++ b/homeassistant/components/kaleidescape/media_player.py @@ -0,0 +1,158 @@ +"""Kaleidescape Media Player.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from kaleidescape import const as kaleidescape_const + +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) +from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.util.dt import utcnow + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .entity import KaleidescapeEntity + +if TYPE_CHECKING: + from datetime import datetime + + from kaleidescape import Device as KaleidescapeDevice + + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +KALEIDESCAPE_PLAYING_STATES = [ + kaleidescape_const.PLAY_STATUS_PLAYING, + kaleidescape_const.PLAY_STATUS_FORWARD, + kaleidescape_const.PLAY_STATUS_REVERSE, +] + +KALEIDESCAPE_PAUSED_STATES = [kaleidescape_const.PLAY_STATUS_PAUSED] + +SUPPORTED_FEATURES = ( + SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_PLAY + | SUPPORT_PAUSE + | SUPPORT_STOP + | SUPPORT_NEXT_TRACK + | SUPPORT_PREVIOUS_TRACK +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the platform from a config entry.""" + entities = [KaleidescapeMediaPlayer(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])] + async_add_entities(entities) + + +class KaleidescapeMediaPlayer(KaleidescapeEntity, MediaPlayerEntity): + """Representation of a Kaleidescape device.""" + + def __init__(self, device: KaleidescapeDevice) -> None: + """Initialize media player.""" + super().__init__(device) + self._attr_supported_features = SUPPORTED_FEATURES + + async def async_turn_on(self) -> None: + """Send leave standby command.""" + await self._device.leave_standby() + + async def async_turn_off(self) -> None: + """Send enter standby command.""" + await self._device.enter_standby() + + async def async_media_pause(self) -> None: + """Send pause command.""" + await self._device.pause() + + async def async_media_play(self) -> None: + """Send play command.""" + await self._device.play() + + async def async_media_stop(self) -> None: + """Send stop command.""" + await self._device.stop() + + async def async_media_next_track(self) -> None: + """Send track next command.""" + await self._device.next() + + async def async_media_previous_track(self) -> None: + """Send track previous command.""" + await self._device.previous() + + @property + def state(self) -> str: + """State of device.""" + if self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_STANDBY: + return STATE_OFF + if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES: + return STATE_PLAYING + if self._device.movie.play_status in KALEIDESCAPE_PAUSED_STATES: + return STATE_PAUSED + return STATE_IDLE + + @property + def available(self) -> bool: + """Return if device is available.""" + return self._device.is_connected + + @property + def media_content_id(self) -> str | None: + """Content ID of current playing media.""" + if self._device.movie.handle: + return self._device.movie.handle + return None + + @property + def media_content_type(self) -> str | None: + """Content type of current playing media.""" + return self._device.movie.media_type + + @property + def media_duration(self) -> int | None: + """Duration of current playing media in seconds.""" + if self._device.movie.title_length: + return self._device.movie.title_length + return None + + @property + def media_position(self) -> int | None: + """Position of current playing media in seconds.""" + if self._device.movie.title_location: + return self._device.movie.title_location + return None + + @property + def media_position_updated_at(self) -> datetime | None: + """When was the position of the current playing media valid.""" + if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES: + return utcnow() + return None + + @property + def media_image_url(self) -> str: + """Image url of current playing media.""" + return self._device.movie.cover + + @property + def media_title(self) -> str: + """Title of current playing media.""" + return self._device.movie.title diff --git a/homeassistant/components/kaleidescape/strings.json b/homeassistant/components/kaleidescape/strings.json new file mode 100644 index 00000000000..07c5d5bafd9 --- /dev/null +++ b/homeassistant/components/kaleidescape/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "flow_title": "{model} ({name})", + "step": { + "user": { + "title": "Kaleidescape Setup", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "discovery_confirm": { + "title": "Kaleidescape", + "description": "Do you want to set up the {model} player named {name}?" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "unsupported": "Unsupported device" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unsupported": "Unsupported device" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/en.json b/homeassistant/components/kaleidescape/translations/en.json new file mode 100644 index 00000000000..38dd84843c3 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "flow_title": "{model} ({name})", + "step": { + "user": { + "title": "Kaleidescape Setup", + "data": { + "host": "Host" + } + }, + "discovery_confirm": { + "title": "Kaleidescape Setup", + "description": "Do you want to set up the {model} player named {name}?" + } + }, + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "unknown": "Unexpected error", + "unsupported": "Unsupported device" + }, + "error": { + "cannot_connect": "Failed to connect", + "unsupported": "Unsupported device" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 968a52d8269..195f09c91a1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -167,6 +167,7 @@ FLOWS = [ "izone", "jellyfin", "juicenet", + "kaleidescape", "keenetic_ndms2", "kmtronic", "knx", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index f0117e2a9c2..40bb9bf295f 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -168,6 +168,12 @@ SSDP = { "manufacturer": "Universal Devices Inc." } ], + "kaleidescape": [ + { + "deviceType": "schemas-upnp-org:device:Basic:1", + "manufacturer": "Kaleidescape, Inc." + } + ], "keenetic_ndms2": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", diff --git a/mypy.ini b/mypy.ini index 2bb0f5b5462..c3f6540273e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1088,6 +1088,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.kaleidescape.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.knx.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 848252818f7..876aa8bbdd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1560,6 +1560,9 @@ pyisy==3.0.1 # homeassistant.components.itach pyitachip2ir==0.0.7 +# homeassistant.components.kaleidescape +pykaleidescape==2022.2.6 + # homeassistant.components.kira pykira==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a27eda2584a..9ec49746695 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1013,6 +1013,9 @@ pyiss==1.0.1 # homeassistant.components.isy994 pyisy==3.0.1 +# homeassistant.components.kaleidescape +pykaleidescape==2022.2.6 + # homeassistant.components.kira pykira==0.1.1 diff --git a/tests/components/kaleidescape/__init__.py b/tests/components/kaleidescape/__init__.py new file mode 100644 index 00000000000..8182cb73743 --- /dev/null +++ b/tests/components/kaleidescape/__init__.py @@ -0,0 +1,18 @@ +"""Tests for Kaleidescape integration.""" + +from homeassistant.components import ssdp +from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL + +MOCK_HOST = "127.0.0.1" +MOCK_SERIAL = "123456" +MOCK_NAME = "Theater" + +MOCK_SSDP_DISCOVERY_INFO = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=f"http://{MOCK_HOST}", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, + ATTR_UPNP_SERIAL: MOCK_SERIAL, + }, +) diff --git a/tests/components/kaleidescape/conftest.py b/tests/components/kaleidescape/conftest.py new file mode 100644 index 00000000000..c86d8f2ccd0 --- /dev/null +++ b/tests/components/kaleidescape/conftest.py @@ -0,0 +1,73 @@ +"""Fixtures for Kaleidescape integration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from kaleidescape import Dispatcher +from kaleidescape.device import Automation, Movie, Power, System +import pytest + +from homeassistant.components.kaleidescape.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from . import MOCK_HOST, MOCK_SERIAL + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_device") +def fixture_mock_device() -> Generator[None, AsyncMock, None]: + """Return a mocked Kaleidescape device.""" + with patch( + "homeassistant.components.kaleidescape.KaleidescapeDevice", autospec=True + ) as mock: + host = MOCK_HOST + + device = mock.return_value + device.dispatcher = Dispatcher() + device.host = host + device.port = 10000 + device.serial_number = MOCK_SERIAL + device.is_connected = True + device.is_server_only = False + device.is_movie_player = True + device.is_music_player = False + device.system = System( + ip_address=host, + serial_number=MOCK_SERIAL, + type="Strato", + protocol=16, + kos_version="10.4.2-19218", + friendly_name=f"Device {MOCK_SERIAL}", + movie_zones=1, + music_zones=1, + ) + device.power = Power(state="standby", readiness="disabled", zone=["available"]) + device.movie = Movie() + device.automation = Automation() + + yield device + + +@pytest.fixture(name="mock_config_entry") +def fixture_mock_config_entry() -> MockConfigEntry: + """Return a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL, + version=1, + data={CONF_HOST: MOCK_HOST}, + ) + + +@pytest.fixture(name="mock_integration") +async def fixture_mock_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> MockConfigEntry: + """Return a mock ConfigEntry setup for Kaleidescape integration.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + return mock_config_entry diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py new file mode 100644 index 00000000000..a2cf8091d02 --- /dev/null +++ b/tests/components/kaleidescape/test_config_flow.py @@ -0,0 +1,130 @@ +"""Tests for Kaleidescape config flow.""" + +import dataclasses +from unittest.mock import AsyncMock + +from homeassistant.components.kaleidescape.const import DOMAIN +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO + +from tests.common import MockConfigEntry + + +async def test_user_config_flow_success( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test user config flow success.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MOCK_HOST} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert "data" in result + assert result["data"][CONF_HOST] == MOCK_HOST + + +async def test_user_config_flow_bad_connect_errors( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test errors when connection error occurs.""" + mock_device.connect.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_config_flow_unsupported_device_errors( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test errors when connecting to unsupported device.""" + mock_device.is_server_only = True + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unsupported"} + + +async def test_user_config_flow_device_exists_abort( + hass: HomeAssistant, mock_device: AsyncMock, mock_integration: MockConfigEntry +) -> None: + """Test flow aborts when device already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_config_flow_success( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test ssdp config flow success.""" + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert "data" in result + assert result["data"][CONF_HOST] == MOCK_HOST + + +async def test_ssdp_config_flow_bad_connect_aborts( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test abort when connection error occurs.""" + mock_device.connect.side_effect = ConnectionError + + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_ssdp_config_flow_unsupported_device_aborts( + hass: HomeAssistant, mock_device: AsyncMock +) -> None: + """Test abort when connecting to unsupported device.""" + mock_device.is_server_only = True + + discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unsupported" diff --git a/tests/components/kaleidescape/test_init.py b/tests/components/kaleidescape/test_init.py new file mode 100644 index 00000000000..876c02ba5a6 --- /dev/null +++ b/tests/components/kaleidescape/test_init.py @@ -0,0 +1,58 @@ +"""Tests for Kaleidescape config entry.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.kaleidescape.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + + +async def test_unload_config_entry( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_integration: MockConfigEntry, +) -> None: + """Test config entry loading and unloading.""" + mock_config_entry = mock_integration + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_device.connect.call_count == 1 + assert mock_device.disconnect.call_count == 0 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_device.disconnect.call_count == 1 + assert mock_config_entry.entry_id not in hass.data[DOMAIN] + + +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config entry not ready.""" + mock_device.connect.side_effect = ConnectionError + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_device( + hass: HomeAssistant, + mock_device: AsyncMock, + mock_integration: MockConfigEntry, +) -> None: + """Test device.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={("kaleidescape", MOCK_SERIAL)} + ) + assert device is not None + assert device.identifiers == {("kaleidescape", MOCK_SERIAL)} diff --git a/tests/components/kaleidescape/test_media_player.py b/tests/components/kaleidescape/test_media_player.py new file mode 100644 index 00000000000..94ba7f82fe8 --- /dev/null +++ b/tests/components/kaleidescape/test_media_player.py @@ -0,0 +1,183 @@ +"""Tests for Kaleidescape media player platform.""" + +from unittest.mock import MagicMock + +from kaleidescape import const as kaleidescape_const +from kaleidescape.device import Movie + +from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import HomeAssistant + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + +ENTITY_ID = f"media_player.kaleidescape_device_{MOCK_SERIAL}" +FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}" + + +async def test_entity( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test entity attributes.""" + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_OFF + assert entity.attributes["friendly_name"] == FRIENDLY_NAME + + +async def test_update_state( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Tests dispatched signals update player.""" + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_OFF + + # Device turns on + mock_device.power.state = kaleidescape_const.DEVICE_POWER_STATE_ON + mock_device.dispatcher.send(kaleidescape_const.DEVICE_POWER_STATE) + await hass.async_block_till_done() + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_IDLE + + # Devices starts playing + mock_device.movie = Movie( + handle="handle", + title="title", + cover="cover", + cover_hires="cover_hires", + rating="rating", + rating_reason="rating_reason", + year="year", + runtime="runtime", + actors=[], + director="director", + directors=[], + genre="genre", + genres=[], + synopsis="synopsis", + color="color", + country="country", + aspect_ratio="aspect_ratio", + media_type="media_type", + play_status=kaleidescape_const.PLAY_STATUS_PLAYING, + play_speed=1, + title_number=1, + title_length=1, + title_location=1, + chapter_number=1, + chapter_length=1, + chapter_location=1, + ) + mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS) + await hass.async_block_till_done() + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_PLAYING + + # Devices pauses playing + mock_device.movie.play_status = kaleidescape_const.PLAY_STATUS_PAUSED + mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS) + await hass.async_block_till_done() + entity = hass.states.get(ENTITY_ID) + assert entity is not None + assert entity.state == STATE_PAUSED + + +async def test_services( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test service calls.""" + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.leave_standby.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.enter_standby.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.play.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PAUSE, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.pause.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_STOP, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.stop.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.next.call_count == 1 + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PREVIOUS_TRACK, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.previous.call_count == 1 + + +async def test_device( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test device attributes.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={("kaleidescape", MOCK_SERIAL)} + ) + assert device.name == FRIENDLY_NAME + assert device.model == "Strato" + assert device.sw_version == "10.4.2-19218" + assert device.manufacturer == "Kaleidescape" From 46d49336a11103e8b5634a1cc095dad308f59946 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 8 Mar 2022 00:43:05 +0100 Subject: [PATCH 0292/1054] Bump python-miio version to 0.5.11 (#67824) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 0091d58e1e2..7157e32299a 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.10"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.11"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 876aa8bbdd2..9749c5876da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1900,7 +1900,7 @@ python-kasa==0.4.1 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.10 +python-miio==0.5.11 # homeassistant.components.mpd python-mpd2==3.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ec49746695..cfe57484344 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1218,7 +1218,7 @@ python-juicenet==1.0.2 python-kasa==0.4.1 # homeassistant.components.xiaomi_miio -python-miio==0.5.10 +python-miio==0.5.11 # homeassistant.components.nest python-nest==4.2.0 From 6b3b21bcfde89865b4b40748cec7c3aa5d12c8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 8 Mar 2022 00:52:15 +0100 Subject: [PATCH 0293/1054] Revert "Add update integration (#66552)" (#67641) --- .core_files.yaml | 1 - .strict-typing | 1 - CODEOWNERS | 2 - .../components/default_config/manifest.json | 3 +- homeassistant/components/update/__init__.py | 273 -------------- homeassistant/components/update/manifest.json | 10 - homeassistant/components/update/strings.json | 3 - .../components/update/translations/ca.json | 3 - .../components/update/translations/de.json | 3 - .../components/update/translations/el.json | 3 - .../components/update/translations/en.json | 3 - .../components/update/translations/it.json | 3 - .../components/update/translations/pt-BR.json | 3 - mypy.ini | 11 - tests/components/update/__init__.py | 1 - tests/components/update/test_init.py | 347 ------------------ 16 files changed, 1 insertion(+), 669 deletions(-) delete mode 100644 homeassistant/components/update/__init__.py delete mode 100644 homeassistant/components/update/manifest.json delete mode 100644 homeassistant/components/update/strings.json delete mode 100644 homeassistant/components/update/translations/ca.json delete mode 100644 homeassistant/components/update/translations/de.json delete mode 100644 homeassistant/components/update/translations/el.json delete mode 100644 homeassistant/components/update/translations/en.json delete mode 100644 homeassistant/components/update/translations/it.json delete mode 100644 homeassistant/components/update/translations/pt-BR.json delete mode 100644 tests/components/update/__init__.py delete mode 100644 tests/components/update/test_init.py diff --git a/.core_files.yaml b/.core_files.yaml index 160a0d80d9a..ebc3ff376f8 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -94,7 +94,6 @@ components: &components - homeassistant/components/tag/* - homeassistant/components/template/* - homeassistant/components/timer/* - - homeassistant/components/update/* - homeassistant/components/usb/* - homeassistant/components/webhook/* - homeassistant/components/websocket_api/* diff --git a/.strict-typing b/.strict-typing index 4e5a2da5b79..498f760bc93 100644 --- a/.strict-typing +++ b/.strict-typing @@ -207,7 +207,6 @@ homeassistant.components.tts.* homeassistant.components.twentemilieu.* homeassistant.components.unifiprotect.* homeassistant.components.upcloud.* -homeassistant.components.update.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* homeassistant.components.usb.* diff --git a/CODEOWNERS b/CODEOWNERS index bc84567a2b0..cd946ea5382 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1057,8 +1057,6 @@ tests/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop tests/components/upcloud/* @scop -homeassistant/components/update/* @home-assistant/core -tests/components/update/* @home-assistant/core homeassistant/components/updater/* @home-assistant/core tests/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman @ehendrix23 diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 7059df580d9..1ab827529c6 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -31,11 +31,10 @@ "tag", "timer", "usb", - "update", "webhook", "zeroconf", "zone" ], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" -} \ No newline at end of file +} diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py deleted file mode 100644 index 66f99b83117..00000000000 --- a/homeassistant/components/update/__init__.py +++ /dev/null @@ -1,273 +0,0 @@ -"""Support for Update.""" -from __future__ import annotations - -import asyncio -import dataclasses -import logging -from typing import Any, Protocol - -import async_timeout -import voluptuous as vol - -from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import integration_platform, storage -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "update" -INFO_CALLBACK_TIMEOUT = 5 -STORAGE_VERSION = 1 - - -class IntegrationUpdateFailed(HomeAssistantError): - """Error to indicate an update has failed.""" - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Update integration.""" - hass.data[DOMAIN] = UpdateManager(hass=hass) - websocket_api.async_register_command(hass, handle_info) - websocket_api.async_register_command(hass, handle_update) - websocket_api.async_register_command(hass, handle_skip) - return True - - -@websocket_api.websocket_command({vol.Required("type"): "update/info"}) -@websocket_api.async_response -async def handle_info( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Get pending updates from all platforms.""" - manager: UpdateManager = hass.data[DOMAIN] - updates = await manager.gather_updates() - connection.send_result(msg["id"], updates) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "update/skip", - vol.Required("domain"): str, - vol.Required("identifier"): str, - vol.Required("version"): str, - } -) -@websocket_api.async_response -async def handle_skip( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Skip an update.""" - manager: UpdateManager = hass.data[DOMAIN] - - if not await manager.domain_is_valid(msg["domain"]): - connection.send_error( - msg["id"], websocket_api.ERR_NOT_FOUND, "Domain not supported" - ) - return - - manager.skip_update(msg["domain"], msg["identifier"], msg["version"]) - connection.send_result(msg["id"]) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "update/update", - vol.Required("domain"): str, - vol.Required("identifier"): str, - vol.Required("version"): str, - vol.Optional("backup"): bool, - } -) -@websocket_api.async_response -async def handle_update( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Handle an update.""" - manager: UpdateManager = hass.data[DOMAIN] - - if not await manager.domain_is_valid(msg["domain"]): - connection.send_error( - msg["id"], - websocket_api.ERR_NOT_FOUND, - f"{msg['domain']} is not a supported domain", - ) - return - - try: - await manager.perform_update( - domain=msg["domain"], - identifier=msg["identifier"], - version=msg["version"], - backup=msg.get("backup"), - ) - except IntegrationUpdateFailed as err: - connection.send_error( - msg["id"], - "update_failed", - str(err), - ) - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Update of %s to version %s failed", - msg["identifier"], - msg["version"], - ) - connection.send_error( - msg["id"], - "update_failed", - "Unknown Error", - ) - else: - connection.send_result(msg["id"]) - - -class UpdatePlatformProtocol(Protocol): - """Define the format that update platforms can have.""" - - async def async_list_updates(self, hass: HomeAssistant) -> list[UpdateDescription]: - """List all updates available in the integration.""" - - async def async_perform_update( - self, - hass: HomeAssistant, - identifier: str, - version: str, - **kwargs: Any, - ) -> None: - """Perform an update.""" - - -@dataclasses.dataclass() -class UpdateDescription: - """Describe an update update.""" - - identifier: str - name: str - current_version: str - available_version: str - changelog_content: str | None = None - changelog_url: str | None = None - icon_url: str | None = None - supports_backup: bool = False - - -class UpdateManager: - """Update manager for the update integration.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the update manager.""" - self._hass = hass - self._store = storage.Store( - hass=hass, - version=STORAGE_VERSION, - key=DOMAIN, - ) - self._skip: set[str] = set() - self._platforms: dict[str, UpdatePlatformProtocol] = {} - self._loaded = False - - async def add_platform( - self, - hass: HomeAssistant, - integration_domain: str, - platform: UpdatePlatformProtocol, - ) -> None: - """Add a platform to the update manager.""" - self._platforms[integration_domain] = platform - - async def _load(self) -> None: - """Load platforms and data from storage.""" - await integration_platform.async_process_integration_platforms( - self._hass, DOMAIN, self.add_platform - ) - from_storage = await self._store.async_load() - if isinstance(from_storage, dict): - self._skip = set(from_storage["skipped"]) - - self._loaded = True - - async def gather_updates(self) -> list[dict[str, Any]]: - """Gather updates.""" - if not self._loaded: - await self._load() - - updates: dict[str, list[UpdateDescription] | None] = {} - - for domain, update_descriptions in zip( - self._platforms, - await asyncio.gather( - *( - self._get_integration_info(integration_domain, registration) - for integration_domain, registration in self._platforms.items() - ) - ), - ): - updates[domain] = update_descriptions - - return [ - { - "domain": integration_domain, - **dataclasses.asdict(description), - } - for integration_domain, update_descriptions in updates.items() - if update_descriptions is not None - for description in update_descriptions - if f"{integration_domain}_{description.identifier}_{description.available_version}" - not in self._skip - ] - - async def domain_is_valid(self, domain: str) -> bool: - """Return if the domain is valid.""" - if not self._loaded: - await self._load() - return domain in self._platforms - - @callback - def _data_to_save(self) -> dict[str, Any]: - """Schedule storing the data.""" - return {"skipped": list(self._skip)} - - async def perform_update( - self, - domain: str, - identifier: str, - version: str, - **kwargs: Any, - ) -> None: - """Perform an update.""" - await self._platforms[domain].async_perform_update( - hass=self._hass, - identifier=identifier, - version=version, - **kwargs, - ) - - @callback - def skip_update(self, domain: str, identifier: str, version: str) -> None: - """Skip an update.""" - self._skip.add(f"{domain}_{identifier}_{version}") - self._store.async_delay_save(self._data_to_save, 60) - - async def _get_integration_info( - self, - integration_domain: str, - platform: UpdatePlatformProtocol, - ) -> list[UpdateDescription] | None: - """Get integration update details.""" - - try: - async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): - return await platform.async_list_updates(hass=self._hass) - except asyncio.TimeoutError: - _LOGGER.warning("Timeout while getting updates from %s", integration_domain) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error fetching info from %s", integration_domain) - return None diff --git a/homeassistant/components/update/manifest.json b/homeassistant/components/update/manifest.json deleted file mode 100644 index a005381fb5f..00000000000 --- a/homeassistant/components/update/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "update", - "name": "Update", - "documentation": "https://www.home-assistant.io/integrations/update", - "codeowners": [ - "@home-assistant/core" - ], - "quality_scale": "internal", - "iot_class": "calculated" -} \ No newline at end of file diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json deleted file mode 100644 index 95b82de3b4d..00000000000 --- a/homeassistant/components/update/strings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Update" -} \ No newline at end of file diff --git a/homeassistant/components/update/translations/ca.json b/homeassistant/components/update/translations/ca.json deleted file mode 100644 index 396e79c14c0..00000000000 --- a/homeassistant/components/update/translations/ca.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Actualitza" -} \ No newline at end of file diff --git a/homeassistant/components/update/translations/de.json b/homeassistant/components/update/translations/de.json deleted file mode 100644 index 18562d81eaf..00000000000 --- a/homeassistant/components/update/translations/de.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aktualisieren" -} \ No newline at end of file diff --git a/homeassistant/components/update/translations/el.json b/homeassistant/components/update/translations/el.json deleted file mode 100644 index d687d342ec3..00000000000 --- a/homeassistant/components/update/translations/el.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" -} \ No newline at end of file diff --git a/homeassistant/components/update/translations/en.json b/homeassistant/components/update/translations/en.json deleted file mode 100644 index 95b82de3b4d..00000000000 --- a/homeassistant/components/update/translations/en.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Update" -} \ No newline at end of file diff --git a/homeassistant/components/update/translations/it.json b/homeassistant/components/update/translations/it.json deleted file mode 100644 index 539f0bb4294..00000000000 --- a/homeassistant/components/update/translations/it.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Aggiornamento" -} \ No newline at end of file diff --git a/homeassistant/components/update/translations/pt-BR.json b/homeassistant/components/update/translations/pt-BR.json deleted file mode 100644 index 4003445e2c3..00000000000 --- a/homeassistant/components/update/translations/pt-BR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "Atualiza\u00e7\u00e3o" -} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index c3f6540273e..d674160c8be 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2078,17 +2078,6 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.update.*] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_subclassing_any = true -disallow_untyped_calls = true -disallow_untyped_decorators = true -disallow_untyped_defs = true -no_implicit_optional = true -warn_return_any = true -warn_unreachable = true - [mypy-homeassistant.components.uptime.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/update/__init__.py b/tests/components/update/__init__.py deleted file mode 100644 index f3e55ca4ed3..00000000000 --- a/tests/components/update/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Update integration.""" diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py deleted file mode 100644 index f91df08bf52..00000000000 --- a/tests/components/update/test_init.py +++ /dev/null @@ -1,347 +0,0 @@ -"""Tests for the Update integration init.""" -from __future__ import annotations - -import asyncio -from collections.abc import Awaitable, Callable -from typing import Any -from unittest.mock import Mock, patch - -from aiohttp import ClientWebSocketResponse -import pytest - -from homeassistant.components.update import ( - DOMAIN, - IntegrationUpdateFailed, - UpdateDescription, -) -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component - -from tests.common import mock_platform - - -async def setup_mock_domain( - hass: HomeAssistant, - async_list_updates: Callable[[HomeAssistant], Awaitable[list[UpdateDescription]]] - | None = None, - async_perform_update: Callable[[HomeAssistant, str, str], Awaitable[bool]] - | None = None, -) -> None: - """Set up a mock domain.""" - - async def _mock_async_list_updates(hass: HomeAssistant) -> list[UpdateDescription]: - return [ - UpdateDescription( - identifier="lorem_ipsum", - name="Lorem Ipsum", - current_version="1.0.0", - available_version="1.0.1", - ) - ] - - async def _mock_async_perform_update( - hass: HomeAssistant, - identifier: str, - version: str, - **kwargs: Any, - ) -> bool: - return True - - mock_platform( - hass, - "some_domain.update", - Mock( - async_list_updates=async_list_updates or _mock_async_list_updates, - async_perform_update=async_perform_update or _mock_async_perform_update, - ), - ) - - assert await async_setup_component(hass, "some_domain", {}) - - -async def gather_update_info( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> list[dict]: - """Gather all info.""" - client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "update/info"}) - resp = await client.receive_json() - return resp["result"] - - -async def test_update_updates( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test getting updates.""" - await setup_mock_domain(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - - with patch( - "homeassistant.components.update.storage.Store.async_load", - return_value={"skipped": []}, - ): - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - data = data[0] == { - "domain": "some_domain", - "identifier": "lorem_ipsum", - "name": "Lorem Ipsum", - "current_version": "1.0.0", - "available_version": "1.0.1", - "changelog_url": None, - "icon_url": None, - } - - -async def test_update_updates_with_timeout_error( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test timeout while getting updates.""" - - async def mock_async_list_updates(hass: HomeAssistant) -> list[UpdateDescription]: - raise asyncio.TimeoutError() - - await setup_mock_domain(hass, async_list_updates=mock_async_list_updates) - - assert await async_setup_component(hass, DOMAIN, {}) - - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 0 - - -async def test_update_updates_with_exception( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test exception while getting updates.""" - - async def mock_async_list_updates(hass: HomeAssistant) -> list[UpdateDescription]: - raise Exception() - - await setup_mock_domain(hass, async_list_updates=mock_async_list_updates) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 0 - - -async def test_update_update( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test performing an update.""" - await setup_mock_domain(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - update = data[0] - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/update", - "domain": update["domain"], - "identifier": update["identifier"], - "version": update["available_version"], - } - ) - resp = await client.receive_json() - assert resp["success"] - - -async def test_skip_update( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test skipping updates.""" - await setup_mock_domain(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - update = data[0] - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/skip", - "domain": update["domain"], - "identifier": update["identifier"], - "version": update["available_version"], - } - ) - resp = await client.receive_json() - assert resp["success"] - - data = await gather_update_info(hass, hass_ws_client) - assert len(data) == 0 - - -async def test_skip_non_existing_update( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test skipping non-existing updates.""" - await setup_mock_domain(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/skip", - "domain": "non_existing", - "identifier": "non_existing", - "version": "non_existing", - } - ) - resp = await client.receive_json() - assert not resp["success"] - - data = await gather_update_info(hass, hass_ws_client) - assert len(data) == 1 - - -async def test_update_update_non_existing( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test that we fail when trying to update something that does not exist.""" - await setup_mock_domain(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/update", - "domain": "does_not_exist", - "identifier": "does_not_exist", - "version": "non_existing", - } - ) - resp = await client.receive_json() - assert not resp["success"] - assert resp["error"]["code"] == "not_found" - - -async def test_update_update_failed( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test that we correctly handle failed updates.""" - - async def mock_async_perform_update( - hass: HomeAssistant, - identifier: str, - version: str, - **kwargs, - ) -> bool: - raise IntegrationUpdateFailed("Test update failed") - - await setup_mock_domain(hass, async_perform_update=mock_async_perform_update) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - update = data[0] - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/update", - "domain": update["domain"], - "identifier": update["identifier"], - "version": update["available_version"], - } - ) - resp = await client.receive_json() - assert not resp["success"] - assert resp["error"]["code"] == "update_failed" - assert resp["error"]["message"] == "Test update failed" - - -async def test_update_update_failed_generic( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], - caplog: pytest.LogCaptureFixture, -) -> None: - """Test that we correctly handle failed updates.""" - - async def mock_async_perform_update( - hass: HomeAssistant, - identifier: str, - version: str, - **kwargs, - ) -> bool: - raise TypeError("Test update failed") - - await setup_mock_domain(hass, async_perform_update=mock_async_perform_update) - - assert await async_setup_component(hass, DOMAIN, {}) - data = await gather_update_info(hass, hass_ws_client) - - assert len(data) == 1 - update = data[0] - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/update", - "domain": update["domain"], - "identifier": update["identifier"], - "version": update["available_version"], - } - ) - resp = await client.receive_json() - assert not resp["success"] - assert resp["error"]["code"] == "update_failed" - assert resp["error"]["message"] == "Unknown Error" - assert "Test update failed" in caplog.text - - -async def test_update_before_info( - hass: HomeAssistant, - hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], -) -> None: - """Test that we fail when trying to update something that does not exist.""" - await setup_mock_domain(hass) - - assert await async_setup_component(hass, DOMAIN, {}) - - client = await hass_ws_client(hass) - await client.send_json( - { - "id": 1, - "type": "update/update", - "domain": "does_not_exist", - "identifier": "does_not_exist", - "version": "non_existing", - } - ) - resp = await client.receive_json() - assert not resp["success"] - assert resp["error"]["code"] == "not_found" From c3744352823106782b48f0060e58007e0445bf77 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Mar 2022 00:18:11 +0000 Subject: [PATCH 0294/1054] [ci skip] Translation update --- .../binary_sensor/translations/fr.json | 4 ++ .../components/dlna_dms/translations/fr.json | 13 ++++ .../components/fritz/translations/fr.json | 3 +- .../components/group/translations/ca.json | 62 +++++++++++++++++++ .../components/group/translations/de.json | 62 +++++++++++++++++++ .../components/group/translations/el.json | 62 +++++++++++++++++++ .../components/group/translations/en.json | 62 +++++++++++++++++++ .../components/group/translations/es.json | 15 +++++ .../components/group/translations/et.json | 62 +++++++++++++++++++ .../components/group/translations/id.json | 62 +++++++++++++++++++ .../components/group/translations/it.json | 62 +++++++++++++++++++ .../components/group/translations/pl.json | 62 +++++++++++++++++++ .../components/group/translations/ru.json | 62 +++++++++++++++++++ .../group/translations/zh-Hant.json | 62 +++++++++++++++++++ .../kaleidescape/translations/en.json | 46 +++++++------- .../components/mjpeg/translations/fr.json | 12 ++++ .../components/moon/translations/fr.json | 12 ++++ .../components/onewire/translations/fr.json | 18 ++++++ .../pure_energie/translations/fr.json | 11 ++++ .../components/rfxtrx/translations/fr.json | 1 + .../components/season/translations/ca.json | 14 +++++ .../components/season/translations/de.json | 14 +++++ .../components/season/translations/el.json | 14 +++++ .../components/season/translations/et.json | 14 +++++ .../components/season/translations/id.json | 14 +++++ .../components/season/translations/it.json | 14 +++++ .../season/translations/sensor.en.json | 6 ++ .../components/sense/translations/fr.json | 10 +++ .../components/sensibo/translations/fr.json | 7 ++- .../components/sleepiq/translations/fr.json | 14 +++++ .../components/sonarr/translations/fr.json | 1 + .../components/switch/translations/id.json | 10 +++ .../components/update/translations/ca.json | 3 + .../components/update/translations/de.json | 3 + .../components/update/translations/el.json | 3 + .../components/update/translations/en.json | 3 + .../components/update/translations/it.json | 3 + .../components/update/translations/pt-BR.json | 3 + 38 files changed, 879 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/dlna_dms/translations/fr.json create mode 100644 homeassistant/components/mjpeg/translations/fr.json create mode 100644 homeassistant/components/moon/translations/fr.json create mode 100644 homeassistant/components/pure_energie/translations/fr.json create mode 100644 homeassistant/components/season/translations/ca.json create mode 100644 homeassistant/components/season/translations/de.json create mode 100644 homeassistant/components/season/translations/el.json create mode 100644 homeassistant/components/season/translations/et.json create mode 100644 homeassistant/components/season/translations/id.json create mode 100644 homeassistant/components/season/translations/it.json create mode 100644 homeassistant/components/sleepiq/translations/fr.json create mode 100644 homeassistant/components/update/translations/ca.json create mode 100644 homeassistant/components/update/translations/de.json create mode 100644 homeassistant/components/update/translations/el.json create mode 100644 homeassistant/components/update/translations/en.json create mode 100644 homeassistant/components/update/translations/it.json create mode 100644 homeassistant/components/update/translations/pt-BR.json diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index 1705fc7aed2..9c1eaf871b0 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -134,6 +134,10 @@ "off": "Pas en charge", "on": "En charge" }, + "carbon_monoxide": { + "off": "RAS", + "on": "D\u00e9tect\u00e9" + }, "co": { "off": "Clair", "on": "D\u00e9tect\u00e9e" diff --git a/homeassistant/components/dlna_dms/translations/fr.json b/homeassistant/components/dlna_dms/translations/fr.json new file mode 100644 index 00000000000..dfa510250ac --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index 38c8a9e802d..7c58c167ae6 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -56,7 +56,8 @@ "step": { "init": { "data": { - "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'" + "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'", + "old_discovery": "Autoriser l'ancienne m\u00e9thode de d\u00e9couverte" } } } diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index 552a2c9677e..bbf9a1657dd 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Membres del grup", + "name": "Nom del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "cover_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "fan": { + "data": { + "entities": "Membres del grup", + "name": "Nom del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "fan_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "init": { + "data": { + "group_type": "Tipus de grup" + }, + "description": "Selecciona el tipus de grup" + }, + "light": { + "data": { + "entities": "Membres del grup", + "name": "Nom del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "light_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "media_player": { + "data": { + "entities": "Membres del grup", + "name": "Nom del grup" + }, + "description": "Selecciona les opcions del grup" + }, + "media_player_options": { + "data": { + "entities": "Membres del grup" + }, + "description": "Selecciona les opcions del grup" + } + } + }, "state": { "_": { "closed": "Tancat/ada", diff --git a/homeassistant/components/group/translations/de.json b/homeassistant/components/group/translations/de.json index 80da069e72a..d67de225e66 100644 --- a/homeassistant/components/group/translations/de.json +++ b/homeassistant/components/group/translations/de.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Gruppenmitglieder", + "name": "Gruppenname" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "cover_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "fan": { + "data": { + "entities": "Gruppenmitglieder", + "name": "Gruppenname" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "fan_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "init": { + "data": { + "group_type": "Gruppentyp" + }, + "description": "Gruppentyp ausw\u00e4hlen" + }, + "light": { + "data": { + "entities": "Gruppenmitglieder", + "name": "Gruppenname" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "light_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "media_player": { + "data": { + "entities": "Gruppenmitglieder", + "name": "Gruppenname" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "media_player_options": { + "data": { + "entities": "Gruppenmitglieder" + }, + "description": "Gruppenoptionen ausw\u00e4hlen" + } + } + }, "state": { "_": { "closed": "Geschlossen", diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index 25931b229df..8cecb56bc9f 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "cover_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "fan": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "fan_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "init": { + "data": { + "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "light": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "light_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "media_player": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "media_player_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + } + } + }, "state": { "_": { "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03ae", diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index 271ada378ca..e0b55e89620 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Group members", + "name": "Group name" + }, + "description": "Select group options" + }, + "cover_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, + "fan": { + "data": { + "entities": "Group members", + "name": "Group name" + }, + "description": "Select group options" + }, + "fan_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, + "init": { + "data": { + "group_type": "Group type" + }, + "description": "Select group type" + }, + "light": { + "data": { + "entities": "Group members", + "name": "Group name" + }, + "description": "Select group options" + }, + "light_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, + "media_player": { + "data": { + "entities": "Group members", + "name": "Group name" + }, + "description": "Select group options" + }, + "media_player_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + } + } + }, "state": { "_": { "closed": "Closed", diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index 9aac8e09780..e74adda486d 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -1,4 +1,19 @@ { + "config": { + "step": { + "cover": { + "data": { + "name": "Nombre del Grupo" + }, + "description": "Seleccionar opciones de grupo" + }, + "cover_options": { + "data": { + "entities": "Miembros del grupo" + } + } + } + }, "state": { "_": { "closed": "Cerrado", diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index dacd0973f1d..b6ba60ead88 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "R\u00fchma liikmed", + "name": "R\u00fchma nimi" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "cover_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "fan": { + "data": { + "entities": "R\u00fchma liikmed", + "name": "R\u00fchma nimi" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "fan_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "init": { + "data": { + "group_type": "R\u00fchma t\u00fc\u00fcp" + }, + "description": "Vali r\u00fchma t\u00fc\u00fcp" + }, + "light": { + "data": { + "entities": "R\u00fchma liikmed", + "name": "R\u00fchma nimi" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "light_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "media_player": { + "data": { + "entities": "R\u00fchma liikmed", + "name": "R\u00fchma nimi" + }, + "description": "R\u00fchmasuvandite valimine" + }, + "media_player_options": { + "data": { + "entities": "R\u00fchma liikmed" + }, + "description": "R\u00fchmasuvandite valimine" + } + } + }, "state": { "_": { "closed": "Suletud", diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json index 553cbce0550..b42b663e546 100644 --- a/homeassistant/components/group/translations/id.json +++ b/homeassistant/components/group/translations/id.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Anggota grup", + "name": "Nama grup" + }, + "description": "Pilih opsi grup" + }, + "cover_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "fan": { + "data": { + "entities": "Anggota grup", + "name": "Nama grup" + }, + "description": "Pilih opsi grup" + }, + "fan_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "init": { + "data": { + "group_type": "Jenis grup" + }, + "description": "Pilih jenis grup" + }, + "light": { + "data": { + "entities": "Anggota grup", + "name": "Nama grup" + }, + "description": "Pilih opsi grup" + }, + "light_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + }, + "media_player": { + "data": { + "entities": "Anggota grup", + "name": "Nama grup" + }, + "description": "Pilih opsi grup" + }, + "media_player_options": { + "data": { + "entities": "Anggota grup" + }, + "description": "Pilih opsi grup" + } + } + }, "state": { "_": { "closed": "Tertutup", diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index 67e13aa7cfa..1f3fcb467dd 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Membri del gruppo", + "name": "Nome del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "cover_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "fan": { + "data": { + "entities": "Membri del gruppo", + "name": "Nome del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "fan_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "init": { + "data": { + "group_type": "Tipo di gruppo" + }, + "description": "Seleziona il tipo di gruppo" + }, + "light": { + "data": { + "entities": "Membri del gruppo", + "name": "Nome del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "light_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "media_player": { + "data": { + "entities": "Membri del gruppo", + "name": "Nome del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + }, + "media_player_options": { + "data": { + "entities": "Membri del gruppo" + }, + "description": "Seleziona le opzioni di gruppo" + } + } + }, "state": { "_": { "closed": "Chiuso", diff --git a/homeassistant/components/group/translations/pl.json b/homeassistant/components/group/translations/pl.json index b45f3f85cfb..fe6653cfd5e 100644 --- a/homeassistant/components/group/translations/pl.json +++ b/homeassistant/components/group/translations/pl.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Encje w grupie", + "name": "Nazwa grupy" + }, + "description": "Wybierz opcje grupy" + }, + "cover_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "fan": { + "data": { + "entities": "Encje w grupie", + "name": "Nazwa grupy" + }, + "description": "Wybierz opcje grupy" + }, + "fan_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "init": { + "data": { + "group_type": "Typ grupy" + }, + "description": "Wybierz typ grupy" + }, + "light": { + "data": { + "entities": "Encje w grupie", + "name": "Nazwa grupy" + }, + "description": "Wybierz opcje grupy" + }, + "light_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + }, + "media_player": { + "data": { + "entities": "Encje w grupie", + "name": "Nazwa grupy" + }, + "description": "Wybierz opcje grupy" + }, + "media_player_options": { + "data": { + "entities": "Encje w grupie" + }, + "description": "Wybierz opcje grupy" + } + } + }, "state": { "_": { "closed": "zamkni\u0119te", diff --git a/homeassistant/components/group/translations/ru.json b/homeassistant/components/group/translations/ru.json index 7e8ab4d8be1..3ec3b2b0c97 100644 --- a/homeassistant/components/group/translations/ru.json +++ b/homeassistant/components/group/translations/ru.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "cover_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "fan": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "fan_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "init": { + "data": { + "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "light": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "light_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "media_player": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "media_player_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + } + } + }, "state": { "_": { "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index 790feb0c5ff..f7cffdad9de 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1", + "name": "\u7fa4\u7d44\u540d\u7a31" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "cover_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "fan": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1", + "name": "\u7fa4\u7d44\u540d\u7a31" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "fan_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "init": { + "data": { + "group_type": "\u7fa4\u7d44\u985e\u578b" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u985e\u578b" + }, + "light": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1", + "name": "\u7fa4\u7d44\u540d\u7a31" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "light_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "media_player": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1", + "name": "\u7fa4\u7d44\u540d\u7a31" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "media_player_options": { + "data": { + "entities": "\u7fa4\u7d44\u6210\u54e1" + }, + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + } + } + }, "state": { "_": { "closed": "\u95dc\u9589", diff --git a/homeassistant/components/kaleidescape/translations/en.json b/homeassistant/components/kaleidescape/translations/en.json index 38dd84843c3..43be9c030c0 100644 --- a/homeassistant/components/kaleidescape/translations/en.json +++ b/homeassistant/components/kaleidescape/translations/en.json @@ -1,27 +1,27 @@ { - "config": { - "flow_title": "{model} ({name})", - "step": { - "user": { - "title": "Kaleidescape Setup", - "data": { - "host": "Host" + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "unknown": "Unexpected error", + "unsupported": "Unsupported device" + }, + "error": { + "cannot_connect": "Failed to connect", + "unsupported": "Unsupported device" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Do you want to set up the {model} player named {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape Setup" + } } - }, - "discovery_confirm": { - "title": "Kaleidescape Setup", - "description": "Do you want to set up the {model} player named {name}?" - } - }, - "abort": { - "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress", - "unknown": "Unexpected error", - "unsupported": "Unsupported device" - }, - "error": { - "cannot_connect": "Failed to connect", - "unsupported": "Unsupported device" } - } } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/fr.json b/homeassistant/components/mjpeg/translations/fr.json new file mode 100644 index 00000000000..abc52237b92 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/fr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/fr.json b/homeassistant/components/moon/translations/fr.json new file mode 100644 index 00000000000..1bd91ac8a21 --- /dev/null +++ b/homeassistant/components/moon/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/fr.json b/homeassistant/components/onewire/translations/fr.json index 13a5438b1a9..b9e373d2cd9 100644 --- a/homeassistant/components/onewire/translations/fr.json +++ b/homeassistant/components/onewire/translations/fr.json @@ -22,5 +22,23 @@ "title": "Configurer 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "S\u00e9lectionnez les appareils \u00e0 configurer" + }, + "step": { + "configure_device": { + "data": { + "precision": "Pr\u00e9cision du capteur" + }, + "description": "Choisissez la pr\u00e9cision du capteur pour {sensor_id}" + }, + "device_selection": { + "data": { + "device_selection": "S\u00e9lectionnez les appareils \u00e0 configurer." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/fr.json b/homeassistant/components/pure_energie/translations/fr.json new file mode 100644 index 00000000000..9cb1d7dfd16 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index d101cfbc57a..edae1557066 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -66,6 +66,7 @@ "debug": "Activer le d\u00e9bogage", "device": "S\u00e9lectionnez l'appareil \u00e0 configurer", "event_code": "Entrez le code d'\u00e9v\u00e9nement \u00e0 ajouter", + "protocols": "Protocoles", "remove_device": "S\u00e9lectionnez l'appareil \u00e0 supprimer" }, "title": "Options Rfxtrx" diff --git a/homeassistant/components/season/translations/ca.json b/homeassistant/components/season/translations/ca.json new file mode 100644 index 00000000000..b12e0e3019c --- /dev/null +++ b/homeassistant/components/season/translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "type": "Definici\u00f3 del tipus d'estaci\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/de.json b/homeassistant/components/season/translations/de.json new file mode 100644 index 00000000000..fe2819b3c40 --- /dev/null +++ b/homeassistant/components/season/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "type": "Definition der Saison" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/el.json b/homeassistant/components/season/translations/el.json new file mode 100644 index 00000000000..52bf5ca6126 --- /dev/null +++ b/homeassistant/components/season/translations/el.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "step": { + "user": { + "data": { + "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03b5\u03c0\u03bf\u03c7\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/et.json b/homeassistant/components/season/translations/et.json new file mode 100644 index 00000000000..6c11c8136e1 --- /dev/null +++ b/homeassistant/components/season/translations/et.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "data": { + "type": "Hooaja t\u00fc\u00fcbi m\u00e4\u00e4ratlus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/id.json b/homeassistant/components/season/translations/id.json new file mode 100644 index 00000000000..ef7de1c9d3a --- /dev/null +++ b/homeassistant/components/season/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "type": "Jenis definisi musim" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/it.json b/homeassistant/components/season/translations/it.json new file mode 100644 index 00000000000..f77a7410705 --- /dev/null +++ b/homeassistant/components/season/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "type": "Tipo di definizione della stagione" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.en.json b/homeassistant/components/season/translations/sensor.en.json index 91c7ac12bfc..54e0ad8e98f 100644 --- a/homeassistant/components/season/translations/sensor.en.json +++ b/homeassistant/components/season/translations/sensor.en.json @@ -5,6 +5,12 @@ "spring": "Spring", "summer": "Summer", "winter": "Winter" + }, + "season__season__": { + "autumn": "Autumn", + "spring": "Spring", + "summer": "Summer", + "winter": "Winter" } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index bdd588eae74..0b0d886a335 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -9,6 +9,11 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_validate": { + "data": { + "password": "Mot de passe" + } + }, "user": { "data": { "email": "Email", @@ -16,6 +21,11 @@ "timeout": "D\u00e9lai expir\u00e9" }, "title": "Connectez-vous \u00e0 votre moniteur d'\u00e9nergie Sense" + }, + "validation": { + "data": { + "code": "Code de v\u00e9rification" + } } } } diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 43b91dff520..79e06ca81a1 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "no_devices": "Aucun appareil d\u00e9couvert" }, "step": { "user": { diff --git a/homeassistant/components/sleepiq/translations/fr.json b/homeassistant/components/sleepiq/translations/fr.json new file mode 100644 index 00000000000..199a0bd89f5 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json index c4a7a8764ac..de666f5e5a2 100644 --- a/homeassistant/components/sonarr/translations/fr.json +++ b/homeassistant/components/sonarr/translations/fr.json @@ -22,6 +22,7 @@ "host": "H\u00f4te", "port": "Port", "ssl": "Utilise un certificat SSL", + "url": "URL", "verify_ssl": "V\u00e9rifier le certificat SSL" } } diff --git a/homeassistant/components/switch/translations/id.json b/homeassistant/components/switch/translations/id.json index ca341378714..c49cab13a9c 100644 --- a/homeassistant/components/switch/translations/id.json +++ b/homeassistant/components/switch/translations/id.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitas saklar" + }, + "description": "Pilih saklar mana untuk saklar lampu." + } + } + }, "device_automation": { "action_type": { "toggle": "Nyala/matikan {entity_name}", diff --git a/homeassistant/components/update/translations/ca.json b/homeassistant/components/update/translations/ca.json new file mode 100644 index 00000000000..396e79c14c0 --- /dev/null +++ b/homeassistant/components/update/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Actualitza" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/de.json b/homeassistant/components/update/translations/de.json new file mode 100644 index 00000000000..18562d81eaf --- /dev/null +++ b/homeassistant/components/update/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Aktualisieren" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/el.json b/homeassistant/components/update/translations/el.json new file mode 100644 index 00000000000..d687d342ec3 --- /dev/null +++ b/homeassistant/components/update/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/en.json b/homeassistant/components/update/translations/en.json new file mode 100644 index 00000000000..95b82de3b4d --- /dev/null +++ b/homeassistant/components/update/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/it.json b/homeassistant/components/update/translations/it.json new file mode 100644 index 00000000000..539f0bb4294 --- /dev/null +++ b/homeassistant/components/update/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Aggiornamento" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/pt-BR.json b/homeassistant/components/update/translations/pt-BR.json new file mode 100644 index 00000000000..4003445e2c3 --- /dev/null +++ b/homeassistant/components/update/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Atualiza\u00e7\u00e3o" +} \ No newline at end of file From aed2c1cce8708497ae2d71404828daaae30128e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 03:13:00 +0100 Subject: [PATCH 0295/1054] Remove unused import code from powerview (#67834) --- .../hunterdouglas_powerview/scene.py | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 224d5bf7b7c..3476db4949c 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -4,21 +4,16 @@ from __future__ import annotations from typing import Any from aiopvapi.resources.scene import Scene as PvScene -import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PLATFORM +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( COORDINATOR, DEVICE_INFO, DOMAIN, - HUB_ADDRESS, PV_API, PV_ROOM_DATA, PV_SCENE_DATA, @@ -27,27 +22,6 @@ from .const import ( ) from .entity import HDEntity -PLATFORM_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(HUB_ADDRESS): cv.string} -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import platform from yaml.""" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: config[HUB_ADDRESS]}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback From a75bbc79a2aa27d48c91396ccbd0164819b41467 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 05:42:16 +0100 Subject: [PATCH 0296/1054] Prevent polling from recreating an entity after removal (#67750) --- homeassistant/helpers/entity.py | 30 +++++++++++++++++++++------ tests/helpers/test_entity.py | 16 ++++++++++++++ tests/helpers/test_entity_platform.py | 24 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8e4f6bc8b58..a554a093c5c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -6,6 +6,7 @@ import asyncio from collections.abc import Awaitable, Iterable, Mapping, MutableMapping from dataclasses import dataclass from datetime import datetime, timedelta +from enum import Enum, auto import functools as ft import logging import math @@ -207,6 +208,19 @@ class EntityCategory(StrEnum): SYSTEM = "system" +class EntityPlatformState(Enum): + """The platform state of an entity.""" + + # Not Added: Not yet added to a platform, polling updates are written to the state machine + NOT_ADDED = auto() + + # Added: Added to a platform, polling updates are written to the state machine + ADDED = auto() + + # Removed: Removed from a platform, polling updates are not written to the state machine + REMOVED = auto() + + def convert_to_entity_category( value: EntityCategory | str | None, raise_report: bool = True ) -> EntityCategory | None: @@ -294,7 +308,7 @@ class Entity(ABC): _context_set: datetime | None = None # If entity is added to an entity platform - _added = False + _platform_state = EntityPlatformState.NOT_ADDED # Entity Properties _attr_assumed_state: bool = False @@ -553,6 +567,10 @@ class Entity(ABC): @callback def _async_write_ha_state(self) -> None: """Write the state to the state machine.""" + if self._platform_state == EntityPlatformState.REMOVED: + # Polling returned after the entity has already been removed + return + if self.registry_entry and self.registry_entry.disabled_by: if not self._disabled_reported: self._disabled_reported = True @@ -758,7 +776,7 @@ class Entity(ABC): parallel_updates: asyncio.Semaphore | None, ) -> None: """Start adding an entity to a platform.""" - if self._added: + if self._platform_state == EntityPlatformState.ADDED: raise HomeAssistantError( f"Entity {self.entity_id} cannot be added a second time to an entity platform" ) @@ -766,7 +784,7 @@ class Entity(ABC): self.hass = hass self.platform = platform self.parallel_updates = parallel_updates - self._added = True + self._platform_state = EntityPlatformState.ADDED @callback def add_to_platform_abort(self) -> None: @@ -774,7 +792,7 @@ class Entity(ABC): self.hass = None # type: ignore[assignment] self.platform = None self.parallel_updates = None - self._added = False + self._platform_state = EntityPlatformState.NOT_ADDED async def add_to_platform_finish(self) -> None: """Finish adding an entity to a platform.""" @@ -792,12 +810,12 @@ class Entity(ABC): If the entity doesn't have a non disabled entry in the entity registry, or if force_remove=True, its state will be removed. """ - if self.platform and not self._added: + if self.platform and self._platform_state != EntityPlatformState.ADDED: raise HomeAssistantError( f"Entity {self.entity_id} async_remove called twice" ) - self._added = False + self._platform_state = EntityPlatformState.REMOVED if self._on_remove is not None: while self._on_remove: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 6b7de074a24..afc0887371e 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -545,6 +545,22 @@ async def test_async_remove_runs_callbacks(hass): assert len(result) == 1 +async def test_async_remove_ignores_in_flight_polling(hass): + """Test in flight polling is ignored after removing.""" + result = [] + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "test.test" + ent.async_on_remove(lambda: result.append(1)) + ent.async_write_ha_state() + assert hass.states.get("test.test").state == STATE_UNKNOWN + await ent.async_remove() + assert len(result) == 1 + assert hass.states.get("test.test") is None + ent.async_write_ha_state() + + async def test_set_context(hass): """Test setting context.""" context = Context() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9aa0a849e5a..c98fdff7858 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -390,6 +390,30 @@ async def test_async_remove_with_platform(hass): assert len(hass.states.async_entity_ids()) == 0 +async def test_async_remove_with_platform_update_finishes(hass): + """Remove an entity when an update finishes after its been removed.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity1 = MockEntity(name="test_1") + + async def _delayed_update(*args, **kwargs): + await asyncio.sleep(0.01) + + entity1.async_update = _delayed_update + + # Add, remove, add, remove and make sure no updates + # cause the entity to reappear after removal + for i in range(2): + await component.async_add_entities([entity1]) + assert len(hass.states.async_entity_ids()) == 1 + entity1.async_write_ha_state() + assert hass.states.get(entity1.entity_id) is not None + task = asyncio.create_task(entity1.async_update_ha_state(True)) + await entity1.async_remove() + assert len(hass.states.async_entity_ids()) == 0 + await task + assert len(hass.states.async_entity_ids()) == 0 + + async def test_not_adding_duplicate_entities_with_unique_id(hass, caplog): """Test for not adding duplicate entities.""" caplog.set_level(logging.ERROR) From c9ac0b49f6e0c566f97a053da6a242455ac40671 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 05:43:19 +0100 Subject: [PATCH 0297/1054] Prevent scene from restoring unavailable states (#67836) --- homeassistant/components/scene/__init__.py | 8 ++++-- tests/components/scene/test_init.py | 29 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 846c0fbc7c6..5dea5965d43 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.light import ATTR_TRANSITION from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON +from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -117,7 +117,11 @@ class Scene(RestoreEntity): """Call when the scene is added to hass.""" await super().async_internal_added_to_hass() state = await self.async_get_last_state() - if state is not None and state.state is not None: + if ( + state is not None + and state.state is not None + and state.state != STATE_UNAVAILABLE + ): self.__last_activated = state.state def activate(self, **kwargs: Any) -> None: diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 41b16261cd1..3dd0cfce7b9 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State @@ -177,6 +178,34 @@ async def test_restore_state(hass, entities, enable_custom_integrations): assert hass.states.get("scene.test").state == "2021-01-01T23:59:59+00:00" +async def test_restore_state_does_not_restore_unavailable( + hass, entities, enable_custom_integrations +): + """Test we restore state integration but ignore unavailable.""" + mock_restore_cache(hass, (State("scene.test", STATE_UNAVAILABLE),)) + + light_1, light_2 = await setup_lights(hass, entities) + + assert await async_setup_component( + hass, + scene.DOMAIN, + { + "scene": [ + { + "name": "test", + "entities": { + light_1.entity_id: "on", + light_2.entity_id: "on", + }, + } + ] + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("scene.test").state == STATE_UNKNOWN + + async def activate(hass, entity_id=ENTITY_MATCH_ALL): """Activate a scene.""" data = {} From 46984afbb80350c8f5228c7f43a7b4c2b22711c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Mar 2022 21:50:25 -0800 Subject: [PATCH 0298/1054] Add media source support to Yamaha MusicCast (#67572) --- .../yamaha_musiccast/media_player.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index bcd1a0a2c1f..ff62799ec58 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,17 +1,22 @@ """Implementation of the musiccast media player.""" from __future__ import annotations +import contextlib import logging from aiomusiccast import MusicCastGroupException, MusicCastMediaContent from aiomusiccast.features import ZoneFeature import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, BrowseMedia, MediaPlayerEntity, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_TRACK, @@ -333,6 +338,10 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if self.state == STATE_OFF: await self.async_turn_on() @@ -353,7 +362,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): ) return - if parts[0] == "http": + if parts[0] in ("http", "https") or media_id.startswith("/"): + media_id = async_process_play_media_url(self.hass, media_id) + await self.coordinator.musiccast.play_url_media( self._zone_id, media_id, "HomeAssistant" ) @@ -365,6 +376,15 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + if media_content_id and media_source.is_media_source_id(media_content_id): + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith( + "audio/" + ), + ) + if self.state == STATE_OFF: raise HomeAssistantError( "The device has to be turned on to be able to browse media." @@ -375,11 +395,13 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): media_content_provider = await MusicCastMediaContent.browse_media( self.coordinator.musiccast, self._zone_id, media_content_path, 24 ) + add_media_source = False else: media_content_provider = MusicCastMediaContent.categories( self.coordinator.musiccast, self._zone_id ) + add_media_source = True def get_content_type(item): if item.can_play: @@ -399,6 +421,21 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): for child in media_content_provider.children ] + if add_media_source: + with contextlib.suppress(media_source.BrowseError): + item = await media_source.async_browse_media( + self.hass, + None, + content_filter=lambda item: item.media_content_type.startswith( + "audio/" + ), + ) + # If domain is None, it's overview of available sources + if item.domain is None: + children.extend(item.children) + else: + children.append(item) + overview = BrowseMedia( title=media_content_provider.title, media_class=MEDIA_CLASS_MAPPING.get(media_content_provider.content_type), From 00c84d892717bae1a35c96550c7756a229644e87 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 8 Mar 2022 06:52:58 +0100 Subject: [PATCH 0299/1054] Modify diagnostics for yale_smart_alarm (#67761) --- .../components/yale_smart_alarm/diagnostics.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/diagnostics.py b/homeassistant/components/yale_smart_alarm/diagnostics.py index 896a3240a22..c650ff5f5ed 100644 --- a/homeassistant/components/yale_smart_alarm/diagnostics.py +++ b/homeassistant/components/yale_smart_alarm/diagnostics.py @@ -15,8 +15,10 @@ TO_REDACT = { "name", "mac", "device_id", - "sensor_map", - "lock_map", + "user_id", + "id", + "mail_address", + "report_account", } @@ -27,4 +29,7 @@ async def async_get_config_entry_diagnostics( coordinator: YaleDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ COORDINATOR ] - return async_redact_data(coordinator.data, TO_REDACT) + + assert coordinator.yale + get_all_data = await hass.async_add_executor_job(coordinator.yale.get_all) + return async_redact_data(get_all_data, TO_REDACT) From d302b0d14e9df9cc46e7e035a0d2be5290182b40 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Tue, 8 Mar 2022 00:00:39 -0600 Subject: [PATCH 0300/1054] Fix todoist parsing due dates for calendar events (#65403) --- CODEOWNERS | 1 + homeassistant/components/todoist/calendar.py | 22 +++++----- homeassistant/components/todoist/types.py | 14 +++++++ requirements_test_all.txt | 3 ++ tests/components/todoist/__init__.py | 1 + tests/components/todoist/test_calendar.py | 44 ++++++++++++++++++++ 6 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/todoist/types.py create mode 100644 tests/components/todoist/__init__.py create mode 100644 tests/components/todoist/test_calendar.py diff --git a/CODEOWNERS b/CODEOWNERS index cd946ea5382..6d145a064a1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1022,6 +1022,7 @@ homeassistant/components/time_date/* @fabaff tests/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl +tests/components/todoist/* @boralyl homeassistant/components/tolo/* @MatthiasLohr tests/components/tolo/* @MatthiasLohr homeassistant/components/totalconnect/* @austinmroczek diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 560ad3efe1f..4582b1de0f0 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,7 +1,7 @@ """Support for Todoist task management (https://todoist.com).""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import logging from todoist.api import TodoistAPI @@ -55,6 +55,7 @@ from .const import ( SUMMARY, TASKS, ) +from .types import DueDate _LOGGER = logging.getLogger(__name__) @@ -219,7 +220,7 @@ def setup_platform( due_date = datetime(due.year, due.month, due.day) # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) - date_format = "%Y-%m-%dT%H:%M%S" + date_format = "%Y-%m-%dT%H:%M:%S" _due["date"] = datetime.strftime(due_date, date_format) if _due: @@ -258,15 +259,15 @@ def setup_platform( ) -def _parse_due_date(data: dict, gmt_string) -> datetime | None: - """Parse the due date dict into a datetime object.""" - # Add time information to date only strings. - if len(data["date"]) == 10: - return datetime.fromisoformat(data["date"]).replace(tzinfo=dt.UTC) +def _parse_due_date(data: DueDate, timezone_offset: int) -> datetime | None: + """Parse the due date dict into a datetime object in UTC. + + This function will always return a timezone aware datetime if it can be parsed. + """ if not (nowtime := dt.parse_datetime(data["date"])): return None if nowtime.tzinfo is None: - data["date"] += gmt_string + nowtime = nowtime.replace(tzinfo=timezone(timedelta(hours=timezone_offset))) return dt.as_utc(nowtime) @@ -441,7 +442,7 @@ class TodoistProjectData: task[START] = dt.utcnow() if data[DUE] is not None: task[END] = _parse_due_date( - data[DUE], self._api.state["user"]["tz_info"]["gmt_string"] + data[DUE], self._api.state["user"]["tz_info"]["hours"] ) if self._due_date_days is not None and ( @@ -564,8 +565,9 @@ class TodoistProjectData: for task in project_task_data: if task["due"] is None: continue + # @NOTE: _parse_due_date always returns the date in UTC time. due_date = _parse_due_date( - task["due"], self._api.state["user"]["tz_info"]["gmt_string"] + task["due"], self._api.state["user"]["tz_info"]["hours"] ) if not due_date: continue diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py new file mode 100644 index 00000000000..b9409e44daa --- /dev/null +++ b/homeassistant/components/todoist/types.py @@ -0,0 +1,14 @@ +"""Types for the Todoist component.""" +from __future__ import annotations + +from typing import TypedDict + + +class DueDate(TypedDict): + """Dict representing a due date in a todoist api response.""" + + date: str + is_recurring: bool + lang: str + string: str + timezone: str | None diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe57484344..f3bdb7d7549 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1446,6 +1446,9 @@ tesla-powerwall==0.3.17 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 +# homeassistant.components.todoist +todoist-python==8.0.0 + # homeassistant.components.tolo tololib==0.1.0b3 diff --git a/tests/components/todoist/__init__.py b/tests/components/todoist/__init__.py new file mode 100644 index 00000000000..e3b605f8f14 --- /dev/null +++ b/tests/components/todoist/__init__.py @@ -0,0 +1 @@ +"""Tests for the Todoist integration.""" diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py new file mode 100644 index 00000000000..2f0832fe739 --- /dev/null +++ b/tests/components/todoist/test_calendar.py @@ -0,0 +1,44 @@ +"""Unit tests for the Todoist calendar platform.""" +from datetime import datetime + +from homeassistant.components.todoist.calendar import _parse_due_date +from homeassistant.components.todoist.types import DueDate +from homeassistant.util import dt + + +def test_parse_due_date_invalid(): + """Test None is returned if the due date can't be parsed.""" + data: DueDate = { + "date": "invalid", + "is_recurring": False, + "lang": "en", + "string": "", + "timezone": None, + } + assert _parse_due_date(data, timezone_offset=-8) is None + + +def test_parse_due_date_with_no_time_data(): + """Test due date is parsed correctly when it has no time data.""" + data: DueDate = { + "date": "2022-02-02", + "is_recurring": False, + "lang": "en", + "string": "Feb 2 2:00 PM", + "timezone": None, + } + actual = _parse_due_date(data, timezone_offset=-8) + assert datetime(2022, 2, 2, 8, 0, 0, tzinfo=dt.UTC) == actual + + +def test_parse_due_date_without_timezone_uses_offset(): + """Test due date uses user local timezone offset when it has no timezone.""" + data: DueDate = { + "date": "2022-02-02T14:00:00", + "is_recurring": False, + "lang": "en", + "string": "Feb 2 2:00 PM", + "timezone": None, + } + actual = _parse_due_date(data, timezone_offset=-8) + assert datetime(2022, 2, 2, 22, 0, 0, tzinfo=dt.UTC) == actual From 1793c29facfcc3956347c2170515731e336d951b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 07:46:34 +0100 Subject: [PATCH 0301/1054] Move key sequences to bridge in SamsungTV (#67762) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 40 ++++++++++++------- .../components/samsungtv/media_player.py | 36 ++++++++--------- .../components/samsungtv/test_media_player.py | 34 ++++++---------- 3 files changed, 56 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index a63ca0f3355..17d6f1554c4 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -45,6 +45,8 @@ from .const import ( WEBSOCKET_PORTS, ) +KEY_PRESS_TIMEOUT = 1.2 + def mac_from_device_info(info: dict[str, Any]) -> str | None: """Extract the mac address from the device info.""" @@ -129,8 +131,8 @@ class SamsungTVBridge(ABC): """Tells if the TV is on.""" @abstractmethod - async def async_send_key(self, key: str) -> None: - """Send a key to the tv and handles exceptions.""" + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys to the tv.""" @abstractmethod async def async_close_remote(self) -> None: @@ -238,12 +240,18 @@ class SamsungTVLegacyBridge(SamsungTVBridge): pass return self._remote - async def async_send_key(self, key: str) -> None: - """Send the key using legacy protocol.""" - await self.hass.async_add_executor_job(self._send_key, key) + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys using legacy protocol.""" + first_key = True + for key in keys: + if first_key: + first_key = False + else: + await asyncio.sleep(KEY_PRESS_TIMEOUT) + await self.hass.async_add_executor_job(self._send_key, key) def _send_key(self, key: str) -> None: - """Send the key using legacy protocol.""" + """Send a key using legacy protocol.""" try: # recreate connection if connection was dead retry_count = 1 @@ -391,15 +399,18 @@ class SamsungTVWSBridge(SamsungTVBridge): async def async_launch_app(self, app_id: str) -> None: """Send the launch_app command using websocket protocol.""" - await self._async_send_command(ChannelEmitCommand.launch_app(app_id)) + await self._async_send_commands([ChannelEmitCommand.launch_app(app_id)]) - async def async_send_key(self, key: str) -> None: - """Send the key using websocket protocol.""" - if key == "KEY_POWEROFF": - key = "KEY_POWER" - await self._async_send_command(SendRemoteKey.click(key)) + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys using websocket protocol.""" + commands: list[SamsungTVCommand] = [] + for key in keys: + if key == "KEY_POWEROFF": + key = "KEY_POWER" + commands.append(SendRemoteKey.click(key)) + await self._async_send_commands(commands) - async def _async_send_command(self, command: SamsungTVCommand) -> None: + async def _async_send_commands(self, commands: list[SamsungTVCommand]) -> None: """Send the commands using websocket protocol.""" try: # recreate connection if connection was dead @@ -407,7 +418,8 @@ class SamsungTVWSBridge(SamsungTVBridge): for _ in range(retry_count + 1): try: if remote := await self._async_get_remote(): - await remote.send_command(command) + for command in commands: + await remote.send_command(command) break except ( BrokenPipeError, diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 894e64dff17..1f779e7b2d4 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -1,7 +1,6 @@ """Support for interface with an Samsung TV.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta from typing import Any @@ -47,7 +46,6 @@ from .const import ( LOGGER, ) -KEY_PRESS_TIMEOUT = 1.2 SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( @@ -181,12 +179,13 @@ class SamsungTVDevice(MediaPlayerEntity): assert isinstance(self._bridge, SamsungTVWSBridge) await self._bridge.async_launch_app(app_id) - async def _async_send_key(self, key: str) -> None: + async def _async_send_keys(self, keys: list[str]) -> None: """Send a key to the tv and handles exceptions.""" - if self._power_off_in_progress() and key != "KEY_POWEROFF": - LOGGER.info("TV is powering off, not sending key: %s", key) + assert keys + if self._power_off_in_progress() and keys[0] != "KEY_POWEROFF": + LOGGER.info("TV is powering off, not sending keys: %s", keys) return - await self._bridge.async_send_key(key) + await self._bridge.async_send_keys(keys) def _power_off_in_progress(self) -> bool: return ( @@ -210,21 +209,21 @@ class SamsungTVDevice(MediaPlayerEntity): """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME - await self._async_send_key("KEY_POWEROFF") + await self._async_send_keys(["KEY_POWEROFF"]) # Force closing of remote session to provide instant UI feedback await self._bridge.async_close_remote() async def async_volume_up(self) -> None: """Volume up the media player.""" - await self._async_send_key("KEY_VOLUP") + await self._async_send_keys(["KEY_VOLUP"]) async def async_volume_down(self) -> None: """Volume down media player.""" - await self._async_send_key("KEY_VOLDOWN") + await self._async_send_keys(["KEY_VOLDOWN"]) async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" - await self._async_send_key("KEY_MUTE") + await self._async_send_keys(["KEY_MUTE"]) async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" @@ -236,20 +235,20 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_media_play(self) -> None: """Send play command.""" self._playing = True - await self._async_send_key("KEY_PLAY") + await self._async_send_keys(["KEY_PLAY"]) async def async_media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False - await self._async_send_key("KEY_PAUSE") + await self._async_send_keys(["KEY_PAUSE"]) async def async_media_next_track(self) -> None: """Send next track command.""" - await self._async_send_key("KEY_CHUP") + await self._async_send_keys(["KEY_CHUP"]) async def async_media_previous_track(self) -> None: """Send the previous track command.""" - await self._async_send_key("KEY_CHDOWN") + await self._async_send_keys(["KEY_CHDOWN"]) async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any @@ -270,10 +269,9 @@ class SamsungTVDevice(MediaPlayerEntity): LOGGER.error("Media ID must be positive integer") return - for digit in media_id: - await self._async_send_key(f"KEY_{digit}") - await asyncio.sleep(KEY_PRESS_TIMEOUT) - await self._async_send_key("KEY_ENTER") + await self._async_send_keys( + keys=[f"KEY_{digit}" for digit in media_id] + ["KEY_ENTER"] + ) def _wake_on_lan(self) -> None: """Wake the device via wake on lan.""" @@ -296,7 +294,7 @@ class SamsungTVDevice(MediaPlayerEntity): return if source in SOURCES: - await self._async_send_key(SOURCES[source]) + await self._async_send_keys([SOURCES[source]]) return LOGGER.error("Unsupported source") diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index d07ec9404d6..71d114c9b5b 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,5 +1,4 @@ """Tests for samsungtv component.""" -import asyncio from copy import deepcopy from datetime import datetime, timedelta import logging @@ -650,7 +649,7 @@ async def test_turn_off_websocket( assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) - assert "TV is powering off, not sending key: KEY_VOLUP" in caplog.text + assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text assert await hass.services.async_call( DOMAIN, SERVICE_SELECT_SOURCE, @@ -853,15 +852,8 @@ async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: """Test for play_media.""" - asyncio_sleep = asyncio.sleep - sleeps = [] - - async def sleep(duration): - sleeps.append(duration) - await asyncio_sleep(0) - await setup_samsungtv(hass, MOCK_CONFIG) - with patch("asyncio.sleep", new=sleep): + with patch("homeassistant.components.samsungtv.bridge.asyncio.sleep") as sleep: assert await hass.services.async_call( DOMAIN, SERVICE_PLAY_MEDIA, @@ -872,17 +864,17 @@ async def test_play_media(hass: HomeAssistant, remote: Mock) -> None: }, True, ) - # keys and update called - assert remote.control.call_count == 4 - assert remote.control.call_args_list == [ - call("KEY_5"), - call("KEY_7"), - call("KEY_6"), - call("KEY_ENTER"), - ] - assert remote.close.call_count == 1 - assert remote.close.call_args_list == [call()] - assert len(sleeps) == 3 + # keys and update called + assert remote.control.call_count == 4 + assert remote.control.call_args_list == [ + call("KEY_5"), + call("KEY_7"), + call("KEY_6"), + call("KEY_ENTER"), + ] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] + assert sleep.call_count == 3 async def test_play_media_invalid_type(hass: HomeAssistant) -> None: From 8260767e8fcf54ac19a712a2b79d5e51c4cd0985 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 07:51:23 +0100 Subject: [PATCH 0302/1054] Enable basic type checking in upnp (#66253) Co-authored-by: epenet --- homeassistant/components/upnp/__init__.py | 2 ++ .../components/upnp/binary_sensor.py | 1 + homeassistant/components/upnp/config_flow.py | 21 +++++++++--------- homeassistant/components/upnp/device.py | 22 +++++++++++-------- homeassistant/components/upnp/sensor.py | 4 ++-- mypy.ini | 15 ------------- script/hassfest/mypy_config.py | 5 ----- 7 files changed, 29 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index a3490ad8037..c7988fe98c9 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -121,6 +121,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_discovered_callback() # Create device. + assert discovery_info is not None + assert discovery_info.ssdp_location is not None location = discovery_info.ssdp_location try: device = await Device.async_create_device(hass, location) diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index d848aff5a88..d49bbddf996 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -43,6 +43,7 @@ async def async_setup_entry( class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): """Class for UPnP/IGD binary sensors.""" + entity_description: UpnpBinarySensorEntityDescription _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY def __init__( diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 1b8507d25bf..e339c69d5d8 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping from datetime import timedelta -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -31,16 +31,17 @@ from .const import ( def _friendly_name_from_discovery(discovery_info: ssdp.SsdpServiceInfo) -> str: """Extract user-friendly name from discovery.""" - return ( + return cast( + str, discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) - or discovery_info.ssdp_headers.get("_host", "") + or discovery_info.ssdp_headers.get("_host", ""), ) def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: """Test if discovery is complete and usable.""" - return ( + return bool( ssdp.ATTR_UPNP_UDN in discovery_info.upnp and discovery_info.ssdp_st and discovery_info.ssdp_location @@ -114,14 +115,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the UPnP/IGD config flow.""" self._discoveries: list[SsdpServiceInfo] | None = None - async def async_step_user( - self, user_input: Mapping | None = None - ) -> Mapping[str, Any]: + async def async_step_user(self, user_input: Mapping | None = None) -> FlowResult: """Handle a flow start.""" LOGGER.debug("async_step_user: user_input: %s", user_input) if user_input is not None: # Ensure wanted device was discovered. + assert self._discoveries matching_discoveries = [ discovery for discovery in self._discoveries @@ -248,12 +248,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp_confirm( self, user_input: Mapping | None = None - ) -> Mapping[str, Any]: + ) -> FlowResult: """Confirm integration via SSDP.""" LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) if user_input is None: return self.async_show_form(step_id="ssdp_confirm") + assert self._discoveries discovery = self._discoveries[0] return await self._async_create_entry_from_discovery(discovery) @@ -268,7 +269,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_discovery( self, discovery: SsdpServiceInfo, - ) -> Mapping[str, Any]: + ) -> FlowResult: """Create an entry from discovery.""" LOGGER.debug( "_async_create_entry_from_discovery: discovery: %s", @@ -291,7 +292,7 @@ class UpnpOptionsFlowHandler(config_entries.OptionsFlow): """Initialize.""" self.config_entry = config_entry - async def async_step_init(self, user_input: Mapping = None) -> None: + async def async_step_init(self, user_input: Mapping = None) -> FlowResult: """Manage the options.""" if user_input is not None: coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index be0ae725396..19e7987a138 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -9,7 +9,7 @@ from urllib.parse import urlparse from async_upnp_client import UpnpDevice, UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.exceptions import UpnpError -from async_upnp_client.profiles.igd import IgdDevice +from async_upnp_client.profiles.igd import IgdDevice, StatusInfo from homeassistant.components import ssdp from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo @@ -49,7 +49,7 @@ class Device: """Initialize UPnP/IGD device.""" self.hass = hass self._igd_device = igd_device - self.coordinator: DataUpdateCoordinator = None + self.coordinator: DataUpdateCoordinator | None = None @classmethod async def async_create_device( @@ -129,7 +129,7 @@ class Device: return self.usn @property - def hostname(self) -> str: + def hostname(self) -> str | None: """Get the hostname.""" url = self._igd_device.device.device_url parsed = urlparse(url) @@ -177,7 +177,9 @@ class Device: self._igd_device.async_get_external_ip_address(), return_exceptions=True, ) - result = [] + status_info: StatusInfo | None = None + ip_address: str | None = None + for idx, value in enumerate(values): if isinstance(value, UpnpError): # Not all routers support some of these items although based @@ -188,16 +190,18 @@ class Device: self, str(value), ) - result.append(None) continue if isinstance(value, Exception): raise value - result.append(value) + if isinstance(value, StatusInfo): + status_info = value + elif isinstance(value, str): + ip_address = value return { - WAN_STATUS: result[0][0] if result[0] is not None else None, - ROUTER_UPTIME: result[0][2] if result[0] is not None else None, - ROUTER_IP: result[1], + WAN_STATUS: status_info[0] if status_info is not None else None, + ROUTER_UPTIME: status_info[2] if status_info is not None else None, + ROUTER_IP: ip_address, } diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 595a14a5a9f..908a5b53940 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -143,6 +143,8 @@ async def async_setup_entry( class UpnpSensor(UpnpEntity, SensorEntity): """Base class for UPnP/IGD sensors.""" + entity_description: UpnpSensorEntityDescription + class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @@ -159,8 +161,6 @@ class RawUpnpSensor(UpnpSensor): class DerivedUpnpSensor(UpnpSensor): """Representation of a UNIT Sent/Received per second sensor.""" - entity_description: UpnpSensorEntityDescription - def __init__( self, coordinator: UpnpDataUpdateCoordinator, diff --git a/mypy.ini b/mypy.ini index d674160c8be..6316ce32abf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2725,21 +2725,6 @@ ignore_errors = true [mypy-homeassistant.components.unifi.unifi_entity_base] ignore_errors = true -[mypy-homeassistant.components.upnp] -ignore_errors = true - -[mypy-homeassistant.components.upnp.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.upnp.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.upnp.device] -ignore_errors = true - -[mypy-homeassistant.components.upnp.sensor] -ignore_errors = true - [mypy-homeassistant.components.vizio.config_flow] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index dc8dc2188d3..5df83f1e527 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -150,11 +150,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.unifi.device_tracker", "homeassistant.components.unifi.diagnostics", "homeassistant.components.unifi.unifi_entity_base", - "homeassistant.components.upnp", - "homeassistant.components.upnp.binary_sensor", - "homeassistant.components.upnp.config_flow", - "homeassistant.components.upnp.device", - "homeassistant.components.upnp.sensor", "homeassistant.components.vizio.config_flow", "homeassistant.components.vizio.media_player", "homeassistant.components.withings", From 2d4ccddd99bcc4bd314f813214be693788b1c0bf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 07:53:59 +0100 Subject: [PATCH 0303/1054] Fix SSDP unique id in SamsungTV config flow (#67811) Co-authored-by: epenet Co-authored-by: J. Nick Koston --- .../components/samsungtv/config_flow.py | 24 +++++- .../components/samsungtv/test_config_flow.py | 78 +++++++++++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 8b7e0f83a49..8c990dece77 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -71,6 +71,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host: str = "" self._mac: str | None = None self._udn: str | None = None + self._upnp_udn: str | None = None self._manufacturer: str | None = None self._model: str | None = None self._name: str | None = None @@ -220,10 +221,19 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Returns the existing entry if it was updated. """ for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] != self._host: + mac = entry.data.get(CONF_MAC) + mac_match = mac and self._mac and mac == self._mac + upnp_udn_match = self._upnp_udn and self._upnp_udn == entry.unique_id + if ( + entry.data[CONF_HOST] != self._host + and not mac_match + and not upnp_udn_match + ): continue entry_kw_args: dict = {} - if self.unique_id and entry.unique_id is None: + if (self._udn and self._upnp_udn and self._upnp_udn != self._udn) or ( + self.unique_id and entry.unique_id is None + ): entry_kw_args["unique_id"] = self.unique_id if self._mac and not entry.data.get(CONF_MAC): entry_kw_args["data"] = {**entry.data, CONF_MAC: self._mac} @@ -264,16 +274,22 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" - self._udn = _strip_uuid(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) + self._udn = self._upnp_udn = _strip_uuid( + discovery_info.upnp[ssdp.ATTR_UPNP_UDN] + ) if hostname := urlparse(discovery_info.ssdp_location or "").hostname: self._host = hostname - await self._async_set_unique_id_from_udn() self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] self._abort_if_manufacturer_is_not_samsung() if not await self._async_get_and_check_device_info(): # If we cannot get device info for an SSDP discovery # its likely a legacy tv. self._name = self._title = self._model = model_name + + # The UDN provided by the ssdp discovery doesn't always match the UDN + # from the device_info, used by the the other methods so we need to + # ensure the device_info is loaded before setting the unique_id + await self._async_set_unique_id_from_udn() self._async_update_and_abort_for_matching_unique_id() self._async_abort_if_host_already_in_progress() self.context["title_placeholders"] = {"device": self._title} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 98eef3d7810..b6f23f83912 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -339,7 +339,7 @@ async def test_ssdp(hass: HomeAssistant) -> None: assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert result["result"].unique_id == "123" @pytest.mark.usefixtures("remote") @@ -373,7 +373,7 @@ async def test_ssdp_noprefix(hass: HomeAssistant) -> None: assert result["data"][CONF_NAME] == "fake2_name" assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" assert result["data"][CONF_MODEL] == "fake2_model" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" + assert result["result"].unique_id == "345" @pytest.mark.usefixtures("remotews") @@ -448,7 +448,7 @@ async def test_ssdp_websocket_success_populates_mac_address( assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "82GXARRS" - assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" async def test_ssdp_websocket_not_supported( @@ -591,7 +591,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: assert result2["reason"] == RESULT_ALREADY_CONFIGURED # check updated device info - assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert entry.unique_id == "123" @pytest.mark.usefixtures("remote") @@ -1013,7 +1013,7 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: # check updated device info assert entry2.data.get(CONF_ID) is not None assert entry2.data.get(CONF_IP_ADDRESS) is not None - assert entry2.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @pytest.mark.usefixtures("remotews") @@ -1099,7 +1099,7 @@ async def test_update_missing_mac_unique_id_added_from_ssdp( assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" - assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" @pytest.mark.usefixtures("remotews") @@ -1308,3 +1308,69 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: assert result2["type"] == "abort" assert result2["reason"] == "not_supported" + + +@pytest.mark.usefixtures("remotews") +async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test updating the wrong udn from ssdp via upnp udn match.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_OLD_ENTRY, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews") +async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test updating the wrong udn from ssdp via mac match.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" From 5b23b9a61786d27071516137ac0bfba8f1b636fc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 07:54:12 +0100 Subject: [PATCH 0304/1054] Adjust get_mac routine in SamsungTV (#67804) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 5 +-- tests/components/samsungtv/const.py | 40 +++++++++++++++++++ .../components/samsungtv/test_config_flow.py | 33 ++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 17d6f1554c4..b32ee28674d 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -50,9 +50,8 @@ KEY_PRESS_TIMEOUT = 1.2 def mac_from_device_info(info: dict[str, Any]) -> str | None: """Extract the mac address from the device info.""" - dev_info = info.get("device", {}) - if dev_info.get("networkType") == "wireless" and dev_info.get("wifiMac"): - return format_mac(dev_info["wifiMac"]) + if wifi_mac := info.get("device", {}).get("wifiMac"): + return format_mac(wifi_mac) return None diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index a3bbcb2dd7f..7f9f175c171 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -33,3 +33,43 @@ SAMPLE_DEVICE_INFO_WIFI = { "networkType": "wireless", }, } + +SAMPLE_DEVICE_INFO_FRAME = { + "device": { + "FrameTVSupport": "true", + "GamePadSupport": "true", + "ImeSyncedSupport": "true", + "OS": "Tizen", + "TokenAuthSupport": "true", + "VoiceSupport": "true", + "countryCode": "FR", + "description": "Samsung DTV RCR", + "developerIP": "0.0.0.0", + "developerMode": "0", + "duid": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "firmwareVersion": "Unknown", + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "ip": "1.2.3.4", + "model": "17_KANTM_UHD", + "modelName": "UE43LS003", + "name": "[TV] Samsung Frame (43)", + "networkType": "wired", + "resolution": "3840x2160", + "smartHubAgreement": "true", + "type": "Samsung SmartTV", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "wifiMac": "aa:ee:tt:hh:ee:rr", + }, + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "isSupport": ( + '{"DMP_DRM_PLAYREADY":"false","DMP_DRM_WIDEVINE":"false","DMP_available":"true",' + '"EDEN_available":"true","FrameTVSupport":"true","ImeSyncedSupport":"true",' + '"TokenAuthSupport":"true","remote_available":"true","remote_fourDirections":"true",' + '"remote_touchPad":"true","remote_voiceControl":"true"}\n' + ), + "name": "[TV] Samsung Frame (43)", + "remote": "1.0", + "type": "Samsung SmartTV", + "uri": "https://1.2.3.4:8002/api/v2/", + "version": "2.0.25", +} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index b6f23f83912..a3565f4d884 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -44,7 +44,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import SAMPLE_APP_LIST +from .const import SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_FRAME from tests.common import MockConfigEntry @@ -697,7 +697,7 @@ async def test_import_unknown_host(hass: HomeAssistant): @pytest.mark.usefixtures("remote", "remotews") -async def test_dhcp(hass: HomeAssistant) -> None: +async def test_dhcp_wireless(hass: HomeAssistant) -> None: """Test starting a flow from dhcp.""" # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -723,6 +723,35 @@ async def test_dhcp(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +@pytest.mark.usefixtures("remote", "remotews") +async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: + """Test starting a flow from dhcp.""" + # Even though it is named "wifiMac", it matches the mac of the wired connection + rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_FRAME + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Samsung Frame (43) (UE43LS003)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Samsung Frame (43)" + assert result["data"][CONF_MAC] == "aa:ee:tt:hh:ee:rr" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "UE43LS003" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + @pytest.mark.usefixtures("remote", "remotews") async def test_zeroconf(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf.""" From 6f38eda1148f69b7998910f999a0398bff58546e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 08:20:58 +0100 Subject: [PATCH 0305/1054] Adjust power_off for TheFrame in SamsungTV (#67841) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 26 ++++++++++---- .../components/samsungtv/media_player.py | 5 +-- .../components/samsungtv/test_media_player.py | 34 ++++++++++++++++++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index b32ee28674d..02a36915dc6 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -133,6 +133,10 @@ class SamsungTVBridge(ABC): async def async_send_keys(self, keys: list[str]) -> None: """Send a list of keys to the tv.""" + @abstractmethod + async def async_power_off(self) -> None: + """Send power off command to remote.""" + @abstractmethod async def async_close_remote(self) -> None: """Close remote object.""" @@ -269,6 +273,12 @@ class SamsungTVLegacyBridge(SamsungTVBridge): # Different reasons, e.g. hostname not resolveable pass + async def async_power_off(self) -> None: + """Send power off command to remote.""" + await self.async_send_keys(["KEY_POWEROFF"]) + # Force closing of remote session to provide instant UI feedback + await self.async_close_remote() + async def async_close_remote(self) -> None: """Close remote object.""" await self.hass.async_add_executor_job(self._close_remote) @@ -402,12 +412,7 @@ class SamsungTVWSBridge(SamsungTVBridge): async def async_send_keys(self, keys: list[str]) -> None: """Send a list of keys using websocket protocol.""" - commands: list[SamsungTVCommand] = [] - for key in keys: - if key == "KEY_POWEROFF": - key = "KEY_POWER" - commands.append(SendRemoteKey.click(key)) - await self._async_send_commands(commands) + await self._async_send_commands([SendRemoteKey.click(key) for key in keys]) async def _async_send_commands(self, commands: list[SamsungTVCommand]) -> None: """Send the commands using websocket protocol.""" @@ -485,6 +490,15 @@ class SamsungTVWSBridge(SamsungTVBridge): self._notify_new_token_callback() return self._remote + async def async_power_off(self) -> None: + """Send power off command to remote.""" + if self._get_device_spec("FrameTVSupport") == "true": + await self._async_send_commands(SendRemoteKey.hold("KEY_POWER", 3)) + else: + await self._async_send_commands([SendRemoteKey.click("KEY_POWER")]) + # Force closing of remote session to provide instant UI feedback + await self.async_close_remote() + async def async_close_remote(self) -> None: """Close remote object.""" try: diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 1f779e7b2d4..a154399725f 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -208,10 +208,7 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_turn_off(self) -> None: """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME - - await self._async_send_keys(["KEY_POWEROFF"]) - # Force closing of remote session to provide instant UI feedback - await self._bridge.async_close_remote() + await self._bridge.async_power_off() async def async_volume_up(self) -> None: """Volume up the media player.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 71d114c9b5b..bc693918b5f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -7,6 +7,7 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch import pytest from samsungctl import exceptions from samsungtvws.async_remote import SamsungTVWSAsyncRemote +from samsungtvws.command import SamsungTVSleepCommand from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import WebSocketException @@ -63,7 +64,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .const import SAMPLE_APP_LIST +from .const import SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_FRAME from tests.common import MockConfigEntry, async_fire_time_changed @@ -660,6 +661,37 @@ async def test_turn_off_websocket( remotews.send_command.assert_not_called() +async def test_turn_off_websocket_frame( + hass: HomeAssistant, remotews: Mock, rest_api: Mock +) -> None: + """Test for turn_off.""" + rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_FRAME + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=[OSError("Boom"), DEFAULT_MOCK], + ): + await setup_samsungtv(hass, MOCK_CONFIGWS) + + remotews.send_command.reset_mock() + + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key called + assert remotews.send_command.call_count == 3 + command = remotews.send_command.call_args_list[0].args[0] + assert isinstance(command, SendRemoteKey) + assert command.params["Cmd"] == "Press" + assert command.params["DataOfCmd"] == "KEY_POWER" + command = remotews.send_command.call_args_list[1].args[0] + assert isinstance(command, SamsungTVSleepCommand) + assert command.delay == 3 + command = remotews.send_command.call_args_list[2].args[0] + assert isinstance(command, SendRemoteKey) + assert command.params["Cmd"] == "Release" + assert command.params["DataOfCmd"] == "KEY_POWER" + + async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: """Test for turn_off.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) From e1b57d83c7203fb4a1bdcc703fa4927559a33b9d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 08:30:11 +0100 Subject: [PATCH 0306/1054] Adjust config entry matching in SamsungTV (#67842) Co-authored-by: epenet --- .../components/samsungtv/config_flow.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 8c990dece77..13697c39633 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -113,9 +113,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set the unique id from the udn.""" assert self._host is not None await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) - if (entry := self._async_update_existing_host_entry()) and _entry_is_complete( - entry - ): + if ( + entry := self._async_update_existing_matching_entry() + ) and _entry_is_complete(entry): raise data_entry_flow.AbortFlow("already_configured") def _async_update_and_abort_for_matching_unique_id(self) -> None: @@ -215,21 +215,25 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) @callback - def _async_update_existing_host_entry(self) -> config_entries.ConfigEntry | None: - """Check existing entries and update them. - - Returns the existing entry if it was updated. - """ + def _async_get_existing_matching_entry(self) -> config_entries.ConfigEntry | None: + """Get first existing matching entry.""" for entry in self._async_current_entries(include_ignore=False): mac = entry.data.get(CONF_MAC) mac_match = mac and self._mac and mac == self._mac upnp_udn_match = self._upnp_udn and self._upnp_udn == entry.unique_id - if ( - entry.data[CONF_HOST] != self._host - and not mac_match - and not upnp_udn_match - ): - continue + if entry.data[CONF_HOST] == self._host or mac_match or upnp_udn_match: + return entry + return None + + @callback + def _async_update_existing_matching_entry( + self, + ) -> config_entries.ConfigEntry | None: + """Check existing entries and update them. + + Returns the existing entry if it was updated. + """ + if entry := self._async_get_existing_matching_entry(): entry_kw_args: dict = {} if (self._udn and self._upnp_udn and self._upnp_udn != self._udn) or ( self.unique_id and entry.unique_id is None @@ -248,7 +252,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_start_discovery_with_mac_address(self) -> None: """Start discovery.""" assert self._host is not None - if (entry := self._async_update_existing_host_entry()) and entry.unique_id: + if (entry := self._async_update_existing_matching_entry()) and entry.unique_id: # If we have the unique id and the mac we abort # as we do not need anything else raise data_entry_flow.AbortFlow("already_configured") From 56abd5f2cffe34bea9c8577b6db1f3712db4b3b3 Mon Sep 17 00:00:00 2001 From: Lester Lo <21245380+lesterlo@users.noreply.github.com> Date: Tue, 8 Mar 2022 15:37:20 +0800 Subject: [PATCH 0307/1054] Add homekit pm type sensor (#46060) Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 10 ++ homeassistant/components/homekit/const.py | 3 +- .../components/homekit/type_sensors.py | 76 ++++++++++++++- homeassistant/components/homekit/util.py | 26 +++++ .../homekit/test_get_accessories.py | 16 +++- tests/components/homekit/test_type_sensors.py | 96 +++++++++++++++++++ 6 files changed, 220 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4129c3225b7..c77fa96a532 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -173,9 +173,19 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 a_type = "TemperatureSensor" elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE: a_type = "HumiditySensor" + elif ( + device_class == SensorDeviceClass.PM10 + or SensorDeviceClass.PM10 in state.entity_id + ): + a_type = "PM10Sensor" elif ( device_class == SensorDeviceClass.PM25 or SensorDeviceClass.PM25 in state.entity_id + ): + a_type = "PM25Sensor" + elif ( + device_class == SensorDeviceClass.GAS + or SensorDeviceClass.GAS in state.entity_id ): a_type = "AirQualitySensor" elif device_class == SensorDeviceClass.CO: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ed7b3d6b293..264801c521f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -150,6 +150,8 @@ SERV_WINDOW_COVERING = "WindowCovering" CHAR_ACTIVE = "Active" CHAR_ACTIVE_IDENTIFIER = "ActiveIdentifier" CHAR_AIR_PARTICULATE_DENSITY = "AirParticulateDensity" +CHAR_PM25_DENSITY = "PM2.5Density" +CHAR_PM10_DENSITY = "PM10Density" CHAR_AIR_QUALITY = "AirQuality" CHAR_BATTERY_LEVEL = "BatteryLevel" CHAR_BRIGHTNESS = "Brightness" @@ -235,7 +237,6 @@ PROP_MIN_VALUE = "minValue" PROP_MIN_STEP = "minStep" PROP_CELSIUS = {"minValue": -273, "maxValue": 999} PROP_VALID_VALUES = "ValidValues" - # #### Thresholds #### THRESHOLD_CO = 25 THRESHOLD_CO2 = 1000 diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 46e241efab0..594d95494f1 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -35,6 +35,8 @@ from .const import ( CHAR_LEAK_DETECTED, CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, + CHAR_PM10_DENSITY, + CHAR_PM25_DENSITY, CHAR_SMOKE_DETECTED, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, @@ -51,7 +53,13 @@ from .const import ( THRESHOLD_CO, THRESHOLD_CO2, ) -from .util import convert_to_float, density_to_air_quality, temperature_to_homekit +from .util import ( + convert_to_float, + density_to_air_quality, + density_to_air_quality_pm10, + density_to_air_quality_pm25, + temperature_to_homekit, +) _LOGGER = logging.getLogger(__name__) @@ -156,6 +164,15 @@ class AirQualitySensor(HomeAccessory): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) state = self.hass.states.get(self.entity_id) + + self.create_services() + + # Set the state so it is in sync on initial + # GET to avoid an event storm after homekit startup + self.async_update_state(state) + + def create_services(self): + """Initialize a AirQualitySensor accessory object.""" serv_air_quality = self.add_preload_service( SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY] ) @@ -163,9 +180,6 @@ class AirQualitySensor(HomeAccessory): self.char_density = serv_air_quality.configure_char( CHAR_AIR_PARTICULATE_DENSITY, value=0 ) - # Set the state so it is in sync on initial - # GET to avoid an event storm after homekit startup - self.async_update_state(state) @callback def async_update_state(self, new_state): @@ -179,6 +193,60 @@ class AirQualitySensor(HomeAccessory): _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) +@TYPES.register("PM10Sensor") +class PM10Sensor(AirQualitySensor): + """Generate a PM10Sensor accessory as PM 10 sensor.""" + + def create_services(self): + """Override the init function for PM 10 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_PM10_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_PM10_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if not density: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_pm10(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + +@TYPES.register("PM25Sensor") +class PM25Sensor(AirQualitySensor): + """Generate a PM25Sensor accessory as PM 2.5 sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_PM25_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_PM25_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if not density: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_pm25(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + @TYPES.register("CarbonMonoxideSensor") class CarbonMonoxideSensor(HomeAccessory): """Generate a CarbonMonoxidSensor accessory as CO sensor.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 7fa4ffa8bf6..b31c55db767 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -407,6 +407,32 @@ def density_to_air_quality(density): return 5 +def density_to_air_quality_pm10(density): + """Map PM10 density to HomeKit AirQuality level.""" + if density <= 40: + return 1 + if density <= 80: + return 2 + if density <= 120: + return 3 + if density <= 300: + return 4 + return 5 + + +def density_to_air_quality_pm25(density): + """Map PM2.5 density to HomeKit AirQuality level.""" + if density <= 25: + return 1 + if density <= 50: + return 2 + if density <= 100: + return 3 + if density <= 300: + return 4 + return 5 + + def get_persist_filename_for_entry_id(entry_id: str): """Determine the filename of the homekit state file.""" return f"{DOMAIN}.{entry_id}.state" diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 31f7b0f3bcc..32f4abe98f1 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -212,8 +212,20 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): ("BinarySensor", "binary_sensor.opening", "on", {ATTR_DEVICE_CLASS: "opening"}), ("BinarySensor", "device_tracker.someone", "not_home", {}), ("BinarySensor", "person.someone", "home", {}), - ("AirQualitySensor", "sensor.air_quality_pm25", "40", {}), - ("AirQualitySensor", "sensor.air_quality", "40", {ATTR_DEVICE_CLASS: "pm25"}), + ("PM10Sensor", "sensor.air_quality_pm10", "30", {}), + ( + "PM10Sensor", + "sensor.air_quality", + "30", + {ATTR_DEVICE_CLASS: "pm10"}, + ), + ("PM25Sensor", "sensor.air_quality_pm25", "40", {}), + ( + "PM25Sensor", + "sensor.air_quality", + "40", + {ATTR_DEVICE_CLASS: "pm25"}, + ), ( "CarbonMonoxideSensor", "sensor.co", diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 9b6d1c9cee2..75069ce9467 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -15,6 +15,8 @@ from homeassistant.components.homekit.type_sensors import ( CarbonMonoxideSensor, HumiditySensor, LightSensor, + PM10Sensor, + PM25Sensor, TemperatureSensor, ) from homeassistant.const import ( @@ -132,6 +134,100 @@ async def test_air_quality(hass, hk_driver): assert acc.char_quality.value == 5 +async def test_pm10(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_pm10" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = PM10Sensor(hass, hk_driver, "PM10 Sensor", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "34") + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "70") + await hass.async_block_till_done() + assert acc.char_density.value == 70 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "110") + await hass.async_block_till_done() + assert acc.char_density.value == 110 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "200") + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "400") + await hass.async_block_till_done() + assert acc.char_density.value == 400 + assert acc.char_quality.value == 5 + + +async def test_pm25(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_pm25" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = PM25Sensor(hass, hk_driver, "PM25 Sensor", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "23") + await hass.async_block_till_done() + assert acc.char_density.value == 23 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "34") + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "90") + await hass.async_block_till_done() + assert acc.char_density.value == 90 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "200") + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "400") + await hass.async_block_till_done() + assert acc.char_density.value == 400 + assert acc.char_quality.value == 5 + + async def test_co(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = "sensor.co" From 6da38d67fff31eef36af54a5f235e4b1bd375737 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 08:54:03 +0100 Subject: [PATCH 0308/1054] Simplify SSDP discovery in SamsungTV (#67843) Co-authored-by: epenet --- homeassistant/components/samsungtv/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 13697c39633..a5d932cc32c 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -285,10 +285,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = hostname self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] self._abort_if_manufacturer_is_not_samsung() - if not await self._async_get_and_check_device_info(): - # If we cannot get device info for an SSDP discovery - # its likely a legacy tv. - self._name = self._title = self._model = model_name + + # Set defaults, in case they cannot be extracted from device_info + self._name = self._title = self._model = model_name + # Update from device_info (if accessible) + await self._async_get_and_check_device_info() # The UDN provided by the ssdp discovery doesn't always match the UDN # from the device_info, used by the the other methods so we need to From e199be6391c62234e6b09fa541f4d7375e8bb1a4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 8 Mar 2022 03:44:39 -0500 Subject: [PATCH 0309/1054] Bump zwave-js-server-python to 0.35.2 (#67839) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 6a892b2791d..8d1a9091bc0 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.35.1"], + "requirements": ["zwave-js-server-python==0.35.2"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9749c5876da..71087ec9565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ zigpy==0.43.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.35.1 +zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me zwave_me_ws==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3bdb7d7549..db7e07175d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1593,7 +1593,7 @@ zigpy-znp==0.7.0 zigpy==0.43.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.35.1 +zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me zwave_me_ws==0.2.1 From 36049ac5146f1f80870c8b66849978c5743b1cce Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 10:10:17 +0100 Subject: [PATCH 0310/1054] Batch send commands in SamsungTV (#67847) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 3 +- .../components/samsungtv/test_media_player.py | 44 ++++++++++--------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 02a36915dc6..99e9f877413 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -422,8 +422,7 @@ class SamsungTVWSBridge(SamsungTVBridge): for _ in range(retry_count + 1): try: if remote := await self._async_get_remote(): - for command in commands: - await remote.send_command(command) + await remote.send_command(commands) break except ( BrokenPipeError, diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index bc693918b5f..e5520098a4a 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -641,9 +641,10 @@ async def test_turn_off_websocket( ) # key called assert remotews.send_command.call_count == 1 - command = remotews.send_command.call_args_list[0].args[0] - assert isinstance(command, SendRemoteKey) - assert command.params["DataOfCmd"] == "KEY_POWER" + commands = remotews.send_command.call_args_list[0].args[0] + assert len(commands) == 1 + assert isinstance(commands[0], SendRemoteKey) + assert commands[0].params["DataOfCmd"] == "KEY_POWER" # commands not sent : power off in progress remotews.send_command.reset_mock() @@ -678,18 +679,17 @@ async def test_turn_off_websocket_frame( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remotews.send_command.call_count == 3 - command = remotews.send_command.call_args_list[0].args[0] - assert isinstance(command, SendRemoteKey) - assert command.params["Cmd"] == "Press" - assert command.params["DataOfCmd"] == "KEY_POWER" - command = remotews.send_command.call_args_list[1].args[0] - assert isinstance(command, SamsungTVSleepCommand) - assert command.delay == 3 - command = remotews.send_command.call_args_list[2].args[0] - assert isinstance(command, SendRemoteKey) - assert command.params["Cmd"] == "Release" - assert command.params["DataOfCmd"] == "KEY_POWER" + assert remotews.send_command.call_count == 1 + commands = remotews.send_command.call_args_list[0].args[0] + assert len(commands) == 3 + assert isinstance(commands[0], SendRemoteKey) + assert commands[0].params["Cmd"] == "Press" + assert commands[0].params["DataOfCmd"] == "KEY_POWER" + assert isinstance(commands[1], SamsungTVSleepCommand) + assert commands[1].delay == 3 + assert isinstance(commands[2], SendRemoteKey) + assert commands[2].params["Cmd"] == "Release" + assert commands[2].params["DataOfCmd"] == "KEY_POWER" async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: @@ -1023,9 +1023,10 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: True, ) assert remotews.send_command.call_count == 1 - command = remotews.send_command.call_args_list[0].args[0] - assert isinstance(command, ChannelEmitCommand) - assert command.params["data"]["appId"] == "3201608010191" + commands = remotews.send_command.call_args_list[0].args[0] + assert len(commands) == 1 + assert isinstance(commands[0], ChannelEmitCommand) + assert commands[0].params["data"]["appId"] == "3201608010191" async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: @@ -1040,6 +1041,7 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: True, ) assert remotews.send_command.call_count == 1 - command = remotews.send_command.call_args_list[0].args[0] - assert isinstance(command, ChannelEmitCommand) - assert command.params["data"]["appId"] == "3201608010191" + commands = remotews.send_command.call_args_list[0].args[0] + assert len(commands) == 1 + assert isinstance(commands[0], ChannelEmitCommand) + assert commands[0].params["data"]["appId"] == "3201608010191" From ee38203e1d4d4875877027a7851199205c62d73d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 10:16:41 +0100 Subject: [PATCH 0311/1054] Add missing callback decorator to sun (#67840) --- homeassistant/components/sun/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index fd819e5ad33..4789490ef0d 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -101,6 +101,7 @@ class Sun(Entity): self.rising = self.phase = None self._next_change = None + @callback def update_location(_event): location, elevation = get_astral_location(self.hass) if location == self.location: From bfe94f1cc52556b76d732c069f4f6ec952946a6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Mar 2022 10:33:08 +0100 Subject: [PATCH 0312/1054] Fix profiler dumping object when repr fails (#67674) * Fix profiler dumping object when repr fails * cover --- homeassistant/components/profiler/__init__.py | 14 +++++++++++- tests/components/profiler/test_init.py | 22 ++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 99d074d95fe..0f997dd41bd 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -8,6 +8,7 @@ import sys import threading import time import traceback +from typing import Any from guppy import hpy import objgraph @@ -87,13 +88,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: persistent_notification.async_dismiss(hass, "profile_object_logging") domain_data.pop(LOG_INTERVAL_SUB)() + def _safe_repr(obj: Any) -> str: + """Get the repr of an object but keep going if there is an exception. + + We wrap repr to ensure if one object cannot be serialized, we can + still get the rest. + """ + try: + return repr(obj) + except Exception: # pylint: disable=broad-except + return f"Failed to serialize {type(obj)}" + def _dump_log_objects(call: ServiceCall) -> None: obj_type = call.data[CONF_TYPE] _LOGGER.critical( "%s objects in memory: %s", obj_type, - objgraph.by_type(obj_type), + [_safe_repr(obj) for obj in objgraph.by_type(obj_type)], ) persistent_notification.create( diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 37e48763131..4c88e6170c6 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -131,19 +131,31 @@ async def test_dump_log_object(hass, caplog): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + class DumpLogDummy: + def __init__(self, fail): + self.fail = fail + + def __repr__(self): + if self.fail: + raise Exception("failed") + return "" + + obj1 = DumpLogDummy(False) + obj2 = DumpLogDummy(True) + assert hass.services.has_service(DOMAIN, SERVICE_DUMP_LOG_OBJECTS) await hass.services.async_call( - DOMAIN, SERVICE_DUMP_LOG_OBJECTS, {CONF_TYPE: "MockConfigEntry"} + DOMAIN, SERVICE_DUMP_LOG_OBJECTS, {CONF_TYPE: "DumpLogDummy"} ) await hass.async_block_till_done() - assert "MockConfigEntry" in caplog.text + assert "" in caplog.text + assert "Failed to serialize" in caplog.text + del obj1 + del obj2 caplog.clear() - assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - async def test_log_thread_frames(hass, caplog): """Test we can log thread frames.""" From 0b7b1baf30182fd12e55ee9fe66fa3788fee71ba Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Tue, 8 Mar 2022 10:35:29 +0100 Subject: [PATCH 0313/1054] Add switch platform to bosch_shc integration (#62315) * Add support for switch platform. * Add untested files to .coveragerc. * Differ between Light Switch and Smart Plug. Bumped to boschshcpy==0.2.27 * Removed duplicated code. Fixed suggestions from code review. * Fixed pylint errors * Fix pylint issue. * Add property statement * Fixed wrong attribute access * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Move switch function to base class. Changes from code review. * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Merged camera switch into SHCSwitch class * Type hint * Removed deprecated sensor entities in switch device. Added routing switch entity. * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Icon and EntityCategory as class attributes Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .coveragerc | 1 + .../components/bosch_shc/__init__.py | 2 +- homeassistant/components/bosch_shc/sensor.py | 24 ++ homeassistant/components/bosch_shc/switch.py | 235 ++++++++++++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/bosch_shc/switch.py diff --git a/.coveragerc b/.coveragerc index 178c130ac13..ce11e4822a8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -129,6 +129,7 @@ omit = homeassistant/components/bosch_shc/cover.py homeassistant/components/bosch_shc/entity.py homeassistant/components/bosch_shc/sensor.py + homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index afcf2571c31..2b95702e44c 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -19,7 +19,7 @@ from .const import ( DOMAIN, ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bosch_shc/sensor.py b/homeassistant/components/bosch_shc/sensor.py index dc1806f0ce5..331a5ebb5f3 100644 --- a/homeassistant/components/bosch_shc/sensor.py +++ b/homeassistant/components/bosch_shc/sensor.py @@ -145,6 +145,13 @@ async def async_setup_entry( entry_id=config_entry.entry_id, ) ) + entities.append( + CommunicationQualitySensor( + device=sensor, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) if entities: async_add_entities(entities) @@ -241,6 +248,23 @@ class TemperatureRatingSensor(SHCEntity, SensorEntity): return self._device.temperature_rating.name +class CommunicationQualitySensor(SHCEntity, SensorEntity): + """Representation of an SHC communication quality reporting sensor.""" + + _attr_icon = "mdi:wifi" + + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC communication quality reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Communication Quality" + self._attr_unique_id = f"{device.serial}_communication_quality" + + @property + def native_value(self): + """Return the state of the sensor.""" + return self._device.communicationquality.name + + class HumidityRatingSensor(SHCEntity, SensorEntity): """Representation of an SHC humidity rating sensor.""" diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py new file mode 100644 index 00000000000..666eb6554d9 --- /dev/null +++ b/homeassistant/components/bosch_shc/switch.py @@ -0,0 +1,235 @@ +"""Platform for switch integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from boschshcpy import ( + SHCCamera360, + SHCCameraEyes, + SHCLightSwitch, + SHCSession, + SHCSmartPlug, + SHCSmartPlugCompact, +) +from boschshcpy.device import SHCDevice + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DATA_SESSION, DOMAIN +from .entity import SHCEntity + + +@dataclass +class SHCSwitchRequiredKeysMixin: + """Mixin for SHC switch required keys.""" + + on_key: str + on_value: StateType + should_poll: bool + + +@dataclass +class SHCSwitchEntityDescription( + SwitchEntityDescription, + SHCSwitchRequiredKeysMixin, +): + """Class describing SHC switch entities.""" + + +SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = { + "smartplug": SHCSwitchEntityDescription( + key="smartplug", + device_class=SwitchDeviceClass.OUTLET, + on_key="state", + on_value=SHCSmartPlug.PowerSwitchService.State.ON, + should_poll=False, + ), + "smartplugcompact": SHCSwitchEntityDescription( + key="smartplugcompact", + device_class=SwitchDeviceClass.OUTLET, + on_key="state", + on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON, + should_poll=False, + ), + "lightswitch": SHCSwitchEntityDescription( + key="lightswitch", + device_class=SwitchDeviceClass.SWITCH, + on_key="state", + on_value=SHCLightSwitch.PowerSwitchService.State.ON, + should_poll=False, + ), + "cameraeyes": SHCSwitchEntityDescription( + key="cameraeyes", + device_class=SwitchDeviceClass.SWITCH, + on_key="cameralight", + on_value=SHCCameraEyes.CameraLightService.State.ON, + should_poll=True, + ), + "camera360": SHCSwitchEntityDescription( + key="camera360", + device_class=SwitchDeviceClass.SWITCH, + on_key="privacymode", + on_value=SHCCamera360.PrivacyModeService.State.DISABLED, + should_poll=True, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SHC switch platform.""" + entities: list[SwitchEntity] = [] + session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION] + + for switch in session.device_helper.smart_plugs: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["smartplug"], + ) + ) + entities.append( + SHCRoutingSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + ) + ) + + for switch in session.device_helper.light_switches: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["lightswitch"], + ) + ) + + for switch in session.device_helper.smart_plugs_compact: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["smartplugcompact"], + ) + ) + + for switch in session.device_helper.camera_eyes: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["cameraeyes"], + ) + ) + + for switch in session.device_helper.camera_360: + + entities.append( + SHCSwitch( + device=switch, + parent_id=session.information.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["camera360"], + ) + ) + + if entities: + async_add_entities(entities) + + +class SHCSwitch(SHCEntity, SwitchEntity): + """Representation of a SHC switch.""" + + entity_description: SHCSwitchEntityDescription + + def __init__( + self, + device: SHCDevice, + parent_id: str, + entry_id: str, + description: SHCSwitchEntityDescription, + ) -> None: + """Initialize a SHC switch.""" + super().__init__(device, parent_id, entry_id) + self.entity_description = description + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return ( + getattr(self._device, self.entity_description.on_key) + == self.entity_description.on_value + ) + + def turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + setattr(self._device, self.entity_description.on_key, True) + + def turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + setattr(self._device, self.entity_description.on_key, False) + + def toggle(self, **kwargs) -> None: + """Toggle the switch.""" + setattr(self._device, self.entity_description.on_key, not self.is_on) + + @property + def should_poll(self) -> bool: + """Switch needs polling.""" + return self.entity_description.should_poll + + def update(self) -> None: + """Trigger an update of the device.""" + self._device.update() + + +class SHCRoutingSwitch(SHCEntity, SwitchEntity): + """Representation of a SHC routing switch.""" + + _attr_icon = "mdi:wifi" + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize an SHC communication quality reporting sensor.""" + super().__init__(device, parent_id, entry_id) + self._attr_name = f"{device.name} Routing" + self._attr_unique_id = f"{device.serial}_routing" + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self._device.routing.name == "ENABLED" + + def turn_on(self, **kwargs) -> None: + """Turn the switch on.""" + self._device.routing = True + + def turn_off(self, **kwargs) -> None: + """Turn the switch off.""" + self._device.routing = False + + def toggle(self, **kwargs) -> None: + """Toggle the switch.""" + self._device.routing = not self.is_on From 2d4d18ab9029dbf6d563d71e6e7db76b48d9bbea Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 8 Mar 2022 11:58:08 +0200 Subject: [PATCH 0314/1054] Add Shelly gen2 cover support (#67705) * Add Shelly gen2 cover support * Make status property --- homeassistant/components/shelly/__init__.py | 1 + homeassistant/components/shelly/cover.py | 101 ++++++++++++++++-- homeassistant/components/shelly/entity.py | 5 + tests/components/shelly/conftest.py | 2 + tests/components/shelly/test_cover.py | 112 ++++++++++++++++++-- 5 files changed, 207 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b29079affcf..4747d044bfa 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -86,6 +86,7 @@ BLOCK_SLEEPING_PLATFORMS: Final = [ RPC_PLATFORMS: Final = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.COVER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index f9bd1b7ce85..4885a2a0d2e 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -18,15 +18,28 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BlockDeviceWrapper -from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN -from .entity import ShellyBlockEntity +from . import BlockDeviceWrapper, RpcDeviceWrapper +from .const import BLOCK, DATA_CONFIG_ENTRY, DOMAIN, RPC +from .entity import ShellyBlockEntity, ShellyRpcEntity +from .utils import get_device_entry_gen, get_rpc_key_ids async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches for device.""" + if get_device_entry_gen(config_entry) == 2: + return await async_setup_rpc_entry(hass, config_entry, async_add_entities) + + return await async_setup_block_entry(hass, config_entry, async_add_entities) + + +async def async_setup_block_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up cover for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][BLOCK] @@ -35,16 +48,32 @@ async def async_setup_entry( if not blocks: return - async_add_entities(ShellyCover(wrapper, block) for block in blocks) + async_add_entities(BlockShellyCover(wrapper, block) for block in blocks) -class ShellyCover(ShellyBlockEntity, CoverEntity): - """Switch that controls a cover block on Shelly devices.""" +async def async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][RPC] + + cover_key_ids = get_rpc_key_ids(wrapper.device.status, "cover") + + if not cover_key_ids: + return + + async_add_entities(RpcShellyCover(wrapper, id_) for id_ in cover_key_ids) + + +class BlockShellyCover(ShellyBlockEntity, CoverEntity): + """Entity that controls a cover on block based Shelly devices.""" _attr_device_class = CoverDeviceClass.SHUTTER def __init__(self, wrapper: BlockDeviceWrapper, block: Block) -> None: - """Initialize light.""" + """Initialize block cover.""" super().__init__(wrapper, block) self.control_result: dict[str, Any] | None = None self._attr_supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP @@ -110,3 +139,61 @@ class ShellyCover(ShellyBlockEntity, CoverEntity): """When device updates, clear control result that overrides state.""" self.control_result = None super()._update_callback() + + +class RpcShellyCover(ShellyRpcEntity, CoverEntity): + """Entity that controls a cover on RPC based Shelly devices.""" + + _attr_device_class = CoverDeviceClass.SHUTTER + + def __init__(self, wrapper: RpcDeviceWrapper, id_: int) -> None: + """Initialize rpc cover.""" + super().__init__(wrapper, f"cover:{id_}") + self._id = id_ + self._attr_supported_features: int = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + if self.status["pos_control"]: + self._attr_supported_features |= SUPPORT_SET_POSITION + + @property + def is_closed(self) -> bool | None: + """If cover is closed.""" + if not self.status["pos_control"]: + return None + + return cast(bool, self.status["state"] == "closed") + + @property + def current_cover_position(self) -> int | None: + """Position of the cover.""" + if not self.status["pos_control"]: + return None + + return cast(int, self.status["current_pos"]) + + @property + def is_closing(self) -> bool: + """Return if the cover is closing.""" + return cast(bool, self.status["state"] == "closing") + + @property + def is_opening(self) -> bool: + """Return if the cover is opening.""" + return cast(bool, self.status["state"] == "opening") + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + await self.call_rpc("Cover.Close", {"id": self._id}) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open cover.""" + await self.call_rpc("Cover.Open", {"id": self._id}) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self.call_rpc( + "Cover.GoToPosition", {"id": self._id, "pos": kwargs[ATTR_POSITION]} + ) + + async def async_stop_cover(self, **_kwargs: Any) -> None: + """Stop the cover.""" + await self.call_rpc("Cover.Stop", {"id": self._id}) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 51e0711b035..85abd67c069 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -351,6 +351,11 @@ class ShellyRpcEntity(entity.Entity): """Available.""" return self.wrapper.device.connected + @property + def status(self) -> dict: + """Device status by entity key.""" + return cast(dict, self.wrapper.device.status[self.key]) + async def async_added_to_hass(self) -> None: """When entity is added to HASS.""" self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 5391f3f74fe..c0e0bbadda2 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -58,6 +58,7 @@ MOCK_BLOCKS = [ MOCK_CONFIG = { "input:0": {"id": 0, "type": "button"}, "switch:0": {"name": "test switch_0"}, + "cover:0": {"name": "test cover_0"}, "sys": { "ui_data": {}, "device": {"name": "Test name"}, @@ -84,6 +85,7 @@ MOCK_STATUS_COAP = { MOCK_STATUS_RPC = { "switch:0": {"output": True}, + "cover:0": {"state": "stopped", "pos_control": True, "current_pos": 50}, "sys": { "available_updates": { "beta": {"version": "some_beta_version"}, diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index 5c34fcc1bf5..f5b25ce6cb5 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -12,13 +12,13 @@ from homeassistant.components.cover import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN ROLLER_BLOCK_ID = 1 -async def test_services(hass, coap_wrapper, monkeypatch): - """Test device turn on/off services.""" +async def test_block_device_services(hass, coap_wrapper, monkeypatch): + """Test block device cover services.""" assert coap_wrapper monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") @@ -61,8 +61,8 @@ async def test_services(hass, coap_wrapper, monkeypatch): assert hass.states.get("cover.test_name").state == STATE_CLOSED -async def test_update(hass, coap_wrapper, monkeypatch): - """Test device update.""" +async def test_block_device_update(hass, coap_wrapper, monkeypatch): + """Test block device update.""" assert coap_wrapper hass.async_create_task( @@ -81,8 +81,8 @@ async def test_update(hass, coap_wrapper, monkeypatch): assert hass.states.get("cover.test_name").state == STATE_OPEN -async def test_no_roller_blocks(hass, coap_wrapper, monkeypatch): - """Test device without roller blocks.""" +async def test_block_device_no_roller_blocks(hass, coap_wrapper, monkeypatch): + """Test block device without roller blocks.""" assert coap_wrapper monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "type", None) @@ -91,3 +91,101 @@ async def test_no_roller_blocks(hass, coap_wrapper, monkeypatch): ) await hass.async_block_till_done() assert hass.states.get("cover.test_name") is None + + +async def test_rpc_device_services(hass, rpc_wrapper, monkeypatch): + """Test RPC device cover services.""" + assert rpc_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test_cover_0", ATTR_POSITION: 50}, + blocking=True, + ) + state = hass.states.get("cover.test_cover_0") + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "opening") + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_cover_0"}, + blocking=True, + ) + rpc_wrapper.async_set_updated_data("") + assert hass.states.get("cover.test_cover_0").state == STATE_OPENING + + monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closing") + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_cover_0"}, + blocking=True, + ) + rpc_wrapper.async_set_updated_data("") + assert hass.states.get("cover.test_cover_0").state == STATE_CLOSING + + monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed") + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.test_cover_0"}, + blocking=True, + ) + rpc_wrapper.async_set_updated_data("") + assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + + +async def test_rpc_device_no_cover_keys(hass, rpc_wrapper, monkeypatch): + """Test RPC device without cover keys.""" + assert rpc_wrapper + + monkeypatch.delitem(rpc_wrapper.device.status, "cover:0") + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("cover.test_cover_0") is None + + +async def test_rpc_device_update(hass, rpc_wrapper, monkeypatch): + """Test RPC device update.""" + assert rpc_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed") + await hass.helpers.entity_component.async_update_entity("cover.test_cover_0") + await hass.async_block_till_done() + assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED + + monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "open") + await hass.helpers.entity_component.async_update_entity("cover.test_cover_0") + await hass.async_block_till_done() + assert hass.states.get("cover.test_cover_0").state == STATE_OPEN + + +async def test_rpc_device_no_position_control(hass, rpc_wrapper, monkeypatch): + """Test RPC device with no position control.""" + assert rpc_wrapper + + monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "pos_control", False) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(rpc_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + await hass.helpers.entity_component.async_update_entity("cover.test_cover_0") + await hass.async_block_till_done() + assert hass.states.get("cover.test_cover_0").state == STATE_UNKNOWN From d12118a425dd550b392556dbf46e898488a3c66c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 11:02:00 +0100 Subject: [PATCH 0315/1054] Fix reauth trigger in SamsungTV (#67850) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 17 ++++-- .../components/samsungtv/test_media_player.py | 57 +++++++++++++------ 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 99e9f877413..62c94e17f4d 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -14,7 +14,7 @@ from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.command import SamsungTVCommand from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey -from websockets.exceptions import WebSocketException +from websockets.exceptions import ConnectionClosedError, WebSocketException from homeassistant.const import ( CONF_HOST, @@ -461,15 +461,24 @@ class SamsungTVWSBridge(SamsungTVBridge): ) try: await self._remote.start_listening() - # This is only happening when the auth was switched to DENY - # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket - except ConnectionFailure as err: + except ConnectionClosedError as err: + # This is only happening when the auth was switched to DENY + # A removed auth will lead to socket timeout because waiting + # for auth popup is just an open socket LOGGER.info( "Failed to get remote for %s, re-authentication required: %s", self.host, err.__repr__(), ) self._notify_reauth_callback() + except ConnectionFailure as err: + LOGGER.warning( + "Unexpected ConnectionFailure trying to get remote for %s, " + "please report this issue: %s", + self.host, + err.__repr__(), + ) + self._remote = None except (WebSocketException, AsyncioTimeoutError, OSError) as err: LOGGER.debug( "Failed to get remote for %s: %s", self.host, err.__repr__() diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e5520098a4a..c38dd2639b1 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -10,7 +10,7 @@ from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.command import SamsungTVSleepCommand from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey -from websockets.exceptions import WebSocketException +from websockets.exceptions import ConnectionClosedError, WebSocketException from homeassistant.components.media_player import MediaPlayerDeviceClass from homeassistant.components.media_player.const import ( @@ -369,27 +369,48 @@ async def test_update_access_denied(hass: HomeAssistant, mock_now: datetime) -> assert state.state == STATE_UNAVAILABLE -async def test_update_connection_failure( +async def test_update_ws_connection_failure( + hass: HomeAssistant, + mock_now: datetime, + remotews: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Testing update tv connection failure exception.""" + await setup_samsungtv(hass, MOCK_CONFIGWS) + + with patch.object( + remotews, + "start_listening", + side_effect=ConnectionFailure('{"event": "ms.voiceApp.hide"}'), + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert ( + "Unexpected ConnectionFailure trying to get remote for fake_host, please " + 'report this issue: ConnectionFailure(\'{"event": "ms.voiceApp.hide"}\')' + in caplog.text + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_update_ws_connection_closed( hass: HomeAssistant, mock_now: datetime, remotews: Mock ) -> None: """Testing update tv connection failure exception.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), DEFAULT_MOCK], - ): - await setup_samsungtv(hass, MOCK_CONFIGWS) + await setup_samsungtv(hass, MOCK_CONFIGWS) - with patch.object( - remotews, "start_listening", side_effect=ConnectionFailure("Boom") - ), patch.object(remotews, "is_alive", return_value=False): - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - next_update = mock_now + timedelta(minutes=10) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + with patch.object( + remotews, "start_listening", side_effect=ConnectionClosedError(None, None) + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() assert [ flow From 13ac6e62e296562e73b4521222ca3a5ee0412eaf Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Tue, 8 Mar 2022 12:53:41 +0100 Subject: [PATCH 0316/1054] Remove bosch_shc switch surplus toggle method (#67851) --- homeassistant/components/bosch_shc/switch.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 666eb6554d9..aa49d873f6f 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -191,10 +191,6 @@ class SHCSwitch(SHCEntity, SwitchEntity): """Turn the switch off.""" setattr(self._device, self.entity_description.on_key, False) - def toggle(self, **kwargs) -> None: - """Toggle the switch.""" - setattr(self._device, self.entity_description.on_key, not self.is_on) - @property def should_poll(self) -> bool: """Switch needs polling.""" @@ -229,7 +225,3 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity): def turn_off(self, **kwargs) -> None: """Turn the switch off.""" self._device.routing = False - - def toggle(self, **kwargs) -> None: - """Toggle the switch.""" - self._device.routing = not self.is_on From e574a3ef1d855304b2a78c389861c421b1548d74 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Mar 2022 16:27:18 +0100 Subject: [PATCH 0317/1054] Add MQTT notify platform (#64728) * Mqtt Notify service draft * fix updates * Remove TARGET config parameter * do not use protected attributes * complete tests * device support for auto discovery * Add targets attribute and support for data param * Add tests and resolve naming issues * CONF_COMMAND_TEMPLATE from .const * Use mqtt as default service name * make sure service has a unique name * pylint error * fix type error * Conditional device removal and test * Improve tests * update description has_notify_services() * Use TypedDict for service config * casting- fix discovery - hass.data * cleanup * move MqttNotificationConfig after the schemas * fix has_notify_services * do not test log for reg update * Improve casting types * Simplify obtaining the device_id Co-authored-by: Erik Montnemery * await not needed Co-authored-by: Erik Montnemery * Improve casting types and naming * cleanup_device_registry signature change and black * remove not needed condition Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/const.py | 12 +- homeassistant/components/mqtt/discovery.py | 12 +- homeassistant/components/mqtt/mixins.py | 19 +- homeassistant/components/mqtt/notify.py | 406 ++++++++ tests/components/mqtt/test_notify.py | 863 ++++++++++++++++++ 7 files changed, 1301 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/mqtt/notify.py create mode 100644 tests/components/mqtt/test_notify.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 199b6238770..8ef62ae8bcd 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -148,6 +148,7 @@ PLATFORMS = [ Platform.HUMIDIFIER, Platform.LIGHT, Platform.LOCK, + Platform.NOTIFY, Platform.NUMBER, Platform.SELECT, Platform.SCENE, diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index ddbced5286d..587f9617124 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -185,6 +185,8 @@ ABBREVIATIONS = { "set_fan_spd_t": "set_fan_speed_topic", "set_pos_tpl": "set_position_template", "set_pos_t": "set_position_topic", + "title": "title", + "trgts": "targets", "pos_t": "position_topic", "pos_tpl": "position_template", "spd_cmd_t": "speed_command_topic", diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 69865733763..63b9d68b863 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,4 +1,6 @@ """Constants used by multiple MQTT modules.""" +from typing import Final + from homeassistant.const import CONF_PAYLOAD ATTR_DISCOVERY_HASH = "discovery_hash" @@ -12,11 +14,11 @@ ATTR_TOPIC = "topic" CONF_AVAILABILITY = "availability" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" -CONF_COMMAND_TEMPLATE = "command_template" -CONF_COMMAND_TOPIC = "command_topic" -CONF_ENCODING = "encoding" -CONF_QOS = ATTR_QOS -CONF_RETAIN = ATTR_RETAIN +CONF_COMMAND_TEMPLATE: Final = "command_template" +CONF_COMMAND_TOPIC: Final = "command_topic" +CONF_ENCODING: Final = "encoding" +CONF_QOS: Final = "qos" +CONF_RETAIN: Final = "retain" CONF_STATE_TOPIC = "state_topic" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 11bc0f6839a..05e06fec666 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -15,6 +15,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -48,6 +49,7 @@ SUPPORTED_COMPONENTS = [ "humidifier", "light", "lock", + "notify", "number", "scene", "siren", @@ -232,7 +234,15 @@ async def async_start( # noqa: C901 from . import device_automation await device_automation.async_setup_entry(hass, config_entry) - elif component == "tag": + elif component in "notify": + # Local import to avoid circular dependencies + # pylint: disable=import-outside-toplevel + from . import notify + + await notify.async_setup_entry( + hass, config_entry, AddEntitiesCallback + ) + elif component in "tag": # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import tag diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 9f3722a8f31..c87e5ccba25 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -5,7 +5,7 @@ from abc import abstractmethod from collections.abc import Callable import json import logging -from typing import Any, Protocol +from typing import Any, Protocol, cast import voluptuous as vol @@ -237,10 +237,10 @@ class SetupEntity(Protocol): async def async_setup_entry_helper(hass, domain, async_setup, schema): - """Set up entity, automation or tag creation dynamically through MQTT discovery.""" + """Set up entity, automation, notify service or tag creation dynamically through MQTT discovery.""" async def async_discover(discovery_payload): - """Discover and add an MQTT entity, automation or tag.""" + """Discover and add an MQTT entity, automation, notify service or tag.""" discovery_data = discovery_payload.discovery_data try: config = schema(discovery_payload) @@ -496,11 +496,13 @@ class MqttAvailability(Entity): return self._available_latest -async def cleanup_device_registry(hass, device_id, config_entry_id): - """Remove device registry entry if there are no remaining entities or triggers.""" +async def cleanup_device_registry( + hass: HomeAssistant, device_id: str | None, config_entry_id: str | None +) -> None: + """Remove device registry entry if there are no remaining entities, triggers or notify services.""" # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import device_trigger, tag + # pylint: disable=import-outside-toplevel + from . import device_trigger, notify, tag device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) @@ -511,9 +513,10 @@ async def cleanup_device_registry(hass, device_id, config_entry_id): ) and not await device_trigger.async_get_triggers(hass, device_id) and not tag.async_has_tags(hass, device_id) + and not notify.device_has_notify_services(hass, device_id) ): device_registry.async_update_device( - device_id, remove_config_entry_id=config_entry_id + device_id, remove_config_entry_id=cast(str, config_entry_id) ) diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py new file mode 100644 index 00000000000..9ba341aab0d --- /dev/null +++ b/homeassistant/components/mqtt/notify.py @@ -0,0 +1,406 @@ +"""Support for MQTT notify.""" +from __future__ import annotations + +import functools +import logging +from typing import Any, Final, TypedDict, cast + +import voluptuous as vol + +from homeassistant.components import notify +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import slugify + +from . import PLATFORMS, MqttCommandTemplate +from .. import mqtt +from .const import ( + ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_PAYLOAD, + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + DOMAIN, +) +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .mixins import ( + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + async_setup_entry_helper, + cleanup_device_registry, + device_info_from_config, +) + +CONF_TARGETS: Final = "targets" +CONF_TITLE: Final = "title" +CONF_CONFIG_ENTRY: Final = "config_entry" +CONF_DISCOVER_HASH: Final = "discovery_hash" + +MQTT_NOTIFY_SERVICES_SETUP = "mqtt_notify_services_setup" + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_TARGETS, default=[]): cv.ensure_list, + vol.Optional(CONF_TITLE, default=notify.ATTR_TITLE_DEFAULT): cv.string, + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + } +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, + }, + extra=vol.REMOVE_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +class MqttNotificationConfig(TypedDict, total=False): + """Supply service parameters for MqttNotificationService.""" + + command_topic: str + command_template: Template + encoding: str + name: str | None + qos: int + retain: bool + targets: list + title: str + device: ConfigType + + +async def async_initialize(hass: HomeAssistant) -> None: + """Initialize globals.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + hass.data.setdefault(MQTT_NOTIFY_SERVICES_SETUP, {}) + + +def device_has_notify_services(hass: HomeAssistant, device_id: str) -> bool: + """Check if the device has registered notify services.""" + if MQTT_NOTIFY_SERVICES_SETUP not in hass.data: + return False + for key, service in hass.data[ # pylint: disable=unused-variable + MQTT_NOTIFY_SERVICES_SETUP + ].items(): + if service.device_id == device_id: + return True + return False + + +def _check_notify_service_name( + hass: HomeAssistant, config: MqttNotificationConfig +) -> str | None: + """Check if the service already exists or else return the service name.""" + service_name = slugify(config[CONF_NAME]) + has_services = hass.services.has_service(notify.DOMAIN, service_name) + services = hass.data[MQTT_NOTIFY_SERVICES_SETUP] + if service_name in services.keys() or has_services: + _LOGGER.error( + "Notify service '%s' already exists, cannot register service", + service_name, + ) + return None + return service_name + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT notify service dynamically through MQTT discovery.""" + await async_initialize(hass) + setup = functools.partial(_async_setup_notify, hass, config_entry=config_entry) + await async_setup_entry_helper(hass, notify.DOMAIN, setup, DISCOVERY_SCHEMA) + + +async def _async_setup_notify( + hass, + legacy_config: ConfigType, + config_entry: ConfigEntry, + discovery_data: dict[str, Any], +): + """Set up the MQTT notify service with auto discovery.""" + config: MqttNotificationConfig = DISCOVERY_SCHEMA( + discovery_data[ATTR_DISCOVERY_PAYLOAD] + ) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + + if not (service_name := _check_notify_service_name(hass, config)): + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) + clear_discovery_hash(hass, discovery_hash) + return + + device_id = _update_device(hass, config_entry, config) + + service = MqttNotificationService( + hass, + config, + config_entry, + device_id, + discovery_hash, + ) + hass.data[MQTT_NOTIFY_SERVICES_SETUP][service_name] = service + + await service.async_setup(hass, service_name, service_name) + await service.async_register_services() + + +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> MqttNotificationService | None: + """Prepare the MQTT notification service through configuration.yaml.""" + await async_initialize(hass) + notification_config: MqttNotificationConfig = cast(MqttNotificationConfig, config) + + if not (service_name := _check_notify_service_name(hass, notification_config)): + return None + + service = hass.data[MQTT_NOTIFY_SERVICES_SETUP][ + service_name + ] = MqttNotificationService( + hass, + notification_config, + ) + return service + + +class MqttNotificationServiceUpdater: + """Add support for auto discovery updates.""" + + def __init__(self, hass: HomeAssistant, service: MqttNotificationService) -> None: + """Initialize the update service.""" + + async def async_discovery_update( + discovery_payload: DiscoveryInfoType | None, + ) -> None: + """Handle discovery update.""" + if not discovery_payload: + # unregister notify service through auto discovery + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None + ) + await async_tear_down_service() + return + + # update notify service through auto discovery + await service.async_update_service(discovery_payload) + _LOGGER.debug( + "Notify service %s updated has been processed", + service.discovery_hash, + ) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None + ) + + async def async_device_removed(event): + """Handle the removal of a device.""" + device_id = event.data["device_id"] + if ( + event.data["action"] != "remove" + or device_id != service.device_id + or self._device_removed + ): + return + self._device_removed = True + await async_tear_down_service() + + async def async_tear_down_service(): + """Handle the removal of the service.""" + services = hass.data[MQTT_NOTIFY_SERVICES_SETUP] + if self._service.service_name in services.keys(): + del services[self._service.service_name] + if not self._device_removed and service.config_entry: + self._device_removed = True + await cleanup_device_registry( + hass, service.device_id, service.config_entry.entry_id + ) + clear_discovery_hash(hass, service.discovery_hash) + self._remove_discovery() + await service.async_unregister_services() + _LOGGER.info( + "Notify service %s has been removed", + service.discovery_hash, + ) + del self._service + + self._service = service + self._remove_discovery = async_dispatcher_connect( + hass, + MQTT_DISCOVERY_UPDATED.format(service.discovery_hash), + async_discovery_update, + ) + if service.device_id: + self._remove_device_updated = hass.bus.async_listen( + EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed + ) + self._device_removed = False + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None + ) + _LOGGER.info( + "Notify service %s has been initialized", + service.discovery_hash, + ) + + +class MqttNotificationService(notify.BaseNotificationService): + """Implement the notification service for MQTT.""" + + def __init__( + self, + hass: HomeAssistant, + service_config: MqttNotificationConfig, + config_entry: ConfigEntry | None = None, + device_id: str | None = None, + discovery_hash: tuple | None = None, + ) -> None: + """Initialize the service.""" + self.hass = hass + self._config = service_config + self._commmand_template = MqttCommandTemplate( + service_config.get(CONF_COMMAND_TEMPLATE), hass=hass + ) + self._device_id = device_id + self._discovery_hash = discovery_hash + self._config_entry = config_entry + self._service_name = slugify(service_config[CONF_NAME]) + + self._updater = ( + MqttNotificationServiceUpdater(hass, self) if discovery_hash else None + ) + + @property + def device_id(self) -> str | None: + """Return the device ID.""" + return self._device_id + + @property + def config_entry(self) -> ConfigEntry | None: + """Return the config_entry.""" + return self._config_entry + + @property + def discovery_hash(self) -> tuple | None: + """Return the discovery hash.""" + return self._discovery_hash + + @property + def service_name(self) -> str: + """Return the service ma,e.""" + return self._service_name + + async def async_update_service( + self, + discovery_payload: DiscoveryInfoType, + ) -> None: + """Update the notify service through auto discovery.""" + config: MqttNotificationConfig = DISCOVERY_SCHEMA(discovery_payload) + # Do not rename a service if that service_name is already in use + if ( + new_service_name := slugify(config[CONF_NAME]) + ) != self._service_name and _check_notify_service_name( + self.hass, config + ) is None: + return + # Only refresh services if service name or targets have changes + if ( + new_service_name != self._service_name + or config[CONF_TARGETS] != self._config[CONF_TARGETS] + ): + services = self.hass.data[MQTT_NOTIFY_SERVICES_SETUP] + await self.async_unregister_services() + if self._service_name in services: + del services[self._service_name] + self._config = config + self._service_name = new_service_name + await self.async_register_services() + services[new_service_name] = self + else: + self._config = config + self._commmand_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), hass=self.hass + ) + _update_device(self.hass, self._config_entry, config) + + @property + def targets(self) -> dict[str, str]: + """Return a dictionary of registered targets.""" + return {target: target for target in self._config[CONF_TARGETS]} + + async def async_send_message(self, message: str = "", **kwargs): + """Build and send a MQTT message.""" + target = kwargs.get(notify.ATTR_TARGET) + if ( + target is not None + and self._config[CONF_TARGETS] + and set(target) & set(self._config[CONF_TARGETS]) != set(target) + ): + _LOGGER.error( + "Cannot send %s, target list %s is invalid, valid available targets: %s", + message, + target, + self._config[CONF_TARGETS], + ) + return + variables = { + "message": message, + "name": self._config[CONF_NAME], + "service": self._service_name, + "target": target or self._config[CONF_TARGETS], + "title": kwargs.get(notify.ATTR_TITLE, self._config[CONF_TITLE]), + } + variables.update(kwargs.get(notify.ATTR_DATA) or {}) + payload = self._commmand_template.async_render( + message, + variables=variables, + ) + await mqtt.async_publish( + self.hass, + self._config[CONF_COMMAND_TOPIC], + payload, + self._config[CONF_QOS], + self._config[CONF_RETAIN], + self._config[CONF_ENCODING], + ) + + +def _update_device( + hass: HomeAssistant, + config_entry: ConfigEntry | None, + config: MqttNotificationConfig, +) -> str | None: + """Update device registry.""" + if config_entry is None or CONF_DEVICE not in config: + return None + + device = None + device_registry = dr.async_get(hass) + config_entry_id = config_entry.entry_id + device_info = device_info_from_config(config[CONF_DEVICE]) + + if config_entry_id is not None and device_info is not None: + update_device_info = cast(dict, device_info) + update_device_info["config_entry_id"] = config_entry_id + device = device_registry.async_get_or_create(**update_device_info) + + return device.id if device else None diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py new file mode 100644 index 00000000000..33a32d858af --- /dev/null +++ b/tests/components/mqtt/test_notify.py @@ -0,0 +1,863 @@ +"""The tests for the MQTT button platform.""" +import copy +import json +from unittest.mock import patch + +import pytest +import yaml + +from homeassistant import config as hass_config +from homeassistant.components import notify +from homeassistant.components.mqtt import DOMAIN +from homeassistant.const import CONF_NAME, SERVICE_RELOAD +from homeassistant.exceptions import ServiceNotFound +from homeassistant.setup import async_setup_component +from homeassistant.util import slugify + +from tests.common import async_fire_mqtt_message, mock_device_registry + +DEFAULT_CONFIG = {notify.DOMAIN: {"platform": "mqtt", "command_topic": "test-topic"}} + +COMMAND_TEMPLATE_TEST_PARAMS = ( + "name,service,parameters,expected_result", + [ + ( + "My service", + "my_service", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_DATA: {"par1": "val1"}, + }, + '{"message":"Message",' + '"name":"My service",' + '"service":"my_service",' + '"par1":"val1",' + '"target":[' + "'t1', 't2'" + "]," + '"title":"Title"}', + ), + ( + "My service", + "my_service", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_DATA: {"par1": "val1"}, + notify.ATTR_TARGET: ["t2"], + }, + '{"message":"Message",' + '"name":"My service",' + '"service":"my_service",' + '"par1":"val1",' + '"target":[' + "'t2'" + "]," + '"title":"Title"}', + ), + ( + "My service", + "my_service_t1", + { + notify.ATTR_TITLE: "Title2", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_DATA: {"par1": "val2"}, + }, + '{"message":"Message",' + '"name":"My service",' + '"service":"my_service",' + '"par1":"val2",' + '"target":[' + "'t1'" + "]," + '"title":"Title2"}', + ), + ], +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +async def async_setup_notifify_service_with_auto_discovery( + hass, mqtt_mock, caplog, device_reg, data, service_name +): + """Test setup notify service with a device config.""" + caplog.clear() + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/{service_name}/config", data + ) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) + assert device_entry is not None + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + + +@pytest.mark.parametrize(*COMMAND_TEMPLATE_TEST_PARAMS) +async def test_sending_with_command_templates_with_config_setup( + hass, mqtt_mock, caplog, name, service, parameters, expected_result +): + """Test the sending MQTT commands using a template using config setup.""" + config = { + "name": name, + "command_topic": "lcd/set", + "command_template": "{" + '"message":"{{message}}",' + '"name":"{{name}}",' + '"service":"{{service}}",' + '"par1":"{{par1}}",' + '"target":{{target}},' + '"title":"{{title}}"' + "}", + "targets": ["t1", "t2"], + "platform": "mqtt", + "qos": "1", + } + service_base_name = slugify(name) + assert await async_setup_component( + hass, + notify.DOMAIN, + {notify.DOMAIN: config}, + ) + await hass.async_block_till_done() + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + await hass.services.async_call( + notify.DOMAIN, + service, + parameters, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + "lcd/set", expected_result, 1, False + ) + mqtt_mock.async_publish.reset_mock() + + +@pytest.mark.parametrize(*COMMAND_TEMPLATE_TEST_PARAMS) +async def test_sending_with_command_templates_auto_discovery( + hass, mqtt_mock, caplog, name, service, parameters, expected_result +): + """Test the sending MQTT commands using a template and auto discovery.""" + config = { + "name": name, + "command_topic": "lcd/set", + "command_template": "{" + '"message":"{{message}}",' + '"name":"{{name}}",' + '"service":"{{service}}",' + '"par1":"{{par1}}",' + '"target":{{target}},' + '"title":"{{title}}"' + "}", + "targets": ["t1", "t2"], + "qos": "1", + } + if name: + config[CONF_NAME] = name + service_base_name = slugify(name) + else: + service_base_name = DOMAIN + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/bla/config", json.dumps(config) + ) + await hass.async_block_till_done() + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + await hass.services.async_call( + notify.DOMAIN, + service, + parameters, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + "lcd/set", expected_result, 1, False + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_sending_mqtt_commands(hass, mqtt_mock, caplog): + """Test the sending MQTT commands.""" + config1 = { + "command_topic": "command-topic1", + "name": "test1", + "platform": "mqtt", + "qos": "2", + } + config2 = { + "command_topic": "command-topic2", + "name": "test2", + "targets": ["t1", "t2"], + "platform": "mqtt", + "qos": "2", + } + assert await async_setup_component( + hass, + notify.DOMAIN, + {notify.DOMAIN: [config1, config2]}, + ) + await hass.async_block_till_done() + assert "" in caplog.text + assert "" in caplog.text + assert ( + "" in caplog.text + ) + assert ( + "" in caplog.text + ) + + # test1 simple call without targets + await hass.services.async_call( + notify.DOMAIN, + "test1", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic1", "Message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + + # test2 simple call without targets + await hass.services.async_call( + notify.DOMAIN, + "test2", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic2", "Message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + + # test2 simple call main service without target + await hass.services.async_call( + notify.DOMAIN, + "test2", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic2", "Message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + + # test2 simple call main service with empty target + await hass.services.async_call( + notify.DOMAIN, + "test2", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_TARGET: [], + }, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic2", "Message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + + # test2 simple call main service with single target + await hass.services.async_call( + notify.DOMAIN, + "test2", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_TARGET: ["t1"], + }, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic2", "Message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + + # test2 simple call main service with invalid target + await hass.services.async_call( + notify.DOMAIN, + "test2", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_TARGET: ["invalid"], + }, + blocking=True, + ) + + assert ( + "Cannot send Message, target list ['invalid'] is invalid, valid available targets: ['t1', 't2']" + in caplog.text + ) + mqtt_mock.async_publish.call_count == 0 + mqtt_mock.async_publish.reset_mock() + + +async def test_with_same_name(hass, mqtt_mock, caplog): + """Test the multiple setups with the same name.""" + config1 = { + "command_topic": "command-topic1", + "name": "test_same_name", + "platform": "mqtt", + "qos": "2", + } + config2 = { + "command_topic": "command-topic2", + "name": "test_same_name", + "targets": ["t1", "t2"], + "platform": "mqtt", + "qos": "2", + } + assert await async_setup_component( + hass, + notify.DOMAIN, + {notify.DOMAIN: [config1, config2]}, + ) + await hass.async_block_till_done() + assert ( + "" + in caplog.text + ) + assert ( + "Notify service 'test_same_name' already exists, cannot register service" + in caplog.text + ) + + # test call main service on service with multiple targets with the same name + # the first configured service should publish + await hass.services.async_call( + notify.DOMAIN, + "test_same_name", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + }, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic1", "Message", 2, False + ) + mqtt_mock.async_publish.reset_mock() + + with pytest.raises(ServiceNotFound): + await hass.services.async_call( + notify.DOMAIN, + "test_same_name_t2", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_TARGET: ["t2"], + }, + blocking=True, + ) + + +async def test_discovery_without_device(hass, mqtt_mock, caplog): + """Test discovery, update and removal of notify service without device.""" + data = '{ "name": "Old name", "command_topic": "test_topic" }' + data_update = '{ "command_topic": "test_topic_update", "name": "New name" }' + data_update_with_targets1 = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target2"] }' + data_update_with_targets2 = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target3"] }' + + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) + await hass.async_block_till_done() + + assert ( + "" in caplog.text + ) + + await hass.services.async_call( + notify.DOMAIN, + "old_name", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with("test_topic", "Message", 0, False) + mqtt_mock.async_publish.reset_mock() + + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update + ) + await hass.async_block_till_done() + + assert "" in caplog.text + assert ( + "" in caplog.text + ) + assert "Notify service ('notify', 'bla') updated has been processed" in caplog.text + + await hass.services.async_call( + notify.DOMAIN, + "new_name", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "test_topic_update", "Message", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") + await hass.async_block_till_done() + + assert "" in caplog.text + + # rediscover with targets + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update_with_targets1 + ) + await hass.async_block_till_done() + + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + caplog.clear() + + # update available targets + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update_with_targets2 + ) + await hass.async_block_till_done() + + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + caplog.clear() + + # test if a new service with same name fails to setup + config1 = { + "command_topic": "command-topic-config.yaml", + "name": "test-setup1", + "platform": "mqtt", + "qos": "2", + } + assert await async_setup_component( + hass, + notify.DOMAIN, + {notify.DOMAIN: [config1]}, + ) + await hass.async_block_till_done() + data = '{ "name": "test-setup1", "command_topic": "test_topic" }' + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/test-setup1/config", data + ) + await hass.async_block_till_done() + assert ( + "Notify service 'test_setup1' already exists, cannot register service" + in caplog.text + ) + await hass.services.async_call( + notify.DOMAIN, + "test_setup1", + { + notify.ATTR_TITLE: "Title", + notify.ATTR_MESSAGE: "Message", + notify.ATTR_TARGET: ["t2"], + }, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + "command-topic-config.yaml", "Message", 2, False + ) + + # Test with same discovery on new name + data = '{ "name": "testa", "command_topic": "test_topic_a" }' + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testa/config", data) + await hass.async_block_till_done() + assert "" in caplog.text + + data = '{ "name": "testb", "command_topic": "test_topic_b" }' + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testb/config", data) + await hass.async_block_till_done() + assert "" in caplog.text + + # Try to update from new discovery of existing service test + data = '{ "name": "testa", "command_topic": "test_topic_c" }' + caplog.clear() + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testc/config", data) + await hass.async_block_till_done() + assert ( + "Notify service 'testa' already exists, cannot register service" in caplog.text + ) + + # Try to update the same discovery to existing service test + data = '{ "name": "testa", "command_topic": "test_topic_c" }' + caplog.clear() + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testb/config", data) + await hass.async_block_till_done() + assert ( + "Notify service 'testa' already exists, cannot register service" in caplog.text + ) + + +async def test_discovery_with_device_update(hass, mqtt_mock, caplog, device_reg): + """Test discovery, update and removal of notify service with a device config.""" + + # Initial setup + data = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' + service_name = "my_notify_service" + await async_setup_notifify_service_with_auto_discovery( + hass, mqtt_mock, caplog, device_reg, data, service_name + ) + assert "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + + +async def test_discovery_with_device_removal(hass, mqtt_mock, caplog, device_reg): + """Test discovery, update and removal of notify service with a device config.""" + + # Initial setup + data1 = '{ "command_topic": "test_topic", "name": "My notify service1", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' + data2 = '{ "command_topic": "test_topic", "name": "My notify service2", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' + service_name1 = "my_notify_service1" + service_name2 = "my_notify_service2" + await async_setup_notifify_service_with_auto_discovery( + hass, mqtt_mock, caplog, device_reg, data1, service_name1 + ) + assert "" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + not in caplog.text + ) + caplog.clear() + + # The device should still be there + device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) + assert device_entry is not None + device_id = device_entry.id + assert device_id == device_entry.id + assert device_entry.name == "Test123" + + # Test removal device from device registry after removing second service + async_fire_mqtt_message( + hass, f"homeassistant/{notify.DOMAIN}/{service_name2}/config", "{}" + ) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) + assert device_entry is None + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + caplog.clear() + + # Recreate the service and device + await async_setup_notifify_service_with_auto_discovery( + hass, mqtt_mock, caplog, device_reg, data1, service_name1 + ) + assert "" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + assert ( + f"" + in caplog.text + ) + + +async def test_publishing_with_custom_encoding(hass, mqtt_mock, caplog): + """Test publishing MQTT payload with different encoding via discovery and configuration.""" + # test with default encoding using configuration setup + assert await async_setup_component( + hass, + notify.DOMAIN, + { + notify.DOMAIN: { + "command_topic": "command-topic", + "name": "test", + "platform": "mqtt", + "qos": "2", + } + }, + ) + await hass.async_block_till_done() + + # test with raw encoding and discovery + data = '{"name": "test2", "command_topic": "test_topic2", "command_template": "{{ pack(int(message), \'b\') }}" }' + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) + await hass.async_block_till_done() + + assert "Notify service ('notify', 'bla') has been initialized" in caplog.text + assert "" in caplog.text + + await hass.services.async_call( + notify.DOMAIN, + "test2", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "4"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with("test_topic2", b"\x04", 0, False) + mqtt_mock.async_publish.reset_mock() + + # test with utf-16 and update discovery + data = '{"encoding":"utf-16", "name": "test3", "command_topic": "test_topic3", "command_template": "{{ message }}" }' + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) + await hass.async_block_till_done() + assert ( + "Component has already been discovered: notify bla, sending update" + in caplog.text + ) + + await hass.services.async_call( + notify.DOMAIN, + "test3", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_topic3", "Message".encode("utf-16"), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") + await hass.async_block_till_done() + + assert "Notify service ('notify', 'bla') has been removed" in caplog.text + + +async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): + """Test reloading the MQTT platform.""" + domain = notify.DOMAIN + config = DEFAULT_CONFIG[domain] + + # Create and test an old config of 2 entities based on the config supplied + old_config_1 = copy.deepcopy(config) + old_config_1["name"] = "Test old 1" + old_config_2 = copy.deepcopy(config) + old_config_2["name"] = "Test old 2" + + assert await async_setup_component( + hass, domain, {domain: [old_config_1, old_config_2]} + ) + await hass.async_block_till_done() + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + caplog.clear() + + # Add an auto discovered notify target + data = '{"name": "Test old 3", "command_topic": "test_topic_discovery" }' + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) + await hass.async_block_till_done() + + assert "Notify service ('notify', 'bla') has been initialized" in caplog.text + assert ( + "" + in caplog.text + ) + + # Create temporary fixture for configuration.yaml based on the supplied config and test a reload with this new config + new_config_1 = copy.deepcopy(config) + new_config_1["name"] = "Test new 1" + new_config_2 = copy.deepcopy(config) + new_config_2["name"] = "test new 2" + new_config_3 = copy.deepcopy(config) + new_config_3["name"] = "test new 3" + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({domain: [new_config_1, new_config_2, new_config_3]}) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert ( + "" in caplog.text + ) + assert ( + "" in caplog.text + ) + + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + assert ( + "" + in caplog.text + ) + assert "" in caplog.text + caplog.clear() + + # test if the auto discovered item survived the platform reload + await hass.services.async_call( + notify.DOMAIN, + "test_old_3", + {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_topic_discovery", "Message", 0, False + ) + + mqtt_mock.async_publish.reset_mock() + + async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") + await hass.async_block_till_done() + + assert "Notify service ('notify', 'bla') has been removed" in caplog.text From accc4fda2810bfff01c36527a95db1967cf2bc2d Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 8 Mar 2022 23:53:17 +0800 Subject: [PATCH 0318/1054] Bump PyAV to v9.0.0 (#67848) --- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index d8fd035a5ab..f2c56d0af80 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "av==8.1.0"], + "requirements": ["PyTurboJPEG==1.6.6", "av==9.0.0"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 71087ec9565..6aa1319bef0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -341,7 +341,7 @@ auroranoaa==0.0.2 aurorapy==0.2.6 # homeassistant.components.stream -av==8.1.0 +av==9.0.0 # homeassistant.components.avea # avea==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db7e07175d9..a2738303a03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -268,7 +268,7 @@ auroranoaa==0.0.2 aurorapy==0.2.6 # homeassistant.components.stream -av==8.1.0 +av==9.0.0 # homeassistant.components.axis axis==44 From 7323ad2799f4eaa37a3bb826d4d79a81b178de8a Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Tue, 8 Mar 2022 16:56:15 +0100 Subject: [PATCH 0319/1054] Support playing local "file" media on Kodi (#67832) Co-authored-by: Paulus Schoutsen --- homeassistant/components/kodi/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 53798a7ccd9..e1dae879bf8 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -717,6 +717,8 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.play_channel(int(media_id)) elif media_type_lower == MEDIA_TYPE_PLAYLIST: await self._kodi.play_playlist(int(media_id)) + elif media_type_lower == "file": + await self._kodi.play_file(media_id) elif media_type_lower == "directory": await self._kodi.play_directory(media_id) elif media_type_lower in [ From 98256746d3966e63fd1c4cd5a2f0a407437ab8ee Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 8 Mar 2022 19:56:57 +0200 Subject: [PATCH 0320/1054] Fix shelly duo scene restore (#67871) --- homeassistant/components/shelly/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index b0fb9cd1a8a..13a7720e7ed 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -336,7 +336,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): ATTR_RGBW_COLOR ] - if ATTR_EFFECT in kwargs: + if ATTR_EFFECT in kwargs and ATTR_COLOR_TEMP not in kwargs: # Color effect change - used only in color mode, switch device mode to color set_mode = "color" if self.wrapper.model == "SHBLB-1": From 8adcd10f55f5df77382029ca7ff94260612fbd91 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Mar 2022 12:56:49 -0800 Subject: [PATCH 0321/1054] Make google calendar loading API centric, not loading from yaml (#67722) * Make google calendar loading API centric, not loading from yaml Update the behavior for google calendar to focus on loading calendars based on the API and using the yaml configuration to override behavior. The old behavior was to first load from yaml, then also load from the API, which is atypical. This is pulled out from a larger change to rewrite calendar using async and config flows. Tests needed to be updated to reflect the new API centric behavior, and changing the API call ordering required changing tests that exercise failures. * Update to use async_fire_time_changed to invoke updates --- homeassistant/components/google/__init__.py | 41 ++++++---- tests/components/google/conftest.py | 2 +- tests/components/google/test_calendar.py | 85 +++++++++++++++++--- tests/components/google/test_init.py | 86 ++++++++++++++++++--- 4 files changed, 177 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index be973ac9b45..87d76b9f4c8 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -74,7 +74,6 @@ SERVICE_SCAN_CALENDARS = "scan_for_calendars" SERVICE_FOUND_CALENDARS = "found_calendar" SERVICE_ADD_EVENT = "add_event" -DATA_CALENDARS = "calendars" DATA_SERVICE = "service" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" @@ -257,7 +256,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: storage = Storage(hass.config.path(TOKEN_FILE)) hass.data[DOMAIN] = { - DATA_CALENDARS: {}, DATA_SERVICE: GoogleCalendarService(hass, storage), } creds = storage.get() @@ -281,14 +279,26 @@ def setup_services( ) -> None: """Set up the service listeners.""" + created_calendars = set() + calendars = load_config(hass.config.path(YAML_DEVICES)) + def _found_calendar(call: ServiceCall) -> None: """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" calendar = get_calendar_info(hass, call.data) calendar_id = calendar[CONF_CAL_ID] - if calendar_id in hass.data[DOMAIN][DATA_CALENDARS]: + + if calendar_id in created_calendars: return - hass.data[DOMAIN][DATA_CALENDARS][calendar_id] = calendar - update_config(hass.config.path(YAML_DEVICES), calendar) + created_calendars.add(calendar_id) + + # Populate the yaml file with all discovered calendars + if calendar_id not in calendars: + calendars[calendar_id] = calendar + update_config(hass.config.path(YAML_DEVICES), calendar) + else: + # Prefer entity/name information from yaml, overriding api + calendar = calendars[calendar_id] + discovery.load_platform( hass, Platform.CALENDAR, @@ -363,17 +373,10 @@ def setup_services( def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) -> None: """Run the setup after we have everything configured.""" - # Load calendars the user has configured - calendars = load_config(hass.config.path(YAML_DEVICES)) - hass.data[DOMAIN][DATA_CALENDARS] = calendars - calendar_service = hass.data[DOMAIN][DATA_SERVICE] setup_services(hass, hass_config, config, calendar_service) - for calendar in calendars.values(): - discovery.load_platform(hass, Platform.CALENDAR, DOMAIN, calendar, hass_config) - - # Look for any new calendars + # Fetch calendars from the API hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) @@ -410,7 +413,8 @@ def load_config(path: str) -> dict[str, Any]: except VoluptuousError as exception: # keep going _LOGGER.warning("Calendar Invalid Data: %s", exception) - except FileNotFoundError: + except FileNotFoundError as err: + _LOGGER.debug("Error reading calendar configuration: %s", err) # When YAML file could not be loaded/did not contain a dict return {} @@ -419,6 +423,9 @@ def load_config(path: str) -> dict[str, Any]: def update_config(path: str, calendar: dict[str, Any]) -> None: """Write the google_calendar_devices.yaml.""" - with open(path, "a", encoding="utf8") as out: - out.write("\n") - yaml.dump([calendar], out, default_flow_style=False) + try: + with open(path, "a", encoding="utf8") as out: + out.write("\n") + yaml.dump([calendar], out, default_flow_style=False) + except FileNotFoundError as err: + _LOGGER.debug("Error persisting calendar configuration: %s", err) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 5e1411c76c8..f3d8f9a28b9 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -99,7 +99,7 @@ def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, ] -@pytest.fixture +@pytest.fixture(autouse=True) async def mock_calendars_yaml( hass: HomeAssistant, calendars_config: list[dict[str, Any]], diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 76ab20ff41b..34227ae02b1 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -5,7 +5,7 @@ from __future__ import annotations import datetime from http import HTTPStatus from typing import Any -from unittest.mock import Mock +from unittest.mock import patch import httplib2 import pytest @@ -16,6 +16,8 @@ import homeassistant.util.dt as dt_util from .conftest import TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME +from tests.common import async_fire_time_changed + TEST_ENTITY = TEST_YAML_ENTITY TEST_ENTITY_NAME = TEST_YAML_ENTITY_NAME @@ -50,8 +52,11 @@ TEST_EVENT = { @pytest.fixture(autouse=True) -def mock_test_setup(mock_calendars_yaml, mock_token_read): +def mock_test_setup( + mock_calendars_yaml, test_api_calendar, mock_calendars_list, mock_token_read +): """Fixture that pulls in the default fixtures for tests in this file.""" + mock_calendars_list({"items": [test_api_calendar]}) return @@ -252,13 +257,72 @@ async def test_all_day_offset_event(hass, mock_events_list_items, component_setu } -async def test_update_error(hass, calendar_resource, component_setup): - """Test that the calendar handles a server error.""" - calendar_resource.return_value.get = Mock( - side_effect=httplib2.ServerNotFoundError("unit test") - ) - assert await component_setup() +async def test_update_error( + hass, calendar_resource, component_setup, test_api_calendar +): + """Test that the calendar update handles a server error.""" + now = dt_util.now() + with patch("homeassistant.components.google.api.google_discovery.build") as mock: + mock.return_value.calendarList.return_value.list.return_value.execute.return_value = { + "items": [test_api_calendar] + } + mock.return_value.events.return_value.list.return_value.execute.return_value = { + "items": [ + { + **TEST_EVENT, + "start": { + "dateTime": (now + datetime.timedelta(minutes=-30)).isoformat() + }, + "end": { + "dateTime": (now + datetime.timedelta(minutes=30)).isoformat() + }, + } + ] + } + assert await component_setup() + + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "on" + + # Advance time to avoid throttling + now += datetime.timedelta(minutes=30) + with patch( + "homeassistant.components.google.api.google_discovery.build", + side_effect=httplib2.ServerNotFoundError("unit test"), + ), patch("homeassistant.util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # No change + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "on" + + # Advance time to avoid throttling + now += datetime.timedelta(minutes=30) + with patch( + "homeassistant.components.google.api.google_discovery.build" + ) as mock, patch("homeassistant.util.utcnow", return_value=now): + + mock.return_value.events.return_value.list.return_value.execute.return_value = { + "items": [ + { + **TEST_EVENT, + "start": { + "dateTime": (now + datetime.timedelta(minutes=30)).isoformat() + }, + "end": { + "dateTime": (now + datetime.timedelta(minutes=60)).isoformat() + }, + } + ] + } + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # State updated state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" @@ -284,11 +348,12 @@ async def test_http_event_api_failure( hass, hass_client, calendar_resource, component_setup ): """Test the Rest API response during a calendar failure.""" - calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") - assert await component_setup() client = await hass_client() + + calendar_resource.side_effect = httplib2.ServerNotFoundError("unit test") + response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.OK # A failure to talk to the server results in an empty list of events diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index a0766c8256e..0f02e20c735 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -13,7 +13,11 @@ from oauth2client.client import ( ) import pytest -from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT +from homeassistant.components.google import ( + DOMAIN, + SERVICE_ADD_EVENT, + SERVICE_SCAN_CALENDARS, +) from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State from homeassistant.util.dt import utcnow @@ -109,10 +113,13 @@ async def test_init_success( mock_code_flow: Mock, mock_exchange: Mock, mock_notification: Mock, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], mock_calendars_yaml: None, component_setup: ComponentSetup, ) -> None: """Test successful creds setup.""" + mock_calendars_list({"items": [test_api_calendar]}) assert await component_setup() # Run one tick to invoke the credential exchange check @@ -199,9 +206,12 @@ async def test_existing_token( mock_token_read: None, component_setup: ComponentSetup, mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], mock_notification: Mock, ) -> None: """Test setup with an existing token file.""" + mock_calendars_list({"items": [test_api_calendar]}) assert await component_setup() state = hass.states.get(TEST_YAML_ENTITY) @@ -221,11 +231,14 @@ async def test_existing_token_missing_scope( mock_token_read: None, component_setup: ComponentSetup, mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], mock_notification: Mock, mock_code_flow: Mock, mock_exchange: Mock, ) -> None: """Test setup where existing token does not have sufficient scopes.""" + mock_calendars_list({"items": [test_api_calendar]}) assert await component_setup() # Run one tick to invoke the credential exchange check @@ -279,6 +292,25 @@ async def test_invalid_calendar_yaml( mock_notification.assert_not_called() +async def test_calendar_yaml_error( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_notification: Mock, +) -> None: + """Test setup with yaml file not found.""" + + mock_calendars_list({"items": [test_api_calendar]}) + + with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()): + assert await component_setup() + + assert not hass.states.get(TEST_YAML_ENTITY) + assert hass.states.get(TEST_API_ENTITY) + + @pytest.mark.parametrize( "google_config_track_new,calendars_config,expected_state", [ @@ -324,7 +356,6 @@ async def test_track_new( mock_calendars_list({"items": [test_api_calendar]}) assert await component_setup() - # The calendar does not state = hass.states.get(TEST_API_ENTITY) assert_state(state, expected_state) @@ -343,7 +374,6 @@ async def test_found_calendar_from_api( mock_calendars_list({"items": [test_api_calendar]}) assert await component_setup() - # The calendar does not state = hass.states.get(TEST_API_ENTITY) assert state assert state.name == TEST_API_ENTITY_NAME @@ -387,12 +417,6 @@ async def test_calendar_config_track_new( state = hass.states.get(TEST_YAML_ENTITY) assert_state(state, expected_state) - if calendars_config_track: - assert state - assert state.name == TEST_YAML_ENTITY_NAME - assert state.state == STATE_OFF - else: - assert not state async def test_add_event( @@ -573,3 +597,47 @@ async def test_add_event_date_time( }, }, ) + + +async def test_scan_calendars( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], +) -> None: + """Test finding a calendar from the API.""" + + assert await component_setup() + + calendar_1 = { + "id": "calendar-id-1", + "summary": "Calendar 1", + } + calendar_2 = { + "id": "calendar-id-2", + "summary": "Calendar 2", + } + + mock_calendars_list({"items": [calendar_1]}) + await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert state.name == "Calendar 1" + assert state.state == STATE_OFF + assert not hass.states.get("calendar.calendar_2") + + mock_calendars_list({"items": [calendar_1, calendar_2]}) + await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert state.name == "Calendar 1" + assert state.state == STATE_OFF + state = hass.states.get("calendar.calendar_2") + assert state + assert state.name == "Calendar 2" + assert state.state == STATE_OFF From 9ab56bd5c846d42ca32c885072d87c0f12932ea2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Mar 2022 22:27:53 +0100 Subject: [PATCH 0322/1054] Allow discovery to update invalid unique_id in SamsungTV (#67859) Co-authored-by: epenet --- .../components/samsungtv/config_flow.py | 33 ++++++--- .../components/samsungtv/test_config_flow.py | 68 +++++++++++++++++++ 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index a5d932cc32c..12ff47869d6 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -215,15 +215,23 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) @callback - def _async_get_existing_matching_entry(self) -> config_entries.ConfigEntry | None: - """Get first existing matching entry.""" + def _async_get_existing_matching_entry( + self, + ) -> tuple[config_entries.ConfigEntry | None, bool]: + """Get first existing matching entry (prefer unique id).""" + matching_host_entry: config_entries.ConfigEntry | None = None for entry in self._async_current_entries(include_ignore=False): - mac = entry.data.get(CONF_MAC) - mac_match = mac and self._mac and mac == self._mac - upnp_udn_match = self._upnp_udn and self._upnp_udn == entry.unique_id - if entry.data[CONF_HOST] == self._host or mac_match or upnp_udn_match: - return entry - return None + if (self._mac and self._mac == entry.data.get(CONF_MAC)) or ( + self._upnp_udn and self._upnp_udn == entry.unique_id + ): + LOGGER.debug("Found entry matching unique_id for %s", self._host) + return entry, True + + if entry.data[CONF_HOST] == self._host: + LOGGER.debug("Found entry matching host for %s", self._host) + matching_host_entry = entry + + return matching_host_entry, False @callback def _async_update_existing_matching_entry( @@ -233,15 +241,18 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Returns the existing entry if it was updated. """ - if entry := self._async_get_existing_matching_entry(): + entry, is_unique_match = self._async_get_existing_matching_entry() + if entry: entry_kw_args: dict = {} - if (self._udn and self._upnp_udn and self._upnp_udn != self._udn) or ( - self.unique_id and entry.unique_id is None + if self.unique_id and ( + entry.unique_id is None + or (is_unique_match and self.unique_id != entry.unique_id) ): entry_kw_args["unique_id"] = self.unique_id if self._mac and not entry.data.get(CONF_MAC): entry_kw_args["data"] = {**entry.data, CONF_MAC: self._mac} if entry_kw_args: + LOGGER.debug("Updating existing config entry with %s", entry_kw_args) self.hass.config_entries.async_update_entry(entry, **entry_kw_args) self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index a3565f4d884..f8a787753f0 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1403,3 +1403,71 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews") +async def test_update_incorrect_udn_matching_mac_from_dhcp( + hass: HomeAssistant, +) -> None: + """Test that DHCP updates the wrong udn from ssdp via mac match.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_WS_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + source=config_entries.SOURCE_SSDP, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews") +async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( + hass: HomeAssistant, +) -> None: + """Test that DHCP does not update the wrong udn from ssdp via host match.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_WS_ENTRY, CONF_MAC: "aa:bb:ss:ss:dd:pp"}, + source=config_entries.SOURCE_SSDP, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + assert result["type"] == "form" + assert result["step_id"] == "confirm" + assert entry.data[CONF_MAC] == "aa:bb:ss:ss:dd:pp" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" From 8549e31b276d69c5cce44d0209aae88d6936e360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 8 Mar 2022 22:28:39 +0100 Subject: [PATCH 0323/1054] Add Airzone HVAC Zoning Systems (#67666) --- CODEOWNERS | 2 + homeassistant/components/airzone/__init__.py | 65 +++++++ .../components/airzone/config_flow.py | 61 +++++++ homeassistant/components/airzone/const.py | 9 + .../components/airzone/coordinator.py | 42 +++++ .../components/airzone/manifest.json | 10 ++ homeassistant/components/airzone/sensor.py | 95 ++++++++++ homeassistant/components/airzone/strings.json | 19 ++ .../components/airzone/translations/en.json | 19 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/airzone/__init__.py | 1 + tests/components/airzone/test_config_flow.py | 82 +++++++++ tests/components/airzone/test_init.py | 31 ++++ tests/components/airzone/test_sensor.py | 39 +++++ tests/components/airzone/util.py | 164 ++++++++++++++++++ 17 files changed, 646 insertions(+) create mode 100644 homeassistant/components/airzone/__init__.py create mode 100644 homeassistant/components/airzone/config_flow.py create mode 100644 homeassistant/components/airzone/const.py create mode 100644 homeassistant/components/airzone/coordinator.py create mode 100644 homeassistant/components/airzone/manifest.json create mode 100644 homeassistant/components/airzone/sensor.py create mode 100644 homeassistant/components/airzone/strings.json create mode 100644 homeassistant/components/airzone/translations/en.json create mode 100644 tests/components/airzone/__init__.py create mode 100644 tests/components/airzone/test_config_flow.py create mode 100644 tests/components/airzone/test_init.py create mode 100644 tests/components/airzone/test_sensor.py create mode 100644 tests/components/airzone/util.py diff --git a/CODEOWNERS b/CODEOWNERS index 6d145a064a1..3313fd7bfb9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -45,6 +45,8 @@ homeassistant/components/airtouch4/* @LonePurpleWolf tests/components/airtouch4/* @LonePurpleWolf homeassistant/components/airvisual/* @bachya tests/components/airvisual/* @bachya +homeassistant/components/airzone/* @Noltari +tests/components/airzone/* @Noltari homeassistant/components/alarm_control_panel/* @home-assistant/core tests/components/alarm_control_panel/* @home-assistant/core homeassistant/components/alert/* @home-assistant/core diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py new file mode 100644 index 00000000000..183a759122a --- /dev/null +++ b/homeassistant/components/airzone/__init__.py @@ -0,0 +1,65 @@ +"""The Airzone integration.""" +from __future__ import annotations + +from aioairzone.common import ConnectionOptions +from aioairzone.localapi_device import AirzoneLocalApi + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import AirzoneUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +class AirzoneEntity(CoordinatorEntity): + """Define an Airzone entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + system_zone_id: str, + zone_name: str, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info: DeviceInfo = { + "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, + "manufacturer": MANUFACTURER, + "name": f"Airzone [{system_zone_id}] {zone_name}", + } + self.system_zone_id = system_zone_id + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Airzone from a config entry.""" + options = ConnectionOptions( + entry.data[CONF_HOST], + entry.data[CONF_PORT], + ) + + airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options) + + coordinator = AirzoneUpdateCoordinator(hass, airzone) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py new file mode 100644 index 00000000000..c78f43a7db7 --- /dev/null +++ b/homeassistant/components/airzone/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for Airzone.""" +from __future__ import annotations + +from typing import Any + +from aioairzone.common import ConnectionOptions +from aioairzone.exceptions import InvalidHost +from aioairzone.localapi_device import AirzoneLocalApi +from aiohttp.client_exceptions import ClientConnectorError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client + +from .const import DEFAULT_LOCAL_API_PORT, DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle config flow for an Airzone device.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + self._async_abort_entries_match( + { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) + + try: + airzone = AirzoneLocalApi( + aiohttp_client.async_get_clientsession(self.hass), + ConnectionOptions( + user_input[CONF_HOST], + user_input[CONF_PORT], + ), + ) + await airzone.validate_airzone() + except (ClientConnectorError, InvalidHost): + errors["base"] = "cannot_connect" + else: + title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}" + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_LOCAL_API_PORT): int, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/airzone/const.py b/homeassistant/components/airzone/const.py new file mode 100644 index 00000000000..8c48cc1aca1 --- /dev/null +++ b/homeassistant/components/airzone/const.py @@ -0,0 +1,9 @@ +"""Constants for the Airzone integration.""" + +from typing import Final + +DOMAIN: Final = "airzone" +MANUFACTURER: Final = "Airzone" + +AIOAIRZONE_DEVICE_TIMEOUT_SEC: Final = 10 +DEFAULT_LOCAL_API_PORT: Final = 3000 diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py new file mode 100644 index 00000000000..b12305e7913 --- /dev/null +++ b/homeassistant/components/airzone/coordinator.py @@ -0,0 +1,42 @@ +"""The Airzone integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioairzone.localapi_device import AirzoneLocalApi +from aiohttp.client_exceptions import ClientConnectorError +import async_timeout + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import AIOAIRZONE_DEVICE_TIMEOUT_SEC, DOMAIN + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +class AirzoneUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the Airzone device.""" + + def __init__(self, hass: HomeAssistant, airzone: AirzoneLocalApi) -> None: + """Initialize.""" + self.airzone = airzone + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Update data via library.""" + async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): + try: + await self.airzone.update_airzone() + return self.airzone.data() + except ClientConnectorError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json new file mode 100644 index 00000000000..c6836a63ee5 --- /dev/null +++ b/homeassistant/components/airzone/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "airzone", + "name": "Airzone", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airzone", + "requirements": ["aioairzone==0.0.2"], + "codeowners": ["@Noltari"], + "iot_class": "local_polling", + "loggers": ["aioairzone"] +} diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py new file mode 100644 index 00000000000..e860eba1ad1 --- /dev/null +++ b/homeassistant/components/airzone/sensor.py @@ -0,0 +1,95 @@ +"""Support for the Airzone sensors.""" +from __future__ import annotations + +from typing import Final + +from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_ZONES + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirzoneEntity +from .const import DOMAIN +from .coordinator import AirzoneUpdateCoordinator + +SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + device_class=DEVICE_CLASS_TEMPERATURE, + key=AZD_TEMP, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + device_class=DEVICE_CLASS_HUMIDITY, + key=AZD_HUMIDITY, + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + sensors = [] + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): + zone_name = zone_data[AZD_NAME] + + for description in SENSOR_TYPES: + if description.key in zone_data: + sensors.append( + AirzoneSensor( + coordinator, + description, + entry, + system_zone_id, + zone_name, + ) + ) + + async_add_entities(sensors) + + +class AirzoneSensor(AirzoneEntity, SensorEntity): + """Define an Airzone sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: SensorEntityDescription, + entry: ConfigEntry, + system_zone_id: str, + zone_name: str, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_zone_id, zone_name) + self._attr_name = f"{zone_name} {description.name}" + self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}" + self.entity_description = description + + @property + def native_value(self): + """Return the state.""" + value = None + if self.system_zone_id in self.coordinator.data[AZD_ZONES]: + zone = self.coordinator.data[AZD_ZONES][self.system_zone_id] + if self.entity_description.key in zone: + value = zone[self.entity_description.key] + return value diff --git a/homeassistant/components/airzone/strings.json b/homeassistant/components/airzone/strings.json new file mode 100644 index 00000000000..e6ee49a6786 --- /dev/null +++ b/homeassistant/components/airzone/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Set up Airzone integration." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/en.json b/homeassistant/components/airzone/translations/en.json new file mode 100644 index 00000000000..b24e62fa34e --- /dev/null +++ b/homeassistant/components/airzone/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Set up Airzone integration." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 195f09c91a1..7e94ae5a165 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -19,6 +19,7 @@ FLOWS = [ "airthings", "airtouch4", "airvisual", + "airzone", "alarmdecoder", "almond", "ambee", diff --git a/requirements_all.txt b/requirements_all.txt index 6aa1319bef0..e318f61ccfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,6 +106,9 @@ aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs aio_georss_gdacs==0.5 +# homeassistant.components.airzone +aioairzone==0.0.2 + # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2738303a03..f1aa2224391 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,6 +87,9 @@ aio_geojson_nsw_rfs_incidents==0.4 # homeassistant.components.gdacs aio_georss_gdacs==0.5 +# homeassistant.components.airzone +aioairzone==0.0.2 + # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/tests/components/airzone/__init__.py b/tests/components/airzone/__init__.py new file mode 100644 index 00000000000..1d38439991b --- /dev/null +++ b/tests/components/airzone/__init__.py @@ -0,0 +1 @@ +"""Tests for the Airzone integration.""" diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py new file mode 100644 index 00000000000..bb878ea6fc8 --- /dev/null +++ b/tests/components/airzone/test_config_flow.py @@ -0,0 +1,82 @@ +"""Define tests for the Airzone config flow.""" + +from unittest.mock import MagicMock, patch + +from aiohttp.client_exceptions import ClientConnectorError + +from homeassistant import data_entry_flow +from homeassistant.components.airzone.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PORT + +from .util import CONFIG, HVAC_MOCK + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test that the form is served with valid input.""" + + with patch( + "homeassistant.components.airzone.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + entry = conf_entries[0] + assert entry.state is ConfigEntryState.LOADED + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" + assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] + assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_duplicated_id(hass): + """Test setting up duplicated entry.""" + + with patch( + "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass): + """Test connection to host error.""" + + with patch( + "aioairzone.localapi_device.AirzoneLocalApi.validate_airzone", + side_effect=ClientConnectorError(MagicMock(), MagicMock()), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py new file mode 100644 index 00000000000..8443148af65 --- /dev/null +++ b/tests/components/airzone/test_init.py @@ -0,0 +1,31 @@ +"""Define tests for the Airzone init.""" + +from unittest.mock import patch + +from homeassistant.components.airzone.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from .util import CONFIG, HVAC_MOCK + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test unload.""" + + with patch( + "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="airzone_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py new file mode 100644 index 00000000000..fc03d8a3301 --- /dev/null +++ b/tests/components/airzone/test_sensor.py @@ -0,0 +1,39 @@ +"""The sensor tests for the Airzone platform.""" + +from .util import async_init_integration + + +async def test_airzone_create_sensors(hass): + """Test creation of sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.despacho_temperature") + assert state.state == "21.2" + + state = hass.states.get("sensor.despacho_humidity") + assert state.state == "36" + + state = hass.states.get("sensor.dorm_1_temperature") + assert state.state == "20.8" + + state = hass.states.get("sensor.dorm_1_humidity") + assert state.state == "35" + + state = hass.states.get("sensor.dorm_2_temperature") + assert state.state == "20.5" + + state = hass.states.get("sensor.dorm_2_humidity") + assert state.state == "40" + + state = hass.states.get("sensor.dorm_ppal_temperature") + assert state.state == "21.1" + + state = hass.states.get("sensor.dorm_ppal_humidity") + assert state.state == "39" + + state = hass.states.get("sensor.salon_temperature") + assert state.state == "19.6" + + state = hass.states.get("sensor.salon_humidity") + assert state.state == "34" diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py new file mode 100644 index 00000000000..268a7fd1d2b --- /dev/null +++ b/tests/components/airzone/util.py @@ -0,0 +1,164 @@ +"""Tests for the Airzone integration.""" + +from unittest.mock import patch + +from aioairzone.const import ( + API_AIR_DEMAND, + API_COLD_STAGE, + API_COLD_STAGES, + API_DATA, + API_ERRORS, + API_FLOOR_DEMAND, + API_HEAT_STAGE, + API_HEAT_STAGES, + API_HUMIDITY, + API_MAX_TEMP, + API_MIN_TEMP, + API_MODE, + API_MODES, + API_NAME, + API_ON, + API_ROOM_TEMP, + API_SET_POINT, + API_SYSTEM_ID, + API_SYSTEMS, + API_UNITS, + API_ZONE_ID, +) + +from homeassistant.components.airzone import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONFIG = { + CONF_HOST: "192.168.1.100", + CONF_PORT: 3000, +} + +HVAC_MOCK = { + API_SYSTEMS: [ + { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_NAME: "Salon", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.5, + API_ROOM_TEMP: 19.6, + API_MODES: [1, 4, 2, 3, 5], + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 34, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 2, + API_NAME: "Dorm Ppal", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.5, + API_ROOM_TEMP: 21.1, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 39, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_NAME: "Dorm #1", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.5, + API_ROOM_TEMP: 20.8, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 35, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 4, + API_NAME: "Despacho", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.5, + API_ROOM_TEMP: 21.2, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 36, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 5, + API_NAME: "Dorm #2", + API_ON: 0, + API_MAX_TEMP: 30, + API_MIN_TEMP: 15, + API_SET_POINT: 19.5, + API_ROOM_TEMP: 20.5, + API_MODE: 3, + API_COLD_STAGES: 1, + API_COLD_STAGE: 1, + API_HEAT_STAGES: 1, + API_HEAT_STAGE: 1, + API_HUMIDITY: 40, + API_UNITS: 0, + API_ERRORS: [], + API_AIR_DEMAND: 0, + API_FLOOR_DEMAND: 0, + }, + ] + } + ] +} + + +async def async_init_integration( + hass: HomeAssistant, +): + """Set up the Airzone integration in Home Assistant.""" + + with patch( + "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ): + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() From 6af9c897e4229dd0ae2c021c2ffb44722f9309cf Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 9 Mar 2022 00:19:01 +0000 Subject: [PATCH 0324/1054] [ci skip] Translation update --- .../components/airzone/translations/it.json | 19 ++++++ .../components/group/translations/bg.json | 9 +++ .../components/group/translations/nl.json | 62 +++++++++++++++++++ .../components/group/translations/pt-BR.json | 62 +++++++++++++++++++ .../components/group/translations/tr.json | 62 +++++++++++++++++++ .../kaleidescape/translations/bg.json | 25 ++++++++ .../kaleidescape/translations/ca.json | 27 ++++++++ .../kaleidescape/translations/de.json | 27 ++++++++ .../kaleidescape/translations/el.json | 27 ++++++++ .../kaleidescape/translations/et.json | 27 ++++++++ .../kaleidescape/translations/id.json | 27 ++++++++ .../kaleidescape/translations/it.json | 27 ++++++++ .../kaleidescape/translations/nl.json | 20 ++++++ .../kaleidescape/translations/pl.json | 27 ++++++++ .../kaleidescape/translations/pt-BR.json | 27 ++++++++ .../kaleidescape/translations/ru.json | 27 ++++++++ .../kaleidescape/translations/tr.json | 27 ++++++++ .../kaleidescape/translations/zh-Hant.json | 27 ++++++++ .../components/season/translations/bg.json | 7 +++ .../components/season/translations/nl.json | 7 +++ .../components/season/translations/pl.json | 14 +++++ .../components/season/translations/pt-BR.json | 14 +++++ .../components/season/translations/ru.json | 14 +++++ .../components/season/translations/tr.json | 14 +++++ .../season/translations/zh-Hant.json | 14 +++++ 25 files changed, 640 insertions(+) create mode 100644 homeassistant/components/airzone/translations/it.json create mode 100644 homeassistant/components/kaleidescape/translations/bg.json create mode 100644 homeassistant/components/kaleidescape/translations/ca.json create mode 100644 homeassistant/components/kaleidescape/translations/de.json create mode 100644 homeassistant/components/kaleidescape/translations/el.json create mode 100644 homeassistant/components/kaleidescape/translations/et.json create mode 100644 homeassistant/components/kaleidescape/translations/id.json create mode 100644 homeassistant/components/kaleidescape/translations/it.json create mode 100644 homeassistant/components/kaleidescape/translations/nl.json create mode 100644 homeassistant/components/kaleidescape/translations/pl.json create mode 100644 homeassistant/components/kaleidescape/translations/pt-BR.json create mode 100644 homeassistant/components/kaleidescape/translations/ru.json create mode 100644 homeassistant/components/kaleidescape/translations/tr.json create mode 100644 homeassistant/components/kaleidescape/translations/zh-Hant.json create mode 100644 homeassistant/components/season/translations/bg.json create mode 100644 homeassistant/components/season/translations/nl.json create mode 100644 homeassistant/components/season/translations/pl.json create mode 100644 homeassistant/components/season/translations/pt-BR.json create mode 100644 homeassistant/components/season/translations/ru.json create mode 100644 homeassistant/components/season/translations/tr.json create mode 100644 homeassistant/components/season/translations/zh-Hant.json diff --git a/homeassistant/components/airzone/translations/it.json b/homeassistant/components/airzone/translations/it.json new file mode 100644 index 00000000000..db377b36fcc --- /dev/null +++ b/homeassistant/components/airzone/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Imposta l'integrazione Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index c737a09216e..d28a170bcd0 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "media_player": { + "data": { + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" + } + } + } + }, "state": { "_": { "closed": "\u0417\u0430\u0442\u0432\u043e\u0440\u0435\u043d\u0430", diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index be9b55699b0..5bec96110c2 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Groep leden", + "name": "Groep naam" + }, + "description": "Selecteer groep opties" + }, + "cover_options": { + "data": { + "entities": "Groep leden" + }, + "description": "Selecteer groep opties" + }, + "fan": { + "data": { + "entities": "Groep leden", + "name": "Groep naam" + }, + "description": "Selecteer groep opties" + }, + "fan_options": { + "data": { + "entities": "Groep leden" + }, + "description": "Selecteer groep opties" + }, + "init": { + "data": { + "group_type": "Groep type" + }, + "description": "Selecteer groep type" + }, + "light": { + "data": { + "entities": "Groep leden", + "name": "Groep naam" + }, + "description": "Selecteer groep opties" + }, + "light_options": { + "data": { + "entities": "Groep leden" + }, + "description": "Selecteer groep opties" + }, + "media_player": { + "data": { + "entities": "Groep leden\n", + "name": "Groep naam" + }, + "description": "Selecteer groep opties" + }, + "media_player_options": { + "data": { + "entities": "Groep leden" + }, + "description": "Selecteer groep opties" + } + } + }, "state": { "_": { "closed": "Gesloten", diff --git a/homeassistant/components/group/translations/pt-BR.json b/homeassistant/components/group/translations/pt-BR.json index e0cbc7c02fd..bceaaa4fb43 100644 --- a/homeassistant/components/group/translations/pt-BR.json +++ b/homeassistant/components/group/translations/pt-BR.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Membros do grupo", + "name": "Nome do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "cover_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "fan": { + "data": { + "entities": "Membros do grupo", + "name": "Nome do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "fan_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "init": { + "data": { + "group_type": "Tipo de grupo" + }, + "description": "Selecione o tipo de grupo" + }, + "light": { + "data": { + "entities": "Membros do grupo", + "name": "Nome do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "light_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "media_player": { + "data": { + "entities": "Membros do grupo", + "name": "Nome do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "media_player_options": { + "data": { + "entities": "Membros do grupo" + }, + "description": "Selecione as op\u00e7\u00f5es do grupo" + } + } + }, "state": { "_": { "closed": "Fechado", diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json index b0a2fb76c9c..bbb4600a4ed 100644 --- a/homeassistant/components/group/translations/tr.json +++ b/homeassistant/components/group/translations/tr.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "cover_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "fan": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "fan_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "init": { + "data": { + "group_type": "Grup t\u00fcr\u00fc" + }, + "description": "Grup t\u00fcr\u00fcn\u00fc se\u00e7in" + }, + "light": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "light_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "media_player": { + "data": { + "entities": "Grup \u00fcyeleri", + "name": "Grup ismi" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "media_player_options": { + "data": { + "entities": "Grup \u00fcyeleri" + }, + "description": "Grup se\u00e7eneklerini se\u00e7in" + } + } + }, "state": { "_": { "closed": "Kapal\u0131", diff --git a/homeassistant/components/kaleidescape/translations/bg.json b/homeassistant/components/kaleidescape/translations/bg.json new file mode 100644 index 00000000000..cb36ec53c4b --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/ca.json b/homeassistant/components/kaleidescape/translations/ca.json new file mode 100644 index 00000000000..ec7eac6ef4f --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "unknown": "Error inesperat", + "unsupported": "Dispositiu no compatible" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unsupported": "Dispositiu no compatible" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Vols configurar el reproductor {name} model {model}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Configuraci\u00f3 de Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/de.json b/homeassistant/components/kaleidescape/translations/de.json new file mode 100644 index 00000000000..3b353208a64 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "unknown": "Unerwarteter Fehler", + "unsupported": "Nicht unterst\u00fctztes Ger\u00e4t" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unsupported": "Nicht unterst\u00fctztes Ger\u00e4t" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du den Player {model} mit dem Namen {name} einrichten?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape-Setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/el.json b/homeassistant/components/kaleidescape/translations/el.json new file mode 100644 index 00000000000..dc042655b02 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unsupported": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unsupported": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 {model} \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1{name};", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/et.json b/homeassistant/components/kaleidescape/translations/et.json new file mode 100644 index 00000000000..52ece37ad97 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine juba k\u00e4ib", + "unknown": "Ootamatu t\u00f5rge", + "unsupported": "Seadet ei toetata" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unsupported": "Seadet ei toetata" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Kas seadistada m\u00e4ngijat {model} nimega {name} ?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Kaleidescape'i seadistamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/id.json b/homeassistant/components/kaleidescape/translations/id.json new file mode 100644 index 00000000000..626f23354d4 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "unknown": "Kesalahan yang tidak diharapkan", + "unsupported": "Perangkat tidak didukung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unsupported": "Perangkat tidak didukung" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan pemutar {model} dengan nama {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Penyiapan Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/it.json b/homeassistant/components/kaleidescape/translations/it.json new file mode 100644 index 00000000000..022b73ae0d7 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "unknown": "Errore imprevisto", + "unsupported": "Dispositivo non supportato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unsupported": "Dispositivo non supportato" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Vuoi configurare il lettore {model} nome {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Configurazione di Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/nl.json b/homeassistant/components/kaleidescape/translations/nl.json new file mode 100644 index 00000000000..bc0facb9dde --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "unknown": "Onverwachte fout", + "unsupported": "Niet-ondersteund apparaat\n" + }, + "error": { + "cannot_connect": "Verbinding mislukt", + "unsupported": "Niet-ondersteund apparaat" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/pl.json b/homeassistant/components/kaleidescape/translations/pl.json new file mode 100644 index 00000000000..111dc4db7e6 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unsupported": "Nieobs\u0142ugiwane urz\u0105dzenie" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unsupported": "Nieobs\u0142ugiwane urz\u0105dzenie" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 odtwarzacz {model} o nazwie {name}?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "title": "Konfiguracja Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/pt-BR.json b/homeassistant/components/kaleidescape/translations/pt-BR.json new file mode 100644 index 00000000000..1be9ae58e2f --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "unknown": "Erro inesperado", + "unsupported": "Dispositivo n\u00e3o compat\u00edvel" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unsupported": "Dispositivo n\u00e3o compat\u00edvel" + }, + "flow_title": "{model} ( {name} )", + "step": { + "discovery_confirm": { + "description": "Deseja configurar o player {model} chamado {name} ?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Configura\u00e7\u00e3o do Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/ru.json b/homeassistant/components/kaleidescape/translations/ru.json new file mode 100644 index 00000000000..3112fdd06e5 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/ru.json @@ -0,0 +1,27 @@ +{ + "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.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unsupported": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({name})?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/tr.json b/homeassistant/components/kaleidescape/translations/tr.json new file mode 100644 index 00000000000..1b891cc450c --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "unknown": "Beklenmeyen hata", + "unsupported": "Desteklenmeyen cihaz" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unsupported": "Desteklenmeyen cihaz" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "{name} adl\u0131 {model} oynat\u0131c\u0131s\u0131n\u0131 kurmak istiyor musunuz?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Sunucu" + }, + "title": "Kaleidescape Kurulumu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/zh-Hant.json b/homeassistant/components/kaleidescape/translations/zh-Hant.json new file mode 100644 index 00000000000..2fedf1ede20 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unsupported": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unsupported": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u540d\u70ba {name} \u7684 {model} \u64ad\u653e\u5668\uff1f", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "Kaleidescape \u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/bg.json b/homeassistant/components/season/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/season/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json new file mode 100644 index 00000000000..933caddaf13 --- /dev/null +++ b/homeassistant/components/season/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/pl.json b/homeassistant/components/season/translations/pl.json new file mode 100644 index 00000000000..bef7f92841d --- /dev/null +++ b/homeassistant/components/season/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "type": "Typ definicji sezonu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/pt-BR.json b/homeassistant/components/season/translations/pt-BR.json new file mode 100644 index 00000000000..aa4f7601808 --- /dev/null +++ b/homeassistant/components/season/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "type": "Defini\u00e7\u00e3o do tipo de temporada" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/ru.json b/homeassistant/components/season/translations/ru.json new file mode 100644 index 00000000000..4a914a520e5 --- /dev/null +++ b/homeassistant/components/season/translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u0435\u0437\u043e\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/tr.json b/homeassistant/components/season/translations/tr.json new file mode 100644 index 00000000000..a625042d5dd --- /dev/null +++ b/homeassistant/components/season/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "type": "Sezon tan\u0131m\u0131 t\u00fcr\u00fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/zh-Hant.json b/homeassistant/components/season/translations/zh-Hant.json new file mode 100644 index 00000000000..4e954253198 --- /dev/null +++ b/homeassistant/components/season/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "type": "\u5b63\u7bc0\u985e\u578b\u5b9a\u7fa9" + } + } + } + } +} \ No newline at end of file From 2fc100926f21205eb39332871007279cd7fa075a Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Tue, 8 Mar 2022 22:16:07 -0500 Subject: [PATCH 0325/1054] Add button entities to Mazda integration (#67597) --- homeassistant/components/mazda/__init__.py | 14 +- homeassistant/components/mazda/button.py | 156 +++++++++++++++++++++ tests/components/mazda/test_button.py | 151 ++++++++++++++++++++ 3 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/mazda/button.py create mode 100644 tests/components/mazda/test_button.py diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 8e25e08dc47..38054bc653e 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -37,7 +37,7 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICE _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR] async def with_timeout(task, timeout_seconds=10): @@ -102,6 +102,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if vehicle_id == 0 or api_client is None: raise HomeAssistantError("Vehicle ID not found") + if service_call.service in ( + "start_engine", + "stop_engine", + "turn_on_hazard_lights", + "turn_off_hazard_lights", + ): + _LOGGER.warning( + "The mazda.%s service is deprecated and has been replaced by a button entity; " + "Please use the button entity instead", + service_call.service, + ) + api_method = getattr(api_client, service_call.service) try: if service_call.service == "send_poi": diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py new file mode 100644 index 00000000000..e747cb33dc2 --- /dev/null +++ b/homeassistant/components/mazda/button.py @@ -0,0 +1,156 @@ +"""Platform for Mazda button integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from pymazda import ( + Client as MazdaAPIClient, + MazdaAccountLockedException, + MazdaAPIEncryptionException, + MazdaAuthenticationException, + MazdaException, + MazdaLoginFailedException, + MazdaTokenExpiredException, +) + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +async def handle_button_press( + client: MazdaAPIClient, + key: str, + vehicle_id: int, + coordinator: DataUpdateCoordinator, +) -> None: + """Handle a press for a Mazda button entity.""" + api_method = getattr(client, key) + + try: + await api_method(vehicle_id) + except ( + MazdaException, + MazdaAuthenticationException, + MazdaAccountLockedException, + MazdaTokenExpiredException, + MazdaAPIEncryptionException, + MazdaLoginFailedException, + ) as ex: + raise HomeAssistantError(ex) from ex + + +async def handle_refresh_vehicle_status( + client: MazdaAPIClient, + key: str, + vehicle_id: int, + coordinator: DataUpdateCoordinator, +) -> None: + """Handle a request to refresh the vehicle status.""" + await handle_button_press(client, key, vehicle_id, coordinator) + + await coordinator.async_request_refresh() + + +@dataclass +class MazdaButtonRequiredKeysMixin: + """Mixin for required keys.""" + + # Suffix to be appended to the vehicle name to obtain the button name + name_suffix: str + + +@dataclass +class MazdaButtonEntityDescription( + ButtonEntityDescription, MazdaButtonRequiredKeysMixin +): + """Describes a Mazda button entity.""" + + # Function to determine whether the vehicle supports this button, given the coordinator data + is_supported: Callable[[dict[str, Any]], bool] = lambda data: True + + async_press: Callable[ + [MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable + ] = handle_button_press + + +BUTTON_ENTITIES = [ + MazdaButtonEntityDescription( + key="start_engine", + name_suffix="Start Engine", + icon="mdi:engine", + ), + MazdaButtonEntityDescription( + key="stop_engine", + name_suffix="Stop Engine", + icon="mdi:engine-off", + ), + MazdaButtonEntityDescription( + key="turn_on_hazard_lights", + name_suffix="Turn On Hazard Lights", + icon="mdi:hazard-lights", + ), + MazdaButtonEntityDescription( + key="turn_off_hazard_lights", + name_suffix="Turn Off Hazard Lights", + icon="mdi:hazard-lights", + ), + MazdaButtonEntityDescription( + key="refresh_vehicle_status", + name_suffix="Refresh Status", + icon="mdi:refresh", + async_press=handle_refresh_vehicle_status, + is_supported=lambda data: data["isElectric"], + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the button platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + MazdaButtonEntity(client, coordinator, index, description) + for index, data in enumerate(coordinator.data) + for description in BUTTON_ENTITIES + if description.is_supported(data) + ) + + +class MazdaButtonEntity(MazdaEntity, ButtonEntity): + """Representation of a Mazda button.""" + + entity_description: MazdaButtonEntityDescription + + def __init__( + self, + client: MazdaAPIClient, + coordinator: DataUpdateCoordinator, + index: int, + description: MazdaButtonEntityDescription, + ) -> None: + """Initialize Mazda button.""" + super().__init__(client, coordinator, index) + self.entity_description = description + + self._attr_name = f"{self.vehicle_name} {description.name_suffix}" + self._attr_unique_id = f"{self.vin}_{description.key}" + + async def async_press(self) -> None: + """Press the button.""" + await self.entity_description.async_press( + self.client, self.entity_description.key, self.vehicle_id, self.coordinator + ) diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py new file mode 100644 index 00000000000..cb9fdb40737 --- /dev/null +++ b/tests/components/mazda/test_button.py @@ -0,0 +1,151 @@ +"""The button tests for the Mazda Connected Services integration.""" + +from pymazda import MazdaException +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.components.mazda import init_integration + + +async def test_button_setup_non_electric_vehicle(hass) -> None: + """Test creation of button entities.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("button.my_mazda3_start_engine") + assert entry + assert entry.unique_id == "JM000000000000000_start_engine" + state = hass.states.get("button.my_mazda3_start_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine" + + entry = entity_registry.async_get("button.my_mazda3_stop_engine") + assert entry + assert entry.unique_id == "JM000000000000000_stop_engine" + state = hass.states.get("button.my_mazda3_stop_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" + + entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + # Since this is a non-electric vehicle, electric vehicle buttons should not be created + entry = entity_registry.async_get("button.my_mazda3_refresh_vehicle_status") + assert entry is None + state = hass.states.get("button.my_mazda3_refresh_vehicle_status") + assert state is None + + +async def test_button_setup_electric_vehicle(hass) -> None: + """Test creation of button entities for an electric vehicle.""" + await init_integration(hass, electric_vehicle=True) + + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("button.my_mazda3_start_engine") + assert entry + assert entry.unique_id == "JM000000000000000_start_engine" + state = hass.states.get("button.my_mazda3_start_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine" + + entry = entity_registry.async_get("button.my_mazda3_stop_engine") + assert entry + assert entry.unique_id == "JM000000000000000_stop_engine" + state = hass.states.get("button.my_mazda3_stop_engine") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" + + entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") + assert entry + assert entry.unique_id == "JM000000000000000_turn_off_hazard_lights" + state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") + assert state + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" + + entry = entity_registry.async_get("button.my_mazda3_refresh_status") + assert entry + assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" + state = hass.states.get("button.my_mazda3_refresh_status") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh Status" + assert state.attributes.get(ATTR_ICON) == "mdi:refresh" + + +@pytest.mark.parametrize( + "entity_id_suffix, api_method_name", + [ + ("start_engine", "start_engine"), + ("stop_engine", "stop_engine"), + ("turn_on_hazard_lights", "turn_on_hazard_lights"), + ("turn_off_hazard_lights", "turn_off_hazard_lights"), + ("refresh_status", "refresh_vehicle_status"), + ], +) +async def test_button_press(hass, entity_id_suffix, api_method_name) -> None: + """Test pressing the button entities.""" + client_mock = await init_integration(hass, electric_vehicle=True) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: f"button.my_mazda3_{entity_id_suffix}"}, + blocking=True, + ) + await hass.async_block_till_done() + + api_method = getattr(client_mock, api_method_name) + api_method.assert_called_once_with(12345) + + +async def test_button_press_error(hass) -> None: + """Test the Mazda API raising an error when a button entity is pressed.""" + client_mock = await init_integration(hass) + + client_mock.start_engine.side_effect = MazdaException("Test error") + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.my_mazda3_start_engine"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert str(err.value) == "Test error" From 36385396b09a14e3f179f74454cb883107dd4564 Mon Sep 17 00:00:00 2001 From: cheng2wei <99464193+cheng2wei@users.noreply.github.com> Date: Tue, 8 Mar 2022 20:01:00 -0800 Subject: [PATCH 0326/1054] Fix discord embed class initialization (#67831) --- homeassistant/components/discord/notify.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 41137e1a32c..2e13b68225c 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -20,9 +20,13 @@ _LOGGER = logging.getLogger(__name__) ATTR_EMBED = "embed" ATTR_EMBED_AUTHOR = "author" +ATTR_EMBED_COLOR = "color" +ATTR_EMBED_DESCRIPTION = "description" ATTR_EMBED_FIELDS = "fields" ATTR_EMBED_FOOTER = "footer" +ATTR_EMBED_TITLE = "title" ATTR_EMBED_THUMBNAIL = "thumbnail" +ATTR_EMBED_URL = "url" ATTR_IMAGES = "images" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) @@ -64,10 +68,16 @@ class DiscordNotificationService(BaseNotificationService): embeds: list[nextcord.Embed] = [] if ATTR_EMBED in data: embedding = data[ATTR_EMBED] + title = embedding.get(ATTR_EMBED_TITLE) or nextcord.Embed.Empty + description = embedding.get(ATTR_EMBED_DESCRIPTION) or nextcord.Embed.Empty + color = embedding.get(ATTR_EMBED_COLOR) or nextcord.Embed.Empty + url = embedding.get(ATTR_EMBED_URL) or nextcord.Embed.Empty fields = embedding.get(ATTR_EMBED_FIELDS) or [] if embedding: - embed = nextcord.Embed(**embedding) + embed = nextcord.Embed( + title=title, description=description, color=color, url=url + ) for field in fields: embed.add_field(**field) if ATTR_EMBED_FOOTER in embedding: From c6952a0ee3234769fbbb796b840dd04e88fe58ff Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Wed, 9 Mar 2022 11:09:29 +0300 Subject: [PATCH 0327/1054] Add Siren to Z-Wave.Me integration (#67200) * Siren integration * Clean up Co-authored-by: Dmitry Vlasov Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/zwave_me/const.py | 2 + .../components/zwave_me/manifest.json | 2 +- homeassistant/components/zwave_me/siren.py | 49 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/zwave_me/siren.py diff --git a/.coveragerc b/.coveragerc index ce11e4822a8..b892dd1f25d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1478,6 +1478,7 @@ omit = homeassistant/components/zwave_me/lock.py homeassistant/components/zwave_me/number.py homeassistant/components/zwave_me/sensor.py + homeassistant/components/zwave_me/siren.py homeassistant/components/zwave_me/switch.py [report] diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py index ccbf6989f07..87d740f1ece 100644 --- a/homeassistant/components/zwave_me/const.py +++ b/homeassistant/components/zwave_me/const.py @@ -16,6 +16,7 @@ class ZWaveMePlatform(StrEnum): NUMBER = "switchMultilevel" SWITCH = "switchBinary" SENSOR = "sensorMultilevel" + SIREN = "siren" RGBW_LIGHT = "switchRGBW" RGB_LIGHT = "switchRGB" @@ -28,5 +29,6 @@ PLATFORMS = [ Platform.LOCK, Platform.NUMBER, Platform.SENSOR, + Platform.SIREN, Platform.SWITCH, ] diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 8863cd6ebf7..2e6efd9576b 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", "requirements": [ - "zwave_me_ws==0.2.1", + "zwave_me_ws==0.2.2", "url-normalize==1.4.1" ], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/zwave_me/siren.py b/homeassistant/components/zwave_me/siren.py new file mode 100644 index 00000000000..c528f610132 --- /dev/null +++ b/homeassistant/components/zwave_me/siren.py @@ -0,0 +1,49 @@ +"""Representation of a sirenBinary.""" +from typing import Any + +from homeassistant.components.siren import SirenEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.SIREN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the siren platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + siren = ZWaveMeSiren(controller, new_device) + + async_add_entities( + [ + siren, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeSiren(ZWaveMeEntity, SirenEntity): + """Representation of a ZWaveMe siren.""" + + @property + def is_on(self) -> bool: + """Return the state of the siren.""" + return self.device.level == "on" + + def turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self.controller.zwave_api.send_command(self.device.id, "on") + + def turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self.controller.zwave_api.send_command(self.device.id, "off") diff --git a/requirements_all.txt b/requirements_all.txt index e318f61ccfe..01b81d8fabc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2506,4 +2506,4 @@ zm-py==0.5.2 zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me -zwave_me_ws==0.2.1 +zwave_me_ws==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1aa2224391..fdde0853f51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,4 +1599,4 @@ zigpy==0.43.0 zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me -zwave_me_ws==0.2.1 +zwave_me_ws==0.2.2 From 723dcbafca45267bf0d57016fc82e61beb69969f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Mar 2022 10:38:12 +0100 Subject: [PATCH 0328/1054] Complete fan speed transition from #59781 (#67743) --- homeassistant/components/balboa/const.py | 2 - homeassistant/components/bond/fan.py | 8 +- homeassistant/components/comfoconnect/fan.py | 1 - homeassistant/components/deconz/fan.py | 81 +-------- homeassistant/components/demo/fan.py | 2 - .../components/emulated_hue/hue_api.py | 30 +--- homeassistant/components/esphome/fan.py | 1 - homeassistant/components/fan/__init__.py | 160 ++--------------- .../components/fan/reproduce_state.py | 3 - homeassistant/components/fjaraskupan/fan.py | 1 - homeassistant/components/freedompro/fan.py | 4 +- homeassistant/components/group/fan.py | 1 - .../components/homekit_controller/fan.py | 1 - homeassistant/components/insteon/fan.py | 1 - homeassistant/components/isy994/fan.py | 2 - homeassistant/components/knx/fan.py | 1 - homeassistant/components/lutron_caseta/fan.py | 1 - homeassistant/components/modbus/fan.py | 1 - homeassistant/components/modern_forms/fan.py | 1 - homeassistant/components/mqtt/fan.py | 17 +- homeassistant/components/ozw/fan.py | 4 +- homeassistant/components/senseme/fan.py | 1 - homeassistant/components/smartthings/fan.py | 1 - homeassistant/components/smarty/fan.py | 4 +- homeassistant/components/tasmota/fan.py | 1 - homeassistant/components/template/fan.py | 5 - homeassistant/components/tolo/fan.py | 1 - homeassistant/components/tradfri/fan.py | 1 - homeassistant/components/tuya/fan.py | 1 - homeassistant/components/vallox/fan.py | 1 - homeassistant/components/vesync/fan.py | 1 - homeassistant/components/wemo/fan.py | 1 - homeassistant/components/wilight/fan.py | 1 - homeassistant/components/xiaomi_miio/fan.py | 1 - homeassistant/components/zha/fan.py | 4 +- homeassistant/components/zwave/fan.py | 2 +- homeassistant/components/zwave_js/fan.py | 1 - tests/components/bond/test_fan.py | 51 ++---- tests/components/deconz/test_fan.py | 77 ++++---- tests/components/demo/test_fan.py | 139 ++++----------- tests/components/emulated_hue/test_hue_api.py | 19 +- tests/components/emulated_kasa/test_init.py | 29 +-- tests/components/fan/common.py | 15 -- tests/components/fan/test_init.py | 6 +- tests/components/fan/test_reproduce_state.py | 20 ++- .../components/google_assistant/test_trait.py | 4 +- .../specific_devices/test_haa_fan.py | 1 - .../test_homeassistant_bridge.py | 1 - .../test_simpleconnect_fan.py | 1 - .../components/homekit_controller/test_fan.py | 52 +++--- tests/components/melissa/test_climate.py | 30 ++-- tests/components/mqtt/test_fan.py | 50 +++--- tests/components/ozw/test_fan.py | 11 -- tests/components/smartthings/test_fan.py | 27 +-- tests/components/template/test_fan.py | 165 ++++++------------ tests/components/zha/test_fan.py | 43 ++--- tests/components/zwave/test_fan.py | 39 +---- tests/components/zwave_js/test_fan.py | 20 +-- 58 files changed, 303 insertions(+), 847 deletions(-) diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index f5b28804952..9fb0b34d003 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -10,7 +10,6 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_OFF from homeassistant.const import Platform _LOGGER = logging.getLogger(__name__) @@ -21,7 +20,6 @@ CLIMATE_SUPPORTED_FANSTATES = [FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH] CLIMATE_SUPPORTED_MODES = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] CONF_SYNC_TIME = "sync_time" DEFAULT_SYNC_TIME = False -FAN_SUPPORTED_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_HIGH] PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE] AUX = "Aux" diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 32edfb206a6..a2e35456ca3 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -10,7 +10,6 @@ from bond_api import Action, BPUPSubscriptions, DeviceType, Direction import voluptuous as vol from homeassistant.components.fan import ( - ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, SUPPORT_DIRECTION, @@ -57,7 +56,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {vol.Required(ATTR_SPEED): vol.All(vol.Number(scale=0), vol.Range(0, 100))}, + {vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))}, "async_set_speed_belief", ) @@ -107,7 +106,9 @@ class BondFan(BondEntity, FanEntity): """Return the current speed percentage for the fan.""" if not self._speed or not self._power: return 0 - return ranged_value_to_percentage(self._speed_range, self._speed) + return min( + 100, max(0, ranged_value_to_percentage(self._speed_range, self._speed)) + ) @property def speed_count(self) -> int: @@ -183,7 +184,6 @@ class BondFan(BondEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 278de0557b9..b2845068dbe 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -120,7 +120,6 @@ class ComfoConnectFan(FanEntity): def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index d1ff85f9d65..222bf51470f 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -13,15 +13,7 @@ from pydeconz.light import ( Fan, ) -from homeassistant.components.fan import ( - DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -41,19 +33,6 @@ ORDERED_NAMED_FAN_SPEEDS = [ FAN_SPEED_100_PERCENT, ] -LEGACY_SPEED_TO_DECONZ = { - SPEED_OFF: FAN_SPEED_OFF, - SPEED_LOW: FAN_SPEED_25_PERCENT, - SPEED_MEDIUM: FAN_SPEED_50_PERCENT, - SPEED_HIGH: FAN_SPEED_100_PERCENT, -} -LEGACY_DECONZ_TO_SPEED = { - FAN_SPEED_OFF: SPEED_OFF, - FAN_SPEED_25_PERCENT: SPEED_LOW, - FAN_SPEED_50_PERCENT: SPEED_MEDIUM, - FAN_SPEED_100_PERCENT: SPEED_HIGH, -} - async def async_setup_entry( hass: HomeAssistant, @@ -130,41 +109,6 @@ class DeconzFan(DeconzDevice, FanEntity): """Return the number of speeds the fan supports.""" return len(ORDERED_NAMED_FAN_SPEEDS) - @property - def speed_list(self) -> list: - """Get the list of available speeds. - - Legacy fan support. - """ - return list(LEGACY_SPEED_TO_DECONZ) - - def speed_to_percentage(self, speed: str) -> int: - """Convert speed to percentage. - - Legacy fan support. - """ - if speed == SPEED_OFF: - return 0 - - if speed not in LEGACY_SPEED_TO_DECONZ: - speed = SPEED_MEDIUM - - return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, LEGACY_SPEED_TO_DECONZ[speed] - ) - - def percentage_to_speed(self, percentage: int) -> str: - """Convert percentage to speed. - - Legacy fan support. - """ - if percentage == 0: - return SPEED_OFF - return LEGACY_DECONZ_TO_SPEED.get( - percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage), - SPEED_MEDIUM, - ) - @callback def async_update_callback(self) -> None: """Store latest configured speed from the device.""" @@ -174,36 +118,23 @@ class DeconzFan(DeconzDevice, FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" + if percentage == 0: + return await self.async_turn_off() await self._device.set_speed( percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) ) - async def async_set_speed(self, speed: str) -> None: - """Set the speed of the fan. - - Legacy fan support. - """ - if speed not in LEGACY_SPEED_TO_DECONZ: - raise ValueError(f"Unsupported speed {speed}") - - await self._device.set_speed(LEGACY_SPEED_TO_DECONZ[speed]) - async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, ) -> None: """Turn on fan.""" - new_speed = self._default_on_speed - if percentage is not None: - new_speed = percentage_to_ordered_list_item( - ORDERED_NAMED_FAN_SPEEDS, percentage - ) - - await self._device.set_speed(new_speed) + await self.async_set_percentage(percentage) + return + await self._device.set_speed(self._default_on_speed) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off fan.""" diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index e70f5efc626..8fcc6a810ed 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -196,7 +196,6 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): def turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -267,7 +266,6 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 210e95f13b7..6f8cf77b13d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -26,14 +26,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_SET_POSITION, ) -from homeassistant.components.fan import ( - ATTR_SPEED, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, -) +from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier.const import ( ATTR_HUMIDITY, @@ -540,14 +533,7 @@ class HueOneLightChangeView(HomeAssistantView): ): domain = entity.domain # Convert 0-100 to a fan speed - if (brightness := parsed[STATE_BRIGHTNESS]) == 0: - data[ATTR_SPEED] = SPEED_OFF - elif 0 < brightness <= 33.3: - data[ATTR_SPEED] = SPEED_LOW - elif 33.3 < brightness <= 66.6: - data[ATTR_SPEED] = SPEED_MEDIUM - elif 66.6 < brightness <= 100: - data[ATTR_SPEED] = SPEED_HIGH + data[ATTR_PERCENTAGE] = parsed[STATE_BRIGHTNESS] # Map the off command to on if entity.domain in config.off_maps_to_on_domains: @@ -679,15 +665,9 @@ def get_entity_state(config, entity): # Convert 0.0-1.0 to 0-254 data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) elif entity.domain == fan.DOMAIN: - speed = entity.attributes.get(ATTR_SPEED, 0) - # Convert 0.0-1.0 to 0-254 - data[STATE_BRIGHTNESS] = 0 - if speed == SPEED_LOW: - data[STATE_BRIGHTNESS] = 85 - elif speed == SPEED_MEDIUM: - data[STATE_BRIGHTNESS] = 170 - elif speed == SPEED_HIGH: - data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX + percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0 + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100) elif entity.domain == cover.DOMAIN: level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 5ae4a47c52e..91cb8bb6fdb 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -92,7 +92,6 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 87f50c7d938..5ed4420c592 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -28,8 +28,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.percentage import ( - ordered_list_item_to_percentage, - percentage_to_ordered_list_item, percentage_to_ranged_value, ranged_value_to_percentage, ) @@ -47,7 +45,6 @@ SUPPORT_OSCILLATE = 2 SUPPORT_DIRECTION = 4 SUPPORT_PRESET_MODE = 8 -SERVICE_SET_SPEED = "set_speed" SERVICE_INCREASE_SPEED = "increase_speed" SERVICE_DECREASE_SPEED = "decrease_speed" SERVICE_OSCILLATE = "oscillate" @@ -55,37 +52,16 @@ SERVICE_SET_DIRECTION = "set_direction" SERVICE_SET_PERCENTAGE = "set_percentage" SERVICE_SET_PRESET_MODE = "set_preset_mode" -SPEED_OFF = "off" -SPEED_LOW = "low" -SPEED_MEDIUM = "medium" -SPEED_HIGH = "high" - DIRECTION_FORWARD = "forward" DIRECTION_REVERSE = "reverse" -ATTR_SPEED = "speed" ATTR_PERCENTAGE = "percentage" ATTR_PERCENTAGE_STEP = "percentage_step" -ATTR_SPEED_LIST = "speed_list" ATTR_OSCILLATING = "oscillating" ATTR_DIRECTION = "direction" ATTR_PRESET_MODE = "preset_mode" ATTR_PRESET_MODES = "preset_modes" -_NOT_SPEED_OFF = "off" - -OFF_SPEED_VALUES = [SPEED_OFF, None] - -LEGACY_SPEED_LIST = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - -class NoValidSpeedsError(ValueError): - """Exception class when there are no valid speeds.""" - - -class NotValidSpeedError(ValueError): - """Exception class when the speed in not in the speed list.""" - class NotValidPresetModeError(ValueError): """Exception class when the preset_mode in not in the preset_modes list.""" @@ -94,10 +70,7 @@ class NotValidPresetModeError(ValueError): @bind_hass def is_on(hass, entity_id: str) -> bool: """Return if the fans are on based on the statemachine.""" - state = hass.states.get(entity_id) - if ATTR_SPEED in state.attributes: - return state.attributes[ATTR_SPEED] not in OFF_SPEED_VALUES - return state.state == STATE_ON + return hass.states.get(entity_id).state == STATE_ON async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -113,24 +86,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_TURN_ON, { - vol.Optional(ATTR_SPEED): cv.string, vol.Optional(ATTR_PERCENTAGE): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), vol.Optional(ATTR_PRESET_MODE): cv.string, }, - "async_turn_on_compat", + "async_turn_on", ) component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - # After the transition to percentage and preset_modes concludes, - # remove this service - component.async_register_entity_service( - SERVICE_SET_SPEED, - {vol.Required(ATTR_SPEED): cv.string}, - "async_set_speed_deprecated", - [SUPPORT_SET_SPEED], - ) component.async_register_entity_service( SERVICE_INCREASE_SPEED, { @@ -212,29 +176,6 @@ class FanEntity(ToggleEntity): _attr_speed_count: int _attr_supported_features: int = 0 - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - raise NotImplementedError() - - async def async_set_speed_deprecated(self, speed: str): - """Set the speed of the fan.""" - _LOGGER.error( - "The fan.set_speed service is deprecated and will fail in 2022.3 and later, use fan.set_percentage or fan.set_preset_mode instead" - ) - await self.async_set_speed(speed) - - async def async_set_speed(self, speed: str): - """Set the speed of the fan.""" - if speed == SPEED_OFF: - await self.async_turn_off() - return - - if self.preset_modes and speed in self.preset_modes: - await self.async_set_preset_mode(speed) - return - - await self.async_set_percentage(self.speed_to_percentage(speed)) - def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" raise NotImplementedError() @@ -301,7 +242,6 @@ class FanEntity(ToggleEntity): # pylint: disable=arguments-differ def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, @@ -309,64 +249,22 @@ class FanEntity(ToggleEntity): """Turn on the fan.""" raise NotImplementedError() - async def async_turn_on_compat( - self, - speed: str | None = None, - percentage: int | None = None, - preset_mode: str | None = None, - **kwargs, - ) -> None: - """Turn on the fan. - - This _compat version wraps async_turn_on with - backwards and forward compatibility. - - This compatibility shim will be removed in 2022.3 - """ - if preset_mode is not None: - self._valid_preset_mode_or_raise(preset_mode) - speed = preset_mode - percentage = None - elif speed is not None: - _LOGGER.error( - "Calling fan.turn_on with the speed argument is deprecated and will fail in 2022.3 and later, use percentage or preset_mode instead" - ) - if self.preset_modes and speed in self.preset_modes: - preset_mode = speed - percentage = None - else: - percentage = self.speed_to_percentage(speed) - elif percentage is not None: - speed = self.percentage_to_speed(percentage) - - await self.async_turn_on( - speed=speed, - percentage=percentage, - preset_mode=preset_mode, - **kwargs, - ) - # pylint: disable=arguments-differ async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, ) -> None: """Turn on the fan.""" - if speed == SPEED_OFF: - await self.async_turn_off() - else: - await self.hass.async_add_executor_job( - ft.partial( - self.turn_on, - speed=speed, - percentage=percentage, - preset_mode=preset_mode, - **kwargs, - ) + await self.hass.async_add_executor_job( + ft.partial( + self.turn_on, + percentage=percentage, + preset_mode=preset_mode, + **kwargs, ) + ) def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" @@ -379,16 +277,9 @@ class FanEntity(ToggleEntity): @property def is_on(self): """Return true if the entity is on.""" - return self.speed not in [SPEED_OFF, None] - - @property - def speed(self) -> str | None: - """Return the current speed.""" - if preset_mode := self.preset_mode: - return preset_mode - if (percentage := self.percentage) is None: - return None - return self.percentage_to_speed(percentage) + return ( + self.percentage is not None and self.percentage > 0 + ) or self.preset_mode is not None @property def percentage(self) -> int | None: @@ -409,14 +300,6 @@ class FanEntity(ToggleEntity): """Return the step size for percentage.""" return 100 / self.speed_count - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - speeds = [SPEED_OFF, *LEGACY_SPEED_LIST] - if preset_modes := self.preset_modes: - speeds.extend(preset_modes) - return speeds - @property def current_direction(self) -> str | None: """Return the current direction of the fan.""" @@ -431,8 +314,6 @@ class FanEntity(ToggleEntity): def capability_attributes(self): """Return capability attributes.""" attrs = {} - if self.supported_features & SUPPORT_SET_SPEED: - attrs[ATTR_SPEED_LIST] = self.speed_list if ( self.supported_features & SUPPORT_SET_SPEED @@ -442,22 +323,6 @@ class FanEntity(ToggleEntity): return attrs - def speed_to_percentage(self, speed: str) -> int: # pylint: disable=no-self-use - """Map a legacy speed to a percentage.""" - if speed in OFF_SPEED_VALUES: - return 0 - if speed not in LEGACY_SPEED_LIST: - raise NotValidSpeedError(f"The speed {speed} is not a valid speed.") - return ordered_list_item_to_percentage(LEGACY_SPEED_LIST, speed) - - def percentage_to_speed( # pylint: disable=no-self-use - self, percentage: int - ) -> str: - """Map a percentage to a legacy speed.""" - if percentage == 0: - return SPEED_OFF - return percentage_to_ordered_list_item(LEGACY_SPEED_LIST, percentage) - @final @property def state_attributes(self) -> dict: @@ -472,7 +337,6 @@ class FanEntity(ToggleEntity): data[ATTR_OSCILLATING] = self.oscillating if supported_features & SUPPORT_SET_SPEED: - data[ATTR_SPEED] = self.speed data[ATTR_PERCENTAGE] = self.percentage data[ATTR_PERCENTAGE_STEP] = self.percentage_step diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 140fdfe9178..be018aa4b54 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -20,13 +20,11 @@ from . import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - ATTR_SPEED, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, ) _LOGGER = logging.getLogger(__name__) @@ -35,7 +33,6 @@ VALID_STATES = {STATE_ON, STATE_OFF} ATTRIBUTES = { # attribute: service ATTR_DIRECTION: SERVICE_SET_DIRECTION, ATTR_OSCILLATING: SERVICE_OSCILLATE, - ATTR_SPEED: SERVICE_SET_SPEED, ATTR_PERCENTAGE: SERVICE_SET_PERCENTAGE, ATTR_PRESET_MODE: SERVICE_SET_PRESET_MODE, } diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index bbc04a9607c..ea4327fc4b6 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -100,7 +100,6 @@ class Fan(CoordinatorEntity[State], FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index fe8ccc6d28d..dc7e3bb3d9d 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -92,9 +92,7 @@ class FreedomproFan(CoordinatorEntity, FanEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ): + async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs): """Async function to turn on the fan.""" payload = {"on": True} payload = json.dumps(payload) diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index f26b1bf57fb..a5f7cff127f 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -208,7 +208,6 @@ class FanGroup(GroupEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 71d71ce469f..3f48d34559b 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -117,7 +117,6 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 9df11fdcc79..bea17ccaa7e 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -65,7 +65,6 @@ class InsteonFanEntity(InsteonEntity, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index bf4d48ad3e8..c215eebd382 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -76,7 +76,6 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, @@ -121,7 +120,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 38c90aa149d..f5255fd25b0 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -106,7 +106,6 @@ class KNXFan(KnxEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 52fa1be65ed..cb00fa6f2c8 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -72,7 +72,6 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index afa26ac1638..6e2bf101de2 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -39,7 +39,6 @@ class ModbusFan(BaseSwitch, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 7c1da064b18..8ebb706096a 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -129,7 +129,6 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): @modernforms_exception_handler async def async_turn_on( self, - speed: int | None = None, percentage: int | None = None, preset_mode: int | None = None, **kwargs: Any, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index bedc3467c3b..89ea7dbdd4b 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -12,10 +12,6 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, @@ -163,10 +159,6 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( vol.Optional( CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET ): cv.string, - vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, - vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, - vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, - vol.Optional(CONF_PAYLOAD_OFF_SPEED, default=SPEED_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional( @@ -176,10 +168,6 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD ): cv.string, vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_SPEED_LIST, - default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], - ): cv.ensure_list, vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, @@ -537,7 +525,6 @@ class MqttFan(MqttEntity, FanEntity): # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -605,9 +592,7 @@ class MqttFan(MqttEntity, FanEntity): This method is a coroutine. """ - if preset_mode not in self.preset_modes: - _LOGGER.warning("'%s'is not a valid preset mode", preset_mode) - return + self._valid_preset_mode_or_raise(preset_mode) mqtt_payload = self._command_templates[ATTR_PRESET_MODE](preset_mode) diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py index 8f6374cc8e5..fdfbdadc2ee 100644 --- a/homeassistant/components/ozw/fan.py +++ b/homeassistant/components/ozw/fan.py @@ -55,9 +55,7 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) self.values.primary.send_value(zwave_speed) - async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ): + async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs): """Turn the device on.""" await self.async_set_percentage(percentage) diff --git a/homeassistant/components/senseme/fan.py b/homeassistant/components/senseme/fan.py index b274941b378..8dd31343058 100644 --- a/homeassistant/components/senseme/fan.py +++ b/homeassistant/components/senseme/fan.py @@ -84,7 +84,6 @@ class HASensemeFan(SensemeEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 81171ecf554..019cd5dc7b6 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -65,7 +65,6 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs, diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 0fadaf667a1..98df91acbc3 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -99,9 +99,9 @@ class SmartyFan(FanEntity): self._smarty_fan_speed = fan_speed self.schedule_update_ha_state() - def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): + def turn_on(self, percentage=None, preset_mode=None, **kwargs): """Turn on the fan.""" - _LOGGER.debug("Turning on fan. Speed is %s", speed) + _LOGGER.debug("Turning on fan. percentage is %s", percentage) self.set_percentage(percentage or DEFAULT_ON_PERCENTAGE) def turn_off(self, **kwargs): diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 6aabfb05091..a0002ff85f8 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -113,7 +113,6 @@ class TasmotaFan( async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 1d25c24017f..b172e94016f 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -11,7 +11,6 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, ENTITY_ID_FORMAT, @@ -250,7 +249,6 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, @@ -258,7 +256,6 @@ class TemplateFan(TemplateEntity, FanEntity): """Turn on the fan.""" await self._on_script.async_run( { - ATTR_SPEED: speed, ATTR_PERCENTAGE: percentage, ATTR_PRESET_MODE: preset_mode, }, @@ -270,8 +267,6 @@ class TemplateFan(TemplateEntity, FanEntity): await self.async_set_preset_mode(preset_mode) elif percentage is not None: await self.async_set_percentage(percentage) - elif speed is not None: - await self.async_set_speed(speed) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" diff --git a/homeassistant/components/tolo/fan.py b/homeassistant/components/tolo/fan.py index 499a348bd0b..e767be9a3ce 100644 --- a/homeassistant/components/tolo/fan.py +++ b/homeassistant/components/tolo/fan.py @@ -43,7 +43,6 @@ class ToloFan(ToloSaunaCoordinatorEntity, FanEntity): def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py index 36e1c8b08ad..ed478025405 100644 --- a/homeassistant/components/tradfri/fan.py +++ b/homeassistant/components/tradfri/fan.py @@ -148,7 +148,6 @@ class TradfriAirPurifierFan(TradfriBaseEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index e2e98e3fd5c..be05acef3de 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -161,7 +161,6 @@ class TuyaFanEntity(TuyaEntity, FanEntity): def turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs: Any, diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 59068d4819e..21e89f49d41 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -168,7 +168,6 @@ class ValloxFan(CoordinatorEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 2dee94f58e6..ea1fe94deb0 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -179,7 +179,6 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): def turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index be78dd1752d..253ff34213e 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -138,7 +138,6 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity): def turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index e3f3ab055f9..fbf5b0858a7 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -108,7 +108,6 @@ class WiLightFan(WiLightDevice, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e4b79ed77a8..3ad9ebb2f7d 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -326,7 +326,6 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): async def async_turn_on( self, - speed: str = None, percentage: int = None, preset_mode: str = None, **kwargs, diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 10b64caf974..e6f28935d10 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -91,9 +91,7 @@ class BaseFan(FanEntity): """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) - async def async_turn_on( - self, speed=None, percentage=None, preset_mode=None, **kwargs - ) -> None: + async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs) -> None: """Turn the entity on.""" if percentage is None: percentage = DEFAULT_ON_PERCENTAGE diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index b368e829eb7..ba758844c69 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -62,7 +62,7 @@ class ZwaveFan(ZWaveDeviceEntity, FanEntity): zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) self.node.set_dimmer(self.values.primary.value_id, zwave_speed) - def turn_on(self, speed=None, percentage=None, preset_mode=None, **kwargs): + def turn_on(self, percentage=None, preset_mode=None, **kwargs): """Turn the device on.""" self.set_percentage(percentage) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index cafab8b84a4..0291e6d3c64 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -83,7 +83,6 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): async def async_turn_on( self, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, **kwargs: Any, diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 4168cbd35d2..061e94595bf 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -16,17 +16,15 @@ from homeassistant.components.bond.const import ( from homeassistant.components.bond.fan import PRESET_MODE_BREEZE from homeassistant.components.fan import ( ATTR_DIRECTION, + ATTR_PERCENTAGE, ATTR_PRESET_MODE, ATTR_PRESET_MODES, - ATTR_SPEED, - ATTR_SPEED_LIST, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, - SPEED_OFF, ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.exceptions import HomeAssistantError @@ -66,7 +64,6 @@ def ceiling_fan_with_breeze(name: str): async def turn_fan_on( hass: core.HomeAssistant, fan_id: str, - speed: str | None = None, percentage: int | None = None, preset_mode: str | None = None, ) -> None: @@ -74,9 +71,7 @@ async def turn_fan_on( service_data = {ATTR_ENTITY_ID: fan_id} if preset_mode: service_data[fan.ATTR_PRESET_MODE] = preset_mode - if speed: - service_data[fan.ATTR_SPEED] = speed - if percentage: + if percentage is not None: service_data[fan.ATTR_PERCENTAGE] = percentage await hass.services.async_call( FAN_DOMAIN, @@ -116,35 +111,27 @@ async def test_non_standard_speed_list(hass: core.HomeAssistant): props={"max_speed": 6}, ) - actual_speeds = hass.states.get("fan.name_1").attributes[ATTR_SPEED_LIST] - assert actual_speeds == [ - fan.SPEED_OFF, - fan.SPEED_LOW, - fan.SPEED_MEDIUM, - fan.SPEED_HIGH, - ] - with patch_bond_device_state(): with patch_bond_action() as mock_set_speed_low: - await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) + await turn_fan_on(hass, "fan.name_1", percentage=100 / 6 * 2) mock_set_speed_low.assert_called_once_with( "test-device-id", Action.set_speed(2) ) with patch_bond_action() as mock_set_speed_medium: - await turn_fan_on(hass, "fan.name_1", fan.SPEED_MEDIUM) + await turn_fan_on(hass, "fan.name_1", percentage=100 / 6 * 4) mock_set_speed_medium.assert_called_once_with( "test-device-id", Action.set_speed(4) ) with patch_bond_action() as mock_set_speed_high: - await turn_fan_on(hass, "fan.name_1", fan.SPEED_HIGH) + await turn_fan_on(hass, "fan.name_1", percentage=100) mock_set_speed_high.assert_called_once_with( "test-device-id", Action.set_speed(6) ) -async def test_fan_speed_with_no_max_seed(hass: core.HomeAssistant): +async def test_fan_speed_with_no_max_speed(hass: core.HomeAssistant): """Tests that fans without max speed (increase/decrease controls) map speed to HA standard.""" await setup_platform( hass, @@ -155,7 +142,7 @@ async def test_fan_speed_with_no_max_seed(hass: core.HomeAssistant): state={"power": 1, "speed": 14}, ) - assert hass.states.get("fan.name_1").attributes["speed"] == fan.SPEED_HIGH + assert hass.states.get("fan.name_1").attributes["percentage"] == 100 async def test_turn_on_fan_with_speed(hass: core.HomeAssistant): @@ -165,7 +152,7 @@ async def test_turn_on_fan_with_speed(hass: core.HomeAssistant): ) with patch_bond_action() as mock_set_speed, patch_bond_device_state(): - await turn_fan_on(hass, "fan.name_1", fan.SPEED_LOW) + await turn_fan_on(hass, "fan.name_1", percentage=1) mock_set_speed.assert_called_with("test-device-id", Action.set_speed(1)) @@ -264,9 +251,7 @@ async def test_turn_on_fan_preset_mode_not_supported(hass: core.HomeAssistant): props={"max_speed": 6}, ) - with patch_bond_action(), patch_bond_device_state(), pytest.raises( - fan.NotValidPresetModeError - ): + with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): await turn_fan_on(hass, "fan.name_1", preset_mode=PRESET_MODE_BREEZE) with patch_bond_action(), patch_bond_device_state(), pytest.raises(ValueError): @@ -296,7 +281,7 @@ async def test_turn_on_fan_with_off_with_breeze(hass: core.HomeAssistant): ) with patch_bond_action() as mock_actions, patch_bond_device_state(): - await turn_fan_on(hass, "fan.name_1", fan.SPEED_OFF) + await turn_fan_on(hass, "fan.name_1", percentage=0) assert mock_actions.mock_calls == [ call("test-device-id", Action(Action.BREEZE_OFF)), @@ -316,14 +301,14 @@ async def test_turn_on_fan_without_speed(hass: core.HomeAssistant): mock_turn_on.assert_called_with("test-device-id", Action.turn_on()) -async def test_turn_on_fan_with_off_speed(hass: core.HomeAssistant): +async def test_turn_on_fan_with_off_percentage(hass: core.HomeAssistant): """Tests that turn off command delegates to turn off API.""" await setup_platform( hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" ) with patch_bond_action() as mock_turn_off, patch_bond_device_state(): - await turn_fan_on(hass, "fan.name_1", fan.SPEED_OFF) + await turn_fan_on(hass, "fan.name_1", percentage=0) mock_turn_off.assert_called_with("test-device-id", Action.turn_off()) @@ -337,8 +322,8 @@ async def test_set_speed_off(hass: core.HomeAssistant): with patch_bond_action() as mock_turn_off, patch_bond_device_state(): await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - service_data={ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: SPEED_OFF}, + SERVICE_SET_PERCENTAGE, + service_data={ATTR_ENTITY_ID: "fan.name_1", ATTR_PERCENTAGE: 0}, blocking=True, ) await hass.async_block_till_done() @@ -374,7 +359,7 @@ async def test_set_speed_belief_speed_zero(hass: core.HomeAssistant): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: 0}, + {ATTR_ENTITY_ID: "fan.name_1", "speed": 0}, blocking=True, ) await hass.async_block_till_done() @@ -396,7 +381,7 @@ async def test_set_speed_belief_speed_api_error(hass: core.HomeAssistant): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: 100}, + {ATTR_ENTITY_ID: "fan.name_1", "speed": 100}, blocking=True, ) await hass.async_block_till_done() @@ -412,7 +397,7 @@ async def test_set_speed_belief_speed_100(hass: core.HomeAssistant): await hass.services.async_call( BOND_DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE, - {ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: 100}, + {ATTR_ENTITY_ID: "fan.name_1", "speed": 100}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index ddd4a4e46f4..6d6877c4500 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -3,19 +3,14 @@ from unittest.mock import patch import pytest +from voluptuous.error import MultipleInvalid from homeassistant.components.fan import ( ATTR_PERCENTAGE, - ATTR_SPEED, DOMAIN as FAN_DOMAIN, SERVICE_SET_PERCENTAGE, - SERVICE_SET_SPEED, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -212,7 +207,7 @@ async def test_fans(hass, aioclient_mock, mock_deconz_websocket): {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) - assert aioclient_mock.mock_calls[8][2] == {"speed": 1} + assert aioclient_mock.mock_calls[8][2] == {"speed": 0} # Events with an unsupported speed does not get converted @@ -273,7 +268,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert len(hass.states.async_all()) == 2 # Light and fan assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_HIGH # Test states @@ -289,7 +283,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 25 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_LOW event_changed_light = { "t": "event", @@ -303,7 +296,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 50 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM event_changed_light = { "t": "event", @@ -317,7 +309,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM event_changed_light = { "t": "event", @@ -331,7 +322,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_ON assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 100 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_HIGH event_changed_light = { "t": "event", @@ -345,7 +335,6 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock assert hass.states.get("fan.ceiling_fan").state == STATE_OFF assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 0 - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_OFF # Test service calls @@ -367,99 +356,99 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) - assert aioclient_mock.mock_calls[2][2] == {"speed": 1} + assert aioclient_mock.mock_calls[2][2] == {"speed": 0} # Service turn on fan with bad speed # async_turn_on_compat use speed_to_percentage which will convert to SPEED_MEDIUM -> 2 - await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[3][2] == {"speed": 2} + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: "bad"}, + blocking=True, + ) # Service turn on fan to low speed await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 25}, blocking=True, ) - assert aioclient_mock.mock_calls[4][2] == {"speed": 1} + assert aioclient_mock.mock_calls[3][2] == {"speed": 1} # Service turn on fan to medium speed await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 50}, blocking=True, ) - assert aioclient_mock.mock_calls[5][2] == {"speed": 2} + assert aioclient_mock.mock_calls[4][2] == {"speed": 2} # Service turn on fan to high speed await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH}, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 100}, blocking=True, ) - assert aioclient_mock.mock_calls[6][2] == {"speed": 4} + assert aioclient_mock.mock_calls[5][2] == {"speed": 4} # Service set fan speed to low await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_LOW}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 25}, blocking=True, ) - assert aioclient_mock.mock_calls[7][2] == {"speed": 1} + assert aioclient_mock.mock_calls[6][2] == {"speed": 1} # Service set fan speed to medium await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_MEDIUM}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 50}, blocking=True, ) - assert aioclient_mock.mock_calls[8][2] == {"speed": 2} + assert aioclient_mock.mock_calls[7][2] == {"speed": 2} # Service set fan speed to high await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_HIGH}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 100}, blocking=True, ) - assert aioclient_mock.mock_calls[9][2] == {"speed": 4} + assert aioclient_mock.mock_calls[8][2] == {"speed": 4} # Service set fan speed to off await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: SPEED_OFF}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: 0}, blocking=True, ) - assert aioclient_mock.mock_calls[10][2] == {"speed": 0} + assert aioclient_mock.mock_calls[9][2] == {"speed": 0} # Service set fan speed to unsupported value - with pytest.raises(ValueError): + with pytest.raises(MultipleInvalid): await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_SPEED: "bad value"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: "fan.ceiling_fan", ATTR_PERCENTAGE: "bad value"}, blocking=True, ) @@ -476,7 +465,7 @@ async def test_fans_legacy_speed_modes(hass, aioclient_mock, mock_deconz_websock await hass.async_block_till_done() assert hass.states.get("fan.ceiling_fan").state == STATE_ON - assert hass.states.get("fan.ceiling_fan").attributes[ATTR_SPEED] == SPEED_MEDIUM + assert hass.states.get("fan.ceiling_fan").attributes[ATTR_PERCENTAGE] == 75 await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 4767a99d0b3..9efcdb26de3 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -55,39 +55,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): """Test turning on the device.""" state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - await hass.services.async_call( - fan.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_HIGH}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH - assert state.attributes[fan.ATTR_PERCENTAGE] == 100 - - await hass.services.async_call( - fan.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_MEDIUM}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM - assert state.attributes[fan.ATTR_PERCENTAGE] == 66 - - await hass.services.async_call( - fan.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW - assert state.attributes[fan.ATTR_PERCENTAGE] == 33 - await hass.services.async_call( fan.DOMAIN, SERVICE_TURN_ON, @@ -96,7 +63,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 await hass.services.async_call( @@ -107,7 +73,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM assert state.attributes[fan.ATTR_PERCENTAGE] == 66 await hass.services.async_call( @@ -118,7 +83,36 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW + assert state.attributes[fan.ATTR_PERCENTAGE] == 33 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 100}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 100 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 66}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON + assert state.attributes[fan.ATTR_PERCENTAGE] == 66 + + await hass.services.async_call( + fan.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_PERCENTAGE: 33}, + blocking=True, + ) + state = hass.states.get(fan_entity_id) + assert state.state == STATE_ON assert state.attributes[fan.ATTR_PERCENTAGE] == 33 await hass.services.async_call( @@ -129,7 +123,6 @@ async def test_turn_on_with_speed_and_percentage(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 @@ -198,19 +191,8 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO assert state.attributes[fan.ATTR_PERCENTAGE] is None assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO - assert state.attributes[fan.ATTR_SPEED_LIST] == [ - fan.SPEED_OFF, - fan.SPEED_LOW, - fan.SPEED_MEDIUM, - fan.SPEED_HIGH, - PRESET_MODE_AUTO, - PRESET_MODE_SMART, - PRESET_MODE_SLEEP, - PRESET_MODE_ON, - ] assert state.attributes[fan.ATTR_PRESET_MODES] == [ PRESET_MODE_AUTO, PRESET_MODE_SMART, @@ -226,7 +208,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 assert state.attributes[fan.ATTR_PRESET_MODE] is None @@ -238,7 +219,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_SMART assert state.attributes[fan.ATTR_PERCENTAGE] is None assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_SMART @@ -247,7 +227,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 assert state.attributes[fan.ATTR_PRESET_MODE] is None @@ -262,7 +241,6 @@ async def test_turn_on_with_preset_mode_and_speed(hass, fan_entity_id): state = hass.states.get(fan_entity_id) assert state.state == STATE_OFF - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 assert state.attributes[fan.ATTR_PRESET_MODE] is None @@ -321,50 +299,6 @@ async def test_set_direction(hass, fan_entity_id): assert state.attributes[fan.ATTR_DIRECTION] == fan.DIRECTION_REVERSE -@pytest.mark.parametrize("fan_entity_id", LIMITED_AND_FULL_FAN_ENTITY_IDS) -async def test_set_speed(hass, fan_entity_id): - """Test setting the speed of the device.""" - state = hass.states.get(fan_entity_id) - assert state.state == STATE_OFF - - await hass.services.async_call( - fan.DOMAIN, - fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_LOW}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW - - await hass.services.async_call( - fan.DOMAIN, - fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: fan.SPEED_OFF}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF - - -@pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) -async def test_set_preset_mode_with_legacy_speed_service(hass, fan_entity_id): - """Test setting the preset mode is possible with the legacy service for backwards compat.""" - state = hass.states.get(fan_entity_id) - assert state.state == STATE_OFF - - await hass.services.async_call( - fan.DOMAIN, - fan.SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: fan_entity_id, fan.ATTR_SPEED: PRESET_MODE_AUTO}, - blocking=True, - ) - state = hass.states.get(fan_entity_id) - assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO - assert state.attributes[fan.ATTR_PERCENTAGE] is None - assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO - - @pytest.mark.parametrize("fan_entity_id", FANS_WITH_PRESET_MODES) async def test_set_preset_mode(hass, fan_entity_id): """Test setting the preset mode of the device.""" @@ -379,7 +313,6 @@ async def test_set_preset_mode(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.state == STATE_ON - assert state.attributes[fan.ATTR_SPEED] == PRESET_MODE_AUTO assert state.attributes[fan.ATTR_PERCENTAGE] is None assert state.attributes[fan.ATTR_PRESET_MODE] == PRESET_MODE_AUTO @@ -422,7 +355,6 @@ async def test_set_percentage(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 33 @@ -440,7 +372,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 33 await hass.services.async_call( @@ -450,7 +381,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM assert state.attributes[fan.ATTR_PERCENTAGE] == 66 await hass.services.async_call( @@ -460,7 +390,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 await hass.services.async_call( @@ -470,7 +399,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 100 await hass.services.async_call( @@ -481,7 +409,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): ) state = hass.states.get(fan_entity_id) assert state.attributes[fan.ATTR_PERCENTAGE] == 66 - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM await hass.services.async_call( fan.DOMAIN, @@ -490,7 +417,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 33 await hass.services.async_call( @@ -500,7 +426,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 await hass.services.async_call( @@ -510,7 +435,6 @@ async def test_increase_decrease_speed(hass, fan_entity_id): blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_OFF assert state.attributes[fan.ATTR_PERCENTAGE] == 0 @@ -524,7 +448,6 @@ async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id) blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_LOW assert state.attributes[fan.ATTR_PERCENTAGE] == 25 await hass.services.async_call( @@ -534,7 +457,6 @@ async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id) blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM assert state.attributes[fan.ATTR_PERCENTAGE] == 50 await hass.services.async_call( @@ -544,7 +466,6 @@ async def test_increase_decrease_speed_with_percentage_step(hass, fan_entity_id) blocking=True, ) state = hass.states.get(fan_entity_id) - assert state.attributes[fan.ATTR_SPEED] == fan.SPEED_HIGH assert state.attributes[fan.ATTR_PERCENTAGE] == 75 diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 19a31b4566b..dd27eed9771 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1033,17 +1033,14 @@ async def test_put_light_state_fan(hass_hue, hue_client): living_room_fan = hass_hue.states.get("fan.living_room_fan") assert living_room_fan.state == "on" - assert living_room_fan.attributes[fan.ATTR_SPEED] == fan.SPEED_MEDIUM + assert living_room_fan.attributes[fan.ATTR_PERCENTAGE] == 43 # Check setting the brightness of a fan to 0, 33%, 66% and 100% will respectively turn it off, low, medium or high # We also check non-cached GET value to exercise the code. await perform_put_light_state( hass_hue, hue_client, "fan.living_room_fan", True, brightness=0 ) - assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_OFF - ) + assert hass_hue.states.get("fan.living_room_fan").state == STATE_OFF await perform_put_light_state( hass_hue, hue_client, @@ -1052,8 +1049,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): brightness=round(33 * 254 / 100), ) assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_LOW + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] == 33 ) with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) @@ -1070,8 +1066,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): brightness=round(66 * 254 / 100), ) assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_MEDIUM + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] == 66 ) with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) @@ -1079,7 +1074,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): hue_client, "fan.living_room_fan", HTTPStatus.OK ) assert ( - round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 67 + round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 66 ) # small rounding error in inverse operation await perform_put_light_state( @@ -1090,8 +1085,8 @@ async def test_put_light_state_fan(hass_hue, hue_client): brightness=round(100 * 254 / 100), ) assert ( - hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_SPEED] - == fan.SPEED_HIGH + hass_hue.states.get("fan.living_room_fan").attributes[fan.ATTR_PERCENTAGE] + == 100 ) with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index d68221d84fd..72fc4dd94e2 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -9,9 +9,9 @@ from homeassistant.components.emulated_kasa.const import ( DOMAIN, ) from homeassistant.components.fan import ( - ATTR_SPEED, + ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN, - SERVICE_SET_SPEED, + SERVICE_SET_PERCENTAGE, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -57,15 +57,15 @@ CONFIG = { ENTITY_FAN: { CONF_POWER: "{% if is_state_attr('" + ENTITY_FAN - + "','speed', 'low') %} " + + "','percentage', 33) %} " + str(ENTITY_FAN_SPEED_LOW) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'medium') %} " + + "','percentage', 66) %} " + str(ENTITY_FAN_SPEED_MED) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'high') %} " + + "','percentage', 100) %} " + str(ENTITY_FAN_SPEED_HIGH) + "{% endif %}" }, @@ -109,15 +109,15 @@ CONFIG_FAN = { ENTITY_FAN: { CONF_POWER: "{% if is_state_attr('" + ENTITY_FAN - + "','speed', 'low') %} " + + "','percentage', 33) %} " + str(ENTITY_FAN_SPEED_LOW) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'medium') %} " + + "','percentage', 66) %} " + str(ENTITY_FAN_SPEED_MED) + "{% elif is_state_attr('" + ENTITY_FAN - + "','speed', 'high') %} " + + "','percentage', 100) %} " + str(ENTITY_FAN_SPEED_HIGH) + "{% endif %}" }, @@ -125,6 +125,7 @@ CONFIG_FAN = { } } + CONFIG_SENSOR = { DOMAIN: { CONF_ENTITIES: { @@ -281,8 +282,8 @@ async def test_template(hass): ) await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "low"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PERCENTAGE: 33}, blocking=True, ) @@ -299,8 +300,8 @@ async def test_template(hass): # Fan High: await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "high"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PERCENTAGE: 100}, blocking=True, ) plug_it = emulated_kasa.get_plug_devices(hass, config) @@ -462,8 +463,8 @@ async def test_multiple_devices(hass): ) await hass.services.async_call( FAN_DOMAIN, - SERVICE_SET_SPEED, - {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "medium"}, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_PERCENTAGE: 66}, blocking=True, ) diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index c32686b9311..58264d80817 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -9,7 +9,6 @@ from homeassistant.components.fan import ( ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, - ATTR_SPEED, DOMAIN, SERVICE_DECREASE_SPEED, SERVICE_INCREASE_SPEED, @@ -17,7 +16,6 @@ from homeassistant.components.fan import ( SERVICE_SET_DIRECTION, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -30,7 +28,6 @@ from homeassistant.const import ( async def async_turn_on( hass, entity_id=ENTITY_MATCH_ALL, - speed: str = None, percentage: int = None, preset_mode: str = None, ) -> None: @@ -39,7 +36,6 @@ async def async_turn_on( key: value for key, value in [ (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), (ATTR_PERCENTAGE, percentage), (ATTR_PRESET_MODE, preset_mode), ] @@ -72,17 +68,6 @@ async def async_oscillate( await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data, blocking=True) -async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: - """Set speed for all or specified fan.""" - data = { - key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_SPEED, speed)] - if value is not None - } - - await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) - - async def async_set_preset_mode( hass, entity_id=ENTITY_MATCH_ALL, preset_mode: str = None ) -> None: diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index d29d4378941..f7021c0fb66 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -16,7 +16,6 @@ def test_fanentity(): """Test fan entity methods.""" fan = BaseFan() assert fan.state == "off" - assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 @@ -25,7 +24,7 @@ def test_fanentity(): # Test set_speed not required with pytest.raises(NotImplementedError): fan.oscillate(True) - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): fan.set_speed("low") with pytest.raises(NotImplementedError): fan.set_percentage(0) @@ -42,7 +41,6 @@ async def test_async_fanentity(hass): fan = BaseFan() fan.hass = hass assert fan.state == "off" - assert len(fan.speed_list) == 4 # legacy compat off,low,medium,high assert fan.preset_modes is None assert fan.supported_features == 0 assert fan.percentage_step == 1 @@ -51,7 +49,7 @@ async def test_async_fanentity(hass): # Test set_speed not required with pytest.raises(NotImplementedError): await fan.async_oscillate(True) - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): await fan.async_set_speed("low") with pytest.raises(NotImplementedError): await fan.async_set_percentage(0) diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py index 149332b3fa8..eb429669429 100644 --- a/tests/components/fan/test_reproduce_state.py +++ b/tests/components/fan/test_reproduce_state.py @@ -8,7 +8,7 @@ async def test_reproducing_states(hass, caplog): """Test reproducing Fan states.""" hass.states.async_set("fan.entity_off", "off", {}) hass.states.async_set("fan.entity_on", "on", {}) - hass.states.async_set("fan.entity_speed", "on", {"speed": "high"}) + hass.states.async_set("fan.entity_speed", "on", {"percentage": 100}) hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True}) hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"}) @@ -16,14 +16,14 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "fan", "turn_off") set_direction_calls = async_mock_service(hass, "fan", "set_direction") oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_speed_calls = async_mock_service(hass, "fan", "set_speed") + set_percentage_calls = async_mock_service(hass, "fan", "set_percentage") # These calls should do nothing as entities already in desired state await hass.helpers.state.async_reproduce_state( [ State("fan.entity_off", "off"), State("fan.entity_on", "on"), - State("fan.entity_speed", "on", {"speed": "high"}), + State("fan.entity_speed", "on", {"percentage": 100}), State("fan.entity_oscillating", "on", {"oscillating": True}), State("fan.entity_direction", "on", {"direction": "forward"}), ], @@ -33,7 +33,6 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 0 assert len(set_direction_calls) == 0 assert len(oscillate_calls) == 0 - assert len(set_speed_calls) == 0 # Test invalid state is handled await hass.helpers.state.async_reproduce_state( @@ -45,14 +44,14 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 0 assert len(set_direction_calls) == 0 assert len(oscillate_calls) == 0 - assert len(set_speed_calls) == 0 + assert len(set_percentage_calls) == 0 # Make sure correct services are called await hass.helpers.state.async_reproduce_state( [ State("fan.entity_on", "off"), State("fan.entity_off", "on"), - State("fan.entity_speed", "on", {"speed": "low"}), + State("fan.entity_speed", "on", {"percentage": 25}), State("fan.entity_oscillating", "on", {"oscillating": False}), State("fan.entity_direction", "on", {"direction": "reverse"}), # Should not raise @@ -78,9 +77,12 @@ async def test_reproducing_states(hass, caplog): "oscillating": False, } - assert len(set_speed_calls) == 1 - assert set_speed_calls[0].domain == "fan" - assert set_speed_calls[0].data == {"entity_id": "fan.entity_speed", "speed": "low"} + assert len(set_percentage_calls) == 1 + assert set_percentage_calls[0].domain == "fan" + assert set_percentage_calls[0].data == { + "entity_id": "fan.entity_speed", + "percentage": 25, + } assert len(turn_off_calls) == 1 assert turn_off_calls[0].domain == "fan" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a56a8f967e6..0012826074b 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1589,7 +1589,7 @@ async def test_fan_speed(hass): hass, State( "fan.living_room_fan", - fan.SPEED_HIGH, + STATE_ON, attributes={ "percentage": 33, "percentage_step": 1.0, @@ -1633,7 +1633,7 @@ async def test_fan_reverse(hass, direction_state, direction_call): hass, State( "fan.living_room_fan", - fan.SPEED_HIGH, + STATE_ON, attributes={ "percentage": 33, "percentage_step": 1.0, diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index ae67bda2000..9d5983650d7 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -59,7 +59,6 @@ async def test_haa_fan_setup(hass): supported_features=SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, - "speed_list": ["off", "low", "medium", "high"], }, ), EntityTestInfo( diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py index 33e53e9413c..76fab93b013 100644 --- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -55,7 +55,6 @@ async def test_homeassistant_bridge_fan_setup(hass): ), capabilities={ "preset_modes": None, - "speed_list": ["off", "low", "medium", "high"], }, state="off", ) diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index ce21d643bab..d3531e1c65f 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -40,7 +40,6 @@ async def test_simpleconnect_fan_setup(hass): supported_features=SUPPORT_DIRECTION | SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, - "speed_list": ["off", "low", "medium", "high"], }, state="off", ), diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index 252e7f87bed..faaaa2e666f 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -95,7 +95,7 @@ async def test_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "high"}, + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -109,7 +109,7 @@ async def test_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "medium"}, + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -123,7 +123,7 @@ async def test_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "low"}, + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -196,8 +196,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "high"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -209,8 +209,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "medium"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -222,8 +222,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "low"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -235,8 +235,8 @@ async def test_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "off"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) helper.async_assert_service_values( @@ -291,7 +291,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 100, }, ) - assert state.attributes["speed"] == "high" assert state.attributes["percentage"] == 100 assert state.attributes["percentage_step"] == 1.0 @@ -301,7 +300,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 50, }, ) - assert state.attributes["speed"] == "medium" assert state.attributes["percentage"] == 50 state = await helper.async_update( @@ -310,7 +308,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 25, }, ) - assert state.attributes["speed"] == "low" assert state.attributes["percentage"] == 25 state = await helper.async_update( @@ -320,7 +317,6 @@ async def test_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 0, }, ) - assert state.attributes["speed"] == "off" assert state.attributes["percentage"] == 0 @@ -392,7 +388,7 @@ async def test_v2_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "high"}, + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -406,7 +402,7 @@ async def test_v2_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "medium"}, + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -420,7 +416,7 @@ async def test_v2_turn_on(hass, utcnow): await hass.services.async_call( "fan", "turn_on", - {"entity_id": "fan.testdevice", "speed": "low"}, + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -488,8 +484,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "high"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, blocking=True, ) helper.async_assert_service_values( @@ -501,8 +497,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "medium"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, blocking=True, ) helper.async_assert_service_values( @@ -514,8 +510,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "low"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, blocking=True, ) helper.async_assert_service_values( @@ -527,8 +523,8 @@ async def test_v2_set_speed(hass, utcnow): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": "fan.testdevice", "speed": "off"}, + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, blocking=True, ) helper.async_assert_service_values( @@ -616,7 +612,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 100, }, ) - assert state.attributes["speed"] == "high" assert state.attributes["percentage"] == 100 state = await helper.async_update( @@ -625,7 +620,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 50, }, ) - assert state.attributes["speed"] == "medium" assert state.attributes["percentage"] == 50 state = await helper.async_update( @@ -634,7 +628,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 25, }, ) - assert state.attributes["speed"] == "low" assert state.attributes["percentage"] == 25 state = await helper.async_update( @@ -644,7 +637,6 @@ async def test_v2_speed_read(hass, utcnow): CharacteristicsTypes.ROTATION_SPEED: 0, }, ) - assert state.attributes["speed"] == "off" assert state.attributes["percentage"] == 0 diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 224a878b065..6eea6a8144b 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -3,6 +3,9 @@ import json from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.climate.const import ( + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -11,7 +14,6 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -94,7 +96,7 @@ async def test_current_fan_mode(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) await thermostat.async_update() - assert thermostat.fan_mode == SPEED_LOW + assert thermostat.fan_mode == FAN_LOW thermostat._cur_settings = None assert thermostat.fan_mode is None @@ -162,7 +164,7 @@ async def test_fan_modes(hass): api = melissa_mock() device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) - assert ["auto", SPEED_HIGH, SPEED_MEDIUM, SPEED_LOW] == thermostat.fan_modes + assert ["auto", FAN_HIGH, FAN_MEDIUM, FAN_LOW] == thermostat.fan_modes async def test_target_temperature(hass): @@ -247,9 +249,9 @@ async def test_fan_mode(hass): thermostat = MelissaClimate(api, _SERIAL, device) await thermostat.async_update() await hass.async_block_till_done() - await thermostat.async_set_fan_mode(SPEED_HIGH) + await thermostat.async_set_fan_mode(FAN_HIGH) await hass.async_block_till_done() - assert thermostat.fan_mode == SPEED_HIGH + assert thermostat.fan_mode == FAN_HIGH async def test_set_operation_mode(hass): @@ -275,12 +277,12 @@ async def test_send(hass): await hass.async_block_till_done() await thermostat.async_send({"fan": api.FAN_MEDIUM}) await hass.async_block_till_done() - assert thermostat.fan_mode == SPEED_MEDIUM + assert thermostat.fan_mode == FAN_MEDIUM api.async_send.return_value = AsyncMock(return_value=False) thermostat._cur_settings = None await thermostat.async_send({"fan": api.FAN_LOW}) await hass.async_block_till_done() - assert SPEED_LOW != thermostat.fan_mode + assert FAN_LOW != thermostat.fan_mode assert thermostat._cur_settings is None @@ -293,7 +295,7 @@ async def test_update(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) await thermostat.async_update() - assert thermostat.fan_mode == SPEED_LOW + assert thermostat.fan_mode == FAN_LOW assert thermostat.state == HVAC_MODE_HEAT api.async_status = AsyncMock(side_effect=KeyError("boom")) await thermostat.async_update() @@ -322,9 +324,9 @@ async def test_melissa_fan_to_hass(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) assert thermostat.melissa_fan_to_hass(0) == "auto" - assert thermostat.melissa_fan_to_hass(1) == SPEED_LOW - assert thermostat.melissa_fan_to_hass(2) == SPEED_MEDIUM - assert thermostat.melissa_fan_to_hass(3) == SPEED_HIGH + assert thermostat.melissa_fan_to_hass(1) == FAN_LOW + assert thermostat.melissa_fan_to_hass(2) == FAN_MEDIUM + assert thermostat.melissa_fan_to_hass(3) == FAN_HIGH assert thermostat.melissa_fan_to_hass(4) is None @@ -355,9 +357,9 @@ async def test_hass_fan_to_melissa(hass): device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) assert thermostat.hass_fan_to_melissa("auto") == 0 - assert thermostat.hass_fan_to_melissa(SPEED_LOW) == 1 - assert thermostat.hass_fan_to_melissa(SPEED_MEDIUM) == 2 - assert thermostat.hass_fan_to_melissa(SPEED_HIGH) == 3 + assert thermostat.hass_fan_to_melissa(FAN_LOW) == 1 + assert thermostat.hass_fan_to_melissa(FAN_MEDIUM) == 2 + assert thermostat.hass_fan_to_melissa(FAN_HIGH) == 3 thermostat.hass_fan_to_melissa("test") mocked_warning.assert_called_once_with( "Melissa have no setting for %s fan mode", "test" diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 5418727ec0e..64b5d272af8 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -194,7 +194,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, "percentage-state-topic", "rEset_percentage") state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) is None - assert state.attributes.get(fan.ATTR_SPEED) is None async_fire_mqtt_message(hass, "state-topic", "None") state = hass.states.get("fan.test") @@ -599,9 +598,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -799,13 +797,11 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") - await common.async_set_preset_mode(hass, "fan.test", "auto") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "auto") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -938,13 +934,11 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_PERCENTAGE) == 0 assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "medium") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1035,9 +1029,8 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "medium") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1131,6 +1124,10 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca with pytest.raises(NotValidPresetModeError): await common.async_turn_on(hass, "fan.test", preset_mode="auto") + assert mqtt_mock.async_publish.call_count == 1 + # We can turn on, but the invalid preset mode will raise + mqtt_mock.async_publish.assert_any_call("command-topic", "ON", 0, False) + mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "fan.test", preset_mode="whoosh") assert mqtt_mock.async_publish.call_count == 2 @@ -1259,13 +1256,11 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca with pytest.raises(MultipleInvalid): await common.async_set_percentage(hass, "fan.test", 101) - await common.async_set_preset_mode(hass, "fan.test", "low") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "low") - await common.async_set_preset_mode(hass, "fan.test", "medium") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "medium") await common.async_set_preset_mode(hass, "fan.test", "whoosh") mqtt_mock.async_publish.assert_called_once_with( @@ -1285,9 +1280,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca assert state.state == STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_preset_mode(hass, "fan.test", "freaking-high") - assert "not a valid preset mode" in caplog.text - caplog.clear() + with pytest.raises(NotValidPresetModeError): + await common.async_set_preset_mode(hass, "fan.test", "freaking-high") mqtt_mock.async_publish.reset_mock() state = hass.states.get("fan.test") diff --git a/tests/components/ozw/test_fan.py b/tests/components/ozw/test_fan.py index 5556b663f6f..9bf0cbf7093 100644 --- a/tests/components/ozw/test_fan.py +++ b/tests/components/ozw/test_fan.py @@ -1,5 +1,4 @@ """Test Z-Wave Fans.""" -import pytest from .common import setup_ozw @@ -119,13 +118,3 @@ async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): state = hass.states.get("fan.in_wall_smart_fan_control_level") assert state is not None assert state.state == "off" - - # Test invalid speed - new_speed = "invalid" - with pytest.raises(ValueError): - await hass.services.async_call( - "fan", - "set_speed", - {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, - blocking=True, - ) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 16b0360f9eb..1ee48c5c47d 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -7,13 +7,8 @@ real HTTP calls are not initiated during testing. from pysmartthings import Attribute, Capability from homeassistant.components.fan import ( - ATTR_SPEED, - ATTR_SPEED_LIST, + ATTR_PERCENTAGE, DOMAIN as FAN_DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE @@ -42,13 +37,7 @@ async def test_entity_state(hass, device_factory): state = hass.states.get("fan.fan_1") assert state.state == "on" assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SET_SPEED - assert state.attributes[ATTR_SPEED] == SPEED_MEDIUM - assert state.attributes[ATTR_SPEED_LIST] == [ - SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - ] + assert state.attributes[ATTR_PERCENTAGE] == 66 async def test_entity_and_device_attributes(hass, device_factory): @@ -128,17 +117,17 @@ async def test_turn_on_with_speed(hass, device_factory): await hass.services.async_call( "fan", "turn_on", - {ATTR_ENTITY_ID: "fan.fan_1", ATTR_SPEED: SPEED_HIGH}, + {ATTR_ENTITY_ID: "fan.fan_1", ATTR_PERCENTAGE: 100}, blocking=True, ) # Assert state = hass.states.get("fan.fan_1") assert state is not None assert state.state == "on" - assert state.attributes[ATTR_SPEED] == SPEED_HIGH + assert state.attributes[ATTR_PERCENTAGE] == 100 -async def test_set_speed(hass, device_factory): +async def test_set_percentage(hass, device_factory): """Test setting to specific fan speed.""" # Arrange device = device_factory( @@ -150,15 +139,15 @@ async def test_set_speed(hass, device_factory): # Act await hass.services.async_call( "fan", - "set_speed", - {ATTR_ENTITY_ID: "fan.fan_1", ATTR_SPEED: SPEED_HIGH}, + "set_percentage", + {ATTR_ENTITY_ID: "fan.fan_1", ATTR_PERCENTAGE: 100}, blocking=True, ) # Assert state = hass.states.get("fan.fan_1") assert state is not None assert state.state == "on" - assert state.attributes[ATTR_SPEED] == SPEED_HIGH + assert state.attributes[ATTR_PERCENTAGE] == 100 async def test_update_from_signal(hass, device_factory): diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 1f1947183de..ccd273571b5 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -8,17 +8,11 @@ from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_PERCENTAGE, ATTR_PRESET_MODE, - ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, - NotValidSpeedError, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -60,7 +54,7 @@ _DIRECTION_INPUT_SELECT = "input_select.direction" ) async def test_missing_optional_config(hass, start_ha): """Test: missing optional template is ok.""" - _verify(hass, STATE_ON, None, None, None, None, None) + _verify(hass, STATE_ON, None, None, None, None) @pytest.mark.parametrize("count,domain", [(0, DOMAIN)]) @@ -165,26 +159,26 @@ async def test_wrong_template_config(hass, start_ha): ) async def test_templates_with_entities(hass, start_ha): """Test tempalates with values from other entities.""" - _verify(hass, STATE_OFF, None, 0, None, None, None) + _verify(hass, STATE_OFF, 0, None, None, None) hass.states.async_set(_STATE_INPUT_BOOLEAN, True) hass.states.async_set(_PERCENTAGE_INPUT_NUMBER, 66) hass.states.async_set(_OSC_INPUT, "True") - for set_state, set_value, speed, value in [ - (_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD, SPEED_MEDIUM, 66), - (_PERCENTAGE_INPUT_NUMBER, 33, SPEED_LOW, 33), - (_PERCENTAGE_INPUT_NUMBER, 66, SPEED_MEDIUM, 66), - (_PERCENTAGE_INPUT_NUMBER, 100, SPEED_HIGH, 100), - (_PERCENTAGE_INPUT_NUMBER, "dog", None, 0), + for set_state, set_value, value in [ + (_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD, 66), + (_PERCENTAGE_INPUT_NUMBER, 33, 33), + (_PERCENTAGE_INPUT_NUMBER, 66, 66), + (_PERCENTAGE_INPUT_NUMBER, 100, 100), + (_PERCENTAGE_INPUT_NUMBER, "dog", 0), ]: hass.states.async_set(set_state, set_value) await hass.async_block_till_done() - _verify(hass, STATE_ON, speed, value, True, DIRECTION_FORWARD, None) + _verify(hass, STATE_ON, value, True, DIRECTION_FORWARD, None) hass.states.async_set(_STATE_INPUT_BOOLEAN, False) await hass.async_block_till_done() - _verify(hass, STATE_OFF, None, 0, True, DIRECTION_FORWARD, None) + _verify(hass, STATE_OFF, 0, True, DIRECTION_FORWARD, None) @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -207,12 +201,12 @@ async def test_templates_with_entities(hass, start_ha): }, "sensor.percentage", [ - ("0", 0, SPEED_OFF, None), - ("33", 33, SPEED_LOW, None), - ("invalid", 0, None, None), - ("5000", 0, None, None), - ("100", 100, SPEED_HIGH, None), - ("0", 0, SPEED_OFF, None), + ("0", 0, None), + ("33", 33, None), + ("invalid", 0, None), + ("5000", 0, None), + ("100", 100, None), + ("0", 0, None), ], ), ( @@ -232,21 +226,21 @@ async def test_templates_with_entities(hass, start_ha): }, "sensor.preset_mode", [ - ("0", None, None, None), - ("invalid", None, None, None), - ("auto", None, None, "auto"), - ("smart", None, None, "smart"), - ("invalid", None, None, None), + ("0", None, None), + ("invalid", None, None), + ("auto", None, "auto"), + ("smart", None, "smart"), + ("invalid", None, None), ], ), ], ) async def test_templates_with_entities2(hass, entity, tests, start_ha): """Test templates with values from other entities.""" - for set_percentage, test_percentage, speed, test_type in tests: + for set_percentage, test_percentage, test_type in tests: hass.states.async_set(entity, set_percentage) await hass.async_block_till_done() - _verify(hass, STATE_ON, speed, test_percentage, None, None, test_type) + _verify(hass, STATE_ON, test_percentage, None, None, test_type) @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -295,7 +289,7 @@ async def test_availability_template_with_entities(hass, start_ha): }, } }, - [STATE_OFF, None, None, None, None], + [STATE_OFF, None, None, None], ), ( { @@ -313,7 +307,7 @@ async def test_availability_template_with_entities(hass, start_ha): }, } }, - [STATE_ON, None, 0, None, None], + [STATE_ON, 0, None, None], ), ( { @@ -331,7 +325,7 @@ async def test_availability_template_with_entities(hass, start_ha): }, } }, - [STATE_ON, SPEED_MEDIUM, 66, True, DIRECTION_FORWARD], + [STATE_ON, 66, True, DIRECTION_FORWARD], ), ( { @@ -349,13 +343,13 @@ async def test_availability_template_with_entities(hass, start_ha): }, } }, - [STATE_OFF, None, 0, None, None], + [STATE_OFF, 0, None, None], ), ], ) async def test_template_with_unavailable_entities(hass, states, start_ha): """Test unavailability with value_template.""" - _verify(hass, states[0], states[1], states[2], states[3], states[4], None) + _verify(hass, states[0], states[1], states[2], states[3], None) @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -399,42 +393,7 @@ async def test_on_off(hass): ]: await func(hass, _TEST_FAN) assert hass.states.get(_STATE_INPUT_BOOLEAN).state == state - _verify(hass, state, None, 0, None, None, None) - - -async def test_set_speed(hass): - """Test set valid speed.""" - await _register_components(hass, preset_modes=["auto", "smart"]) - - await common.async_turn_on(hass, _TEST_FAN) - for cmd, type, state, value in [ - (SPEED_HIGH, SPEED_HIGH, STATE_ON, 100), - (SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), - (SPEED_OFF, SPEED_OFF, STATE_OFF, 0), - (SPEED_MEDIUM, SPEED_MEDIUM, STATE_ON, 66), - ]: - await common.async_set_speed(hass, _TEST_FAN, cmd) - assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == value - _verify(hass, state, type, value, None, None, None) - - with pytest.raises(NotValidSpeedError): - await common.async_set_speed(hass, _TEST_FAN, "invalid") - - assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == 66 - _verify(hass, STATE_ON, SPEED_MEDIUM, 66, None, None, None) - - -async def test_set_invalid_speed(hass): - """Test set invalid speed when fan has valid speed.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN) - await common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) - assert float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state) == 100 - _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) - - with pytest.raises(NotValidSpeedError): - await common.async_set_speed(hass, _TEST_FAN, "invalid") + _verify(hass, state, 0, None, None, None) async def test_set_invalid_direction_from_initial_stage(hass, calls): @@ -445,7 +404,7 @@ async def test_set_invalid_direction_from_initial_stage(hass, calls): await common.async_set_direction(hass, _TEST_FAN, "invalid") assert hass.states.get(_DIRECTION_INPUT_SELECT).state == "" - _verify(hass, STATE_ON, None, 0, None, None, None) + _verify(hass, STATE_ON, 0, None, None, None) async def test_set_osc(hass): @@ -456,7 +415,7 @@ async def test_set_osc(hass): for state in [True, False]: await common.async_oscillate(hass, _TEST_FAN, state) assert hass.states.get(_OSC_INPUT).state == str(state) - _verify(hass, STATE_ON, None, 0, state, None, None) + _verify(hass, STATE_ON, 0, state, None, None) async def test_set_direction(hass): @@ -467,7 +426,7 @@ async def test_set_direction(hass): for cmd in [DIRECTION_FORWARD, DIRECTION_REVERSE]: await common.async_set_direction(hass, _TEST_FAN, cmd) assert hass.states.get(_DIRECTION_INPUT_SELECT).state == cmd - _verify(hass, STATE_ON, None, 0, None, cmd, None) + _verify(hass, STATE_ON, 0, None, cmd, None) async def test_set_invalid_direction(hass): @@ -478,17 +437,7 @@ async def test_set_invalid_direction(hass): for cmd in [DIRECTION_FORWARD, "invalid"]: await common.async_set_direction(hass, _TEST_FAN, cmd) assert hass.states.get(_DIRECTION_INPUT_SELECT).state == DIRECTION_FORWARD - _verify(hass, STATE_ON, None, 0, None, DIRECTION_FORWARD, None) - - -async def test_on_with_speed(hass): - """Test turn on with speed.""" - await _register_components(hass) - - await common.async_turn_on(hass, _TEST_FAN, SPEED_HIGH) - assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON - assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 100 - _verify(hass, STATE_ON, SPEED_HIGH, 100, None, None, None) + _verify(hass, STATE_ON, 0, None, DIRECTION_FORWARD, None) async def test_preset_modes(hass): @@ -515,18 +464,18 @@ async def test_set_percentage(hass): await _register_components(hass) await common.async_turn_on(hass, _TEST_FAN) - for type, state, value in [ - (SPEED_HIGH, STATE_ON, 100), - (SPEED_MEDIUM, STATE_ON, 66), - (SPEED_OFF, STATE_OFF, 0), + for state, value in [ + (STATE_ON, 100), + (STATE_ON, 66), + (STATE_OFF, 0), ]: await common.async_set_percentage(hass, _TEST_FAN, value) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value - _verify(hass, state, type, value, None, None, None) + _verify(hass, state, value, None, None, None) await common.async_turn_on(hass, _TEST_FAN, percentage=50) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 50 - _verify(hass, STATE_ON, SPEED_MEDIUM, 50, None, None, None) + _verify(hass, STATE_ON, 50, None, None, None) async def test_increase_decrease_speed(hass): @@ -534,16 +483,16 @@ async def test_increase_decrease_speed(hass): await _register_components(hass, speed_count=3) await common.async_turn_on(hass, _TEST_FAN) - for func, extra, state, type, value in [ - (common.async_set_percentage, 100, STATE_ON, SPEED_HIGH, 100), - (common.async_decrease_speed, None, STATE_ON, SPEED_MEDIUM, 66), - (common.async_decrease_speed, None, STATE_ON, SPEED_LOW, 33), - (common.async_decrease_speed, None, STATE_OFF, SPEED_OFF, 0), - (common.async_increase_speed, None, STATE_ON, SPEED_LOW, 33), + for func, extra, state, value in [ + (common.async_set_percentage, 100, STATE_ON, 100), + (common.async_decrease_speed, None, STATE_ON, 66), + (common.async_decrease_speed, None, STATE_ON, 33), + (common.async_decrease_speed, None, STATE_OFF, 0), + (common.async_increase_speed, None, STATE_ON, 33), ]: await func(hass, _TEST_FAN, extra) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value - _verify(hass, state, type, value, None, None, None) + _verify(hass, state, value, None, None, None) async def test_increase_decrease_speed_default_speed_count(hass): @@ -551,16 +500,16 @@ async def test_increase_decrease_speed_default_speed_count(hass): await _register_components(hass) await common.async_turn_on(hass, _TEST_FAN) - for func, extra, state, type, value in [ - (common.async_set_percentage, 100, STATE_ON, SPEED_HIGH, 100), - (common.async_decrease_speed, None, STATE_ON, SPEED_HIGH, 99), - (common.async_decrease_speed, None, STATE_ON, SPEED_HIGH, 98), - (common.async_decrease_speed, 31, STATE_ON, SPEED_HIGH, 67), - (common.async_decrease_speed, None, STATE_ON, SPEED_MEDIUM, 66), + for func, extra, state, value in [ + (common.async_set_percentage, 100, STATE_ON, 100), + (common.async_decrease_speed, None, STATE_ON, 99), + (common.async_decrease_speed, None, STATE_ON, 98), + (common.async_decrease_speed, 31, STATE_ON, 67), + (common.async_decrease_speed, None, STATE_ON, 66), ]: await func(hass, _TEST_FAN, extra) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value - _verify(hass, state, type, value, None, None, None) + _verify(hass, state, value, None, None, None) async def test_set_invalid_osc_from_initial_state(hass): @@ -571,7 +520,7 @@ async def test_set_invalid_osc_from_initial_state(hass): with pytest.raises(vol.Invalid): await common.async_oscillate(hass, _TEST_FAN, "invalid") assert hass.states.get(_OSC_INPUT).state == "" - _verify(hass, STATE_ON, None, 0, None, None, None) + _verify(hass, STATE_ON, 0, None, None, None) async def test_set_invalid_osc(hass): @@ -581,18 +530,17 @@ async def test_set_invalid_osc(hass): await common.async_turn_on(hass, _TEST_FAN) await common.async_oscillate(hass, _TEST_FAN, True) assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, None, 0, True, None, None) + _verify(hass, STATE_ON, 0, True, None, None) with pytest.raises(vol.Invalid): await common.async_oscillate(hass, _TEST_FAN, None) assert hass.states.get(_OSC_INPUT).state == "True" - _verify(hass, STATE_ON, None, 0, True, None, None) + _verify(hass, STATE_ON, 0, True, None, None) def _verify( hass, expected_state, - expected_speed, expected_percentage, expected_oscillating, expected_direction, @@ -602,7 +550,6 @@ def _verify( state = hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == str(expected_state) - assert attributes.get(ATTR_SPEED) == expected_speed or SPEED_OFF assert attributes.get(ATTR_PERCENTAGE) == expected_percentage assert attributes.get(ATTR_OSCILLATING) == expected_oscillating assert attributes.get(ATTR_DIRECTION) == expected_direction diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a8f5f645059..e367857b260 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -8,19 +8,13 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.hvac as hvac import zigpy.zcl.foundation as zcl_f -from homeassistant.components import fan from homeassistant.components.fan import ( ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, - ATTR_SPEED, DOMAIN as FAN_DOMAIN, + SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, - SERVICE_SET_SPEED, - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, NotValidPresetModeError, ) from homeassistant.components.zha.core.discovery import GROUP_PROBE @@ -187,7 +181,7 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device): # change speed from HA cluster.write_attributes.reset_mock() - await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) + await async_set_percentage(hass, entity_id, percentage=100) assert len(cluster.write_attributes.mock_calls) == 1 assert cluster.write_attributes.call_args == call({"fan_mode": 3}) @@ -209,11 +203,11 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device): await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) -async def async_turn_on(hass, entity_id, speed=None): +async def async_turn_on(hass, entity_id, percentage=None): """Turn fan on.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_SPEED, speed)] + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)] if value is not None } @@ -227,15 +221,17 @@ async def async_turn_off(hass, entity_id): await hass.services.async_call(Platform.FAN, SERVICE_TURN_OFF, data, blocking=True) -async def async_set_speed(hass, entity_id, speed=None): - """Set speed for specified fan.""" +async def async_set_percentage(hass, entity_id, percentage=None): + """Set percentage for specified fan.""" data = { key: value - for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_SPEED, speed)] + for key, value in [(ATTR_ENTITY_ID, entity_id), (ATTR_PERCENTAGE, percentage)] if value is not None } - await hass.services.async_call(Platform.FAN, SERVICE_SET_SPEED, data, blocking=True) + await hass.services.async_call( + Platform.FAN, SERVICE_SET_PERCENTAGE, data, blocking=True + ) async def async_set_preset_mode(hass, entity_id, preset_mode=None): @@ -321,7 +317,7 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato # change speed from HA group_fan_cluster.write_attributes.reset_mock() - await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) + await async_set_percentage(hass, entity_id, percentage=100) assert len(group_fan_cluster.write_attributes.mock_calls) == 1 assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 3} @@ -428,13 +424,13 @@ async def test_zha_group_fan_entity_failure_state( @pytest.mark.parametrize( - "plug_read, expected_state, expected_speed, expected_percentage", + "plug_read, expected_state, expected_percentage", ( - (None, STATE_OFF, None, None), - ({"fan_mode": 0}, STATE_OFF, SPEED_OFF, 0), - ({"fan_mode": 1}, STATE_ON, SPEED_LOW, 33), - ({"fan_mode": 2}, STATE_ON, SPEED_MEDIUM, 66), - ({"fan_mode": 3}, STATE_ON, SPEED_HIGH, 100), + (None, STATE_OFF, None), + ({"fan_mode": 0}, STATE_OFF, 0), + ({"fan_mode": 1}, STATE_ON, 33), + ({"fan_mode": 2}, STATE_ON, 66), + ({"fan_mode": 3}, STATE_ON, 100), ), ) async def test_fan_init( @@ -443,7 +439,6 @@ async def test_fan_init( zigpy_device, plug_read, expected_state, - expected_speed, expected_percentage, ): """Test zha fan platform.""" @@ -455,7 +450,6 @@ async def test_fan_init( entity_id = await find_entity_id(Platform.FAN, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == expected_state - assert hass.states.get(entity_id).attributes[ATTR_SPEED] == expected_speed assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == expected_percentage assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None @@ -474,7 +468,6 @@ async def test_fan_update_entity( entity_id = await find_entity_id(Platform.FAN, zha_device, hass) assert entity_id is not None assert hass.states.get(entity_id).state == STATE_OFF - assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 @@ -487,7 +480,6 @@ async def test_fan_update_entity( "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_OFF - assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF assert cluster.read_attributes.await_count == 3 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} @@ -496,7 +488,6 @@ async def test_fan_update_entity( ) assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 33 - assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 assert cluster.read_attributes.await_count == 4 diff --git a/tests/components/zwave/test_fan.py b/tests/components/zwave/test_fan.py index d71ba0713d2..0104353f851 100644 --- a/tests/components/zwave/test_fan.py +++ b/tests/components/zwave/test_fan.py @@ -1,16 +1,10 @@ """Test Z-Wave fans.""" import pytest -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_OFF, - SUPPORT_SET_SPEED, -) +from homeassistant.components.fan import SUPPORT_SET_SPEED from homeassistant.components.zwave import fan -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue # Integration is disabled pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) @@ -25,7 +19,6 @@ def test_get_device_detects_fan(mock_openzwave): device = fan.get_device(node=node, values=values, node_config={}) assert isinstance(device, fan.ZwaveFan) assert device.supported_features == SUPPORT_SET_SPEED - assert device.speed_list == [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] def test_fan_turn_on(mock_openzwave): @@ -96,31 +89,3 @@ def test_fan_turn_off(mock_openzwave): value_id, brightness = node.set_dimmer.mock_calls[0][1] assert value_id == value.value_id assert brightness == 0 - - -def test_fan_value_changed(mock_openzwave): - """Test value changed for zwave fan.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - device = fan.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - value.data = 10 - value_changed(value) - - assert device.is_on - assert device.speed == SPEED_LOW - - value.data = 50 - value_changed(value) - - assert device.is_on - assert device.speed == SPEED_MEDIUM - - value.data = 90 - value_changed(value) - - assert device.is_on - assert device.speed == SPEED_HIGH diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 74ad642127f..80c057223ac 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -2,14 +2,10 @@ import math import pytest +from voluptuous.error import MultipleInvalid from zwave_js_server.event import Event -from homeassistant.components.fan import ( - ATTR_PERCENTAGE, - ATTR_PERCENTAGE_STEP, - ATTR_SPEED, - SPEED_MEDIUM, -) +from homeassistant.components.fan import ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP async def test_generic_fan(hass, client, fan_generic, integration): @@ -25,7 +21,7 @@ async def test_generic_fan(hass, client, fan_generic, integration): await hass.services.async_call( "fan", "turn_on", - {"entity_id": entity_id, "speed": SPEED_MEDIUM}, + {"entity_id": entity_id, "percentage": 66}, blocking=True, ) @@ -54,11 +50,11 @@ async def test_generic_fan(hass, client, fan_generic, integration): client.async_send_command.reset_mock() # Test setting unknown speed - with pytest.raises(ValueError): + with pytest.raises(MultipleInvalid): await hass.services.async_call( "fan", - "set_speed", - {"entity_id": entity_id, "speed": 99}, + "set_percentage", + {"entity_id": entity_id, "percentage": "bad"}, blocking=True, ) @@ -150,7 +146,7 @@ async def test_generic_fan(hass, client, fan_generic, integration): state = hass.states.get(entity_id) assert state.state == "on" - assert state.attributes[ATTR_SPEED] == "high" + assert state.attributes[ATTR_PERCENTAGE] == 100 client.async_send_command.reset_mock() @@ -175,7 +171,7 @@ async def test_generic_fan(hass, client, fan_generic, integration): state = hass.states.get(entity_id) assert state.state == "off" - assert state.attributes[ATTR_SPEED] == "off" + assert state.attributes[ATTR_PERCENTAGE] == 0 async def test_configurable_speeds_fan(hass, client, hs_fc200, integration): From 2791cd68c2eba1a926eb4c6b0c8d38076330343b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 9 Mar 2022 04:42:07 -0500 Subject: [PATCH 0329/1054] Timer code cleanup (#67604) --- homeassistant/components/timer/__init__.py | 35 ++++++------------ tests/components/timer/test_init.py | 41 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index e4aa6be1ff1..35258555537 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -55,8 +55,7 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 CREATE_FIELDS = { - vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), - vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, } @@ -199,6 +198,9 @@ class Timer(RestoreEntity): self._end: datetime | None = None self._listener: Callable[[], None] | None = None + self._attr_should_poll = False + self._attr_force_update = True + @classmethod def from_yaml(cls, config: dict) -> Timer: """Return entity instance initialized from yaml storage.""" @@ -207,16 +209,6 @@ class Timer(RestoreEntity): timer.editable = False return timer - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def force_update(self) -> bool: - """Return True to fix restart issues.""" - return True - @property def name(self): """Return name of the timer.""" @@ -266,9 +258,6 @@ class Timer(RestoreEntity): if self._listener: self._listener() self._listener = None - newduration = None - if duration: - newduration = duration event = EVENT_TIMER_STARTED if self._state in (STATUS_ACTIVE, STATUS_PAUSED): @@ -277,17 +266,13 @@ class Timer(RestoreEntity): self._state = STATUS_ACTIVE start = dt_util.utcnow().replace(microsecond=0) - if self._remaining and newduration is None: - self._end = start + self._remaining - - elif newduration: - self._duration = newduration - self._remaining = newduration - self._end = start + self._duration - - else: + # Set remaining to new value if needed + if duration: + self._remaining = self._duration = duration + elif not self._remaining: self._remaining = self._duration - self._end = start + self._duration + + self._end = start + self._remaining self.hass.bus.async_fire(event, {"entity_id": self.entity_id}) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 92958a7bce6..5c276bc0cfc 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.timer import ( ATTR_DURATION, + ATTR_REMAINING, CONF_DURATION, CONF_ICON, CONF_NAME, @@ -190,6 +191,46 @@ async def test_methods_and_events(hass): assert len(results) == expectedEvents +async def test_start_service(hass): + """Test the start/stop service.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) + + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_IDLE + assert state.attributes[ATTR_DURATION] == "0:00:10" + + await hass.services.async_call( + DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"} + ) + await hass.async_block_till_done() + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_ACTIVE + assert state.attributes[ATTR_DURATION] == "0:00:10" + assert state.attributes[ATTR_REMAINING] == "0:00:10" + + await hass.services.async_call( + DOMAIN, SERVICE_CANCEL, {CONF_ENTITY_ID: "timer.test1"} + ) + await hass.async_block_till_done() + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_IDLE + assert state.attributes[ATTR_DURATION] == "0:00:10" + assert ATTR_REMAINING not in state.attributes + + await hass.services.async_call( + DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 15} + ) + await hass.async_block_till_done() + state = hass.states.get("timer.test1") + assert state + assert state.state == STATUS_ACTIVE + assert state.attributes[ATTR_DURATION] == "0:00:15" + assert state.attributes[ATTR_REMAINING] == "0:00:15" + + async def test_wait_till_timer_expires(hass): """Test for a timer to end.""" hass.state = CoreState.starting From e7fba46a0636b16919144ea07dc8253f619eba22 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Mar 2022 13:18:19 +0100 Subject: [PATCH 0330/1054] Refactor helper_config_entry_flow (#67895) --- homeassistant/components/group/config_flow.py | 93 ++++---- .../components/switch/config_flow.py | 27 ++- .../helpers/helper_config_entry_flow.py | 210 ++++++++---------- tests/components/group/test_config_flow.py | 2 +- 4 files changed, 161 insertions(+), 171 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 82de2056da5..0547578b131 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -1,13 +1,18 @@ """Config flow for Group integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any, cast import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITIES -from homeassistant.helpers import helper_config_entry_flow, selector +from homeassistant.core import callback +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowStep, +) from . import DOMAIN @@ -30,52 +35,54 @@ def basic_group_config_schema(domain: str) -> vol.Schema: ) -STEPS = { - "init": vol.Schema( - { - vol.Required("group_type"): selector.selector( - { - "select": { - "options": [ - "cover", - "fan", - "light", - "media_player", - ] - } +INITIAL_STEP_SCHEMA = vol.Schema( + { + vol.Required("group_type"): selector.selector( + { + "select": { + "options": [ + "cover", + "fan", + "light", + "media_player", + ] } - ) - } - ), - "cover": basic_group_config_schema("cover"), - "fan": basic_group_config_schema("fan"), - "light": basic_group_config_schema("light"), - "media_player": basic_group_config_schema("media_player"), - "cover_options": basic_group_options_schema("cover"), - "fan_options": basic_group_options_schema("fan"), - "light_options": basic_group_options_schema("light"), - "media_player_options": basic_group_options_schema("media_player"), + } + ) + } +) + + +@callback +def choose_config_step(options: dict[str, Any]) -> str: + """Return next step_id when group_type is selected.""" + return cast(str, options["group_type"]) + + +CONFIG_FLOW = { + "user": HelperFlowStep(INITIAL_STEP_SCHEMA, next_step=choose_config_step), + "cover": HelperFlowStep(basic_group_config_schema("cover")), + "fan": HelperFlowStep(basic_group_config_schema("fan")), + "light": HelperFlowStep(basic_group_config_schema("light")), + "media_player": HelperFlowStep(basic_group_config_schema("media_player")), } -class GroupConfigFlowHandler( - helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN -): +OPTIONS_FLOW = { + "init": HelperFlowStep(None, next_step=choose_config_step), + "cover": HelperFlowStep(basic_group_options_schema("cover")), + "fan": HelperFlowStep(basic_group_options_schema("fan")), + "light": HelperFlowStep(basic_group_options_schema("light")), + "media_player": HelperFlowStep(basic_group_options_schema("media_player")), +} + + +class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): """Handle a config or options flow for Switch Light.""" - steps = STEPS + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW - def async_config_entry_title(self, user_input: dict[str, Any]) -> str: + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" - return cast(str, user_input["name"]) if "name" in user_input else "" - - @staticmethod - def async_initial_options_step(config_entry: ConfigEntry) -> str: - """Return initial options step.""" - return f"{config_entry.options['group_type']}_options" - - def async_next_step(self, step_id: str, user_input: dict[str, Any]) -> str | None: - """Return next step_id.""" - if step_id == "init": - return cast(str, user_input["group_type"]) - return None + return cast(str, options["name"]) if "name" in options else "" diff --git a/homeassistant/components/switch/config_flow.py b/homeassistant/components/switch/config_flow.py index 1adc4ec0aee..efb3baf363f 100644 --- a/homeassistant/components/switch/config_flow.py +++ b/homeassistant/components/switch/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Switch integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -14,13 +15,15 @@ from homeassistant.helpers import ( from .const import DOMAIN -STEPS = { - "init": vol.Schema( - { - vol.Required("entity_id"): selector.selector( - {"entity": {"domain": "switch"}} - ), - } +CONFIG_FLOW = { + "user": helper_config_entry_flow.HelperFlowStep( + vol.Schema( + { + vol.Required("entity_id"): selector.selector( + {"entity": {"domain": "switch"}} + ), + } + ) ) } @@ -30,16 +33,16 @@ class SwitchLightConfigFlowHandler( ): """Handle a config or options flow for Switch Light.""" - steps = STEPS + config_flow = CONFIG_FLOW - def async_config_entry_title(self, user_input: dict[str, Any]) -> str: + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" registry = er.async_get(self.hass) - object_id = split_entity_id(user_input["entity_id"])[1] - entry = registry.async_get(user_input["entity_id"]) + object_id = split_entity_id(options["entity_id"])[1] + entry = registry.async_get(options["entity_id"]) if entry: return entry.name or entry.original_name or object_id - state = self.hass.states.get(user_input["entity_id"]) + state = self.hass.states.get(options["entity_id"]) if state: return state.name or object_id return object_id diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index c632ad60eae..7ef69b7f360 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -2,20 +2,32 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Mapping import copy -import types +from dataclasses import dataclass from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - FlowResult, - UnknownHandler, -) +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult, UnknownHandler + + +@dataclass +class HelperFlowStep: + """Define a helper config or options flow step.""" + + # Optional schema for requesting and validating user input. If schema validation + # fails, the step will be retried. If the schema is None, no user input is requested. + schema: vol.Schema | None + + # Optional function to identify next step. + # The next_step function is called if the schema validates successfully or if no + # schema is defined. The next_step function is passed the union of config entry + # options and user input from previous steps. + # If next_step returns None, the flow is ended with RESULT_TYPE_CREATE_ENTRY. + next_step: Callable[[dict[str, Any]], str | None] = lambda _: None class HelperCommonFlowHandler: @@ -24,61 +36,64 @@ class HelperCommonFlowHandler: def __init__( self, handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, + flow: dict[str, HelperFlowStep], config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" + self._flow = flow self._handler = handler self._options = dict(config_entry.options) if config_entry is not None else {} async def async_step( - self, step_id: str, _user_input: dict[str, Any] | None = None + self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a step.""" - errors = None - if _user_input is not None: - errors = {} - try: - user_input = await self._handler.async_validate_input( - self._handler.hass, step_id, _user_input - ) - except vol.Invalid as exc: - errors["base"] = str(exc) - else: - self._options.update(user_input) - if ( - next_step_id := self._handler.async_next_step(step_id, user_input) - ) is None: - title = self._handler.async_config_entry_title(user_input) - return self._handler.async_create_entry( - title=title, data=self._options - ) - return self._handler.async_show_form( - step_id=next_step_id, data_schema=self._handler.steps[next_step_id] - ) + next_step_id: str = step_id - schema = dict(self._handler.steps[step_id].schema) - for key in list(schema): - if key in self._options and isinstance(key, vol.Marker): - new_key = copy.copy(key) - new_key.description = {"suggested_value": self._options[key]} - val = schema.pop(key) - schema[new_key] = val + if user_input is not None: + # User input was validated successfully, update options + self._options.update(user_input) + if self._flow[next_step_id].next_step and ( + user_input is not None or self._flow[next_step_id].schema is None + ): + # Get next step + next_step_id_or_end_flow = self._flow[next_step_id].next_step(self._options) + if next_step_id_or_end_flow is None: + # Flow done, create entry or update config entry options + return self._handler.async_create_entry(data=self._options) + + next_step_id = next_step_id_or_end_flow + + if (data_schema := self._flow[next_step_id].schema) and data_schema.schema: + # Copy the schema, then set suggested field values to saved options + schema = dict(data_schema.schema) + for key in list(schema): + if key in self._options and isinstance(key, vol.Marker): + # Copy the marker to not modify the flow schema + new_key = copy.copy(key) + new_key.description = {"suggested_value": self._options[key]} + val = schema.pop(key) + schema[new_key] = val + data_schema = vol.Schema(schema) + + # Show form for next step return self._handler.async_show_form( - step_id=step_id, data_schema=vol.Schema(schema), errors=errors + step_id=next_step_id, data_schema=data_schema ) class HelperConfigFlowHandler(config_entries.ConfigFlow): """Handle a config flow for helper integrations.""" - steps: dict[str, vol.Schema] + config_flow: dict[str, HelperFlowStep] + options_flow: dict[str, HelperFlowStep] | None = None VERSION = 1 # pylint: disable-next=arguments-differ def __init_subclass__(cls, **kwargs: Any) -> None: - """Initialize a subclass, register if possible.""" + """Initialize a subclass.""" super().__init_subclass__(**kwargs) @callback @@ -86,30 +101,21 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): config_entry: config_entries.ConfigEntry, ) -> config_entries.OptionsFlow: """Get the options flow for this handler.""" - if ( - cls.async_initial_options_step - is HelperConfigFlowHandler.async_initial_options_step - ): + if cls.options_flow is None: raise UnknownHandler - return HelperOptionsFlowHandler( - config_entry, - cls.steps, - cls.async_config_entry_title, - cls.async_initial_options_step, - cls.async_next_step, - cls.async_validate_input, - ) + return HelperOptionsFlowHandler(config_entry, cls.options_flow) # Create an async_get_options_flow method cls.async_get_options_flow = _async_get_options_flow # type: ignore[assignment] + # Create flow step methods for each step defined in the flow schema - for step in cls.steps: - setattr(cls, f"async_step_{step}", cls.async_step) + for step in cls.config_flow: + setattr(cls, f"async_step_{step}", cls._async_step) def __init__(self) -> None: """Initialize config flow.""" - self._common_handler = HelperCommonFlowHandler(self, None) + self._common_handler = HelperCommonFlowHandler(self, self.config_flow, None) @classmethod @callback @@ -117,50 +123,34 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): cls, config_entry: config_entries.ConfigEntry ) -> bool: """Return options flow support for this handler.""" - return ( - cls.async_initial_options_step - is not HelperConfigFlowHandler.async_initial_options_step - ) + return cls.options_flow is not None - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step.""" - return await self.async_step() - - async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle a step.""" - step_id = self.cur_step["step_id"] if self.cur_step else "init" + async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle a config flow step.""" + step_id = self.cur_step["step_id"] if self.cur_step else "user" result = await self._common_handler.async_step(step_id, user_input) - if result["type"] == RESULT_TYPE_CREATE_ENTRY: - result["options"] = result["data"] - result["data"] = {} + return result # pylint: disable-next=no-self-use @abstractmethod - def async_config_entry_title(self, user_input: dict[str, Any]) -> str: - """Return config entry title.""" + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title. - # pylint: disable-next=no-self-use - def async_next_step(self, step_id: str, user_input: dict[str, Any]) -> str | None: - """Return next step_id, or None to finish the flow.""" - return None + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ - @staticmethod @callback - def async_initial_options_step( - config_entry: config_entries.ConfigEntry, - ) -> str: - """Return initial step_id of options flow.""" - raise UnknownHandler - - # pylint: disable-next=no-self-use - async def async_validate_input( - self, hass: HomeAssistant, step_id: str, user_input: dict[str, Any] - ) -> dict[str, Any]: - """Validate user input.""" - return user_input + def async_create_entry( # pylint: disable=arguments-differ + self, + data: Mapping[str, Any], + **kwargs: Any, + ) -> FlowResult: + """Finish config flow and create a config entry.""" + return super().async_create_entry( + data={}, options=data, title=self.async_config_entry_title(data), **kwargs + ) class HelperOptionsFlowHandler(config_entries.OptionsFlow): @@ -169,35 +159,25 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): def __init__( self, config_entry: config_entries.ConfigEntry, - steps: dict[str, vol.Schema], - config_entry_title: Callable[[Any, dict[str, Any]], str], - initial_step: Callable[[config_entries.ConfigEntry], str], - next_step: Callable[[Any, str, dict[str, Any]], str | None], - validate: Callable[ - [Any, HomeAssistant, str, dict[str, Any]], Awaitable[dict[str, Any]] - ], + options_flow: dict[str, vol.Schema], ) -> None: """Initialize options flow.""" - self._common_handler = HelperCommonFlowHandler(self, config_entry) + self._common_handler = HelperCommonFlowHandler(self, options_flow, config_entry) self._config_entry = config_entry - self._initial_step = initial_step(config_entry) - self.async_config_entry_title = types.MethodType(config_entry_title, self) - self.async_next_step = types.MethodType(next_step, self) - self.async_validate_input = types.MethodType(validate, self) - self.steps = steps - for step in self.steps: - if step == "init": - continue - setattr(self, f"async_step_{step}", self.async_step) - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the initial step.""" - return await self.async_step(user_input) + for step in options_flow: + setattr(self, f"async_step_{step}", self._async_step) - async def async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle a step.""" + async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: + """Handle an options flow step.""" # pylint: disable-next=unsubscriptable-object # self.cur_step is a dict - step_id = self.cur_step["step_id"] if self.cur_step else self._initial_step + step_id = self.cur_step["step_id"] if self.cur_step else "init" return await self._common_handler.async_step(step_id, user_input) + + @callback + def async_create_entry( # pylint: disable=arguments-differ + self, + **kwargs: Any, + ) -> FlowResult: + """Finish config flow and create a config entry.""" + return super().async_create_entry(title="", **kwargs) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index cc97ff8c95f..121ecc717e6 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -142,7 +142,7 @@ async def test_options(hass: HomeAssistant, group_type, member_state) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == f"{group_type}_options" + assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") == members1 assert "name" not in result["data_schema"].schema From 3dc100e81661a1f1ba519e51ac4a28e0f501b0f5 Mon Sep 17 00:00:00 2001 From: escoand Date: Wed, 9 Mar 2022 14:42:00 +0100 Subject: [PATCH 0331/1054] Removing myself from Samsung TV codeowners (#67903) --- CODEOWNERS | 4 ++-- homeassistant/components/samsungtv/manifest.json | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3313fd7bfb9..3ad23837c43 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -850,8 +850,8 @@ tests/components/ruckus_unleashed/* @gabe565 homeassistant/components/safe_mode/* @home-assistant/core tests/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl -homeassistant/components/samsungtv/* @escoand @chemelli74 @epenet -tests/components/samsungtv/* @escoand @chemelli74 @epenet +homeassistant/components/samsungtv/* @chemelli74 @epenet +tests/components/samsungtv/* @chemelli74 @epenet homeassistant/components/scene/* @home-assistant/core tests/components/scene/* @home-assistant/core homeassistant/components/schluter/* @prairieapps diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 93ae1c0d73a..b15a5e5d56e 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -28,7 +28,6 @@ {"macaddress": "8CEA48*"} ], "codeowners": [ - "@escoand", "@chemelli74", "@epenet" ], From a8ad9fc5f4837dff5d56822e1f3b0831acddebaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Mar 2022 15:41:14 +0100 Subject: [PATCH 0332/1054] Mock Radio Browser entry setup in onboarding tests (#67905) --- tests/components/onboarding/test_views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 976e2b84c68..2ff08f0988b 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -390,12 +390,16 @@ async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_cl client = await hass_client() - resp = await client.post("/api/onboarding/core_config") + with patch( + "homeassistant.components.radio_browser.async_setup_entry", return_value=True + ) as mock_setup: + resp = await client.post("/api/onboarding/core_config") assert resp.status == 200 await hass.async_block_till_done() assert len(hass.config_entries.async_entries("radio_browser")) == 1 + assert len(mock_setup.mock_calls) == 1 async def test_onboarding_core_sets_up_rpi_power( From e02c21a4de1adfb1c12e0b8e856e29ff3568e57e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Mar 2022 16:45:09 +0100 Subject: [PATCH 0333/1054] Remove unused constant from group (#67910) --- homeassistant/components/group/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index e5dbb3c3630..16958be9663 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -59,14 +59,6 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" -PLATFORMS_CONFIG_ENTRY = [ - Platform.BINARY_SENSOR, - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.MEDIA_PLAYER, -] - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, From eee0c5372cb2ce1ba5c7deb1c5518565a3f8608e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 9 Mar 2022 20:29:20 +0200 Subject: [PATCH 0334/1054] Use import instead of hass in Shelly tests (#67909) --- tests/components/shelly/test_cover.py | 11 ++++++----- tests/components/shelly/test_switch.py | 9 +++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py index f5b25ce6cb5..8813de200da 100644 --- a/tests/components/shelly/test_cover.py +++ b/tests/components/shelly/test_cover.py @@ -13,6 +13,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.helpers.entity_component import async_update_entity ROLLER_BLOCK_ID = 1 @@ -71,12 +72,12 @@ async def test_block_device_update(hass, coap_wrapper, monkeypatch): await hass.async_block_till_done() monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 0) - await hass.helpers.entity_component.async_update_entity("cover.test_name") + await async_update_entity(hass, "cover.test_name") await hass.async_block_till_done() assert hass.states.get("cover.test_name").state == STATE_CLOSED monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 100) - await hass.helpers.entity_component.async_update_entity("cover.test_name") + await async_update_entity(hass, "cover.test_name") await hass.async_block_till_done() assert hass.states.get("cover.test_name").state == STATE_OPEN @@ -165,12 +166,12 @@ async def test_rpc_device_update(hass, rpc_wrapper, monkeypatch): await hass.async_block_till_done() monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "closed") - await hass.helpers.entity_component.async_update_entity("cover.test_cover_0") + await async_update_entity(hass, "cover.test_cover_0") await hass.async_block_till_done() assert hass.states.get("cover.test_cover_0").state == STATE_CLOSED monkeypatch.setitem(rpc_wrapper.device.status["cover:0"], "state", "open") - await hass.helpers.entity_component.async_update_entity("cover.test_cover_0") + await async_update_entity(hass, "cover.test_cover_0") await hass.async_block_till_done() assert hass.states.get("cover.test_cover_0").state == STATE_OPEN @@ -186,6 +187,6 @@ async def test_rpc_device_no_position_control(hass, rpc_wrapper, monkeypatch): ) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity("cover.test_cover_0") + await async_update_entity(hass, "cover.test_cover_0") await hass.async_block_till_done() assert hass.states.get("cover.test_cover_0").state == STATE_UNKNOWN diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index fc61102507b..cb93d9dace5 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -7,6 +7,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.helpers.entity_component import async_update_entity RELAY_BLOCK_ID = 0 @@ -47,16 +48,12 @@ async def test_block_device_update(hass, coap_wrapper, monkeypatch): await hass.async_block_till_done() monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", False) - await hass.helpers.entity_component.async_update_entity( - "switch.test_name_channel_1" - ) + await async_update_entity(hass, "switch.test_name_channel_1") await hass.async_block_till_done() assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", True) - await hass.helpers.entity_component.async_update_entity( - "switch.test_name_channel_1" - ) + await async_update_entity(hass, "switch.test_name_channel_1") await hass.async_block_till_done() assert hass.states.get("switch.test_name_channel_1").state == STATE_ON From 251691f5b77029dd917dda0c4c4cbc23548fdd6e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 9 Mar 2022 20:21:31 +0100 Subject: [PATCH 0335/1054] Make sure blueprint cache is flushed on script reload (#67899) --- homeassistant/components/script/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 593e585c4a5..2b9c9976ce4 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -175,7 +175,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Call a service to reload scripts.""" if (conf := await component.async_prepare_reload()) is None: return - + async_get_blueprints(hass).async_reset_cache() await _async_process_config(hass, conf, component) async def turn_on_service(service: ServiceCall) -> None: From 9f12ee7b2dce6fad6f1ae98b959cc98531ac4dac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Mar 2022 20:22:10 +0100 Subject: [PATCH 0336/1054] Update radios to 0.1.1 (#67902) --- homeassistant/components/radio_browser/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/radio_browser/manifest.json b/homeassistant/components/radio_browser/manifest.json index 865d8b25ab1..9c6858ae27e 100644 --- a/homeassistant/components/radio_browser/manifest.json +++ b/homeassistant/components/radio_browser/manifest.json @@ -3,7 +3,7 @@ "name": "Radio Browser", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/radio", - "requirements": ["radios==0.1.0"], + "requirements": ["radios==0.1.1"], "codeowners": ["@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 01b81d8fabc..85efc83287a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2030,7 +2030,7 @@ quantum-gateway==0.0.6 rachiopy==1.0.3 # homeassistant.components.radio_browser -radios==0.1.0 +radios==0.1.1 # homeassistant.components.radiotherm radiotherm==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdde0853f51..ff0e0c48d9b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1297,7 +1297,7 @@ pyzerproc==0.4.8 rachiopy==1.0.3 # homeassistant.components.radio_browser -radios==0.1.0 +radios==0.1.1 # homeassistant.components.rainmachine regenmaschine==2022.01.0 From cc11562fa251ddf39a9e030dcd929eadd73e4541 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 9 Mar 2022 20:31:27 +0100 Subject: [PATCH 0337/1054] Bump volvooncall to 0.10.0 (#67918) --- homeassistant/components/volvooncall/manifest.json | 2 +- requirements_all.txt | 2 +- script/pip_check | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 48caa75a824..93b7642425c 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -2,7 +2,7 @@ "domain": "volvooncall", "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": ["volvooncall==0.9.1"], + "requirements": ["volvooncall==0.10.0"], "codeowners": ["@molobrakos", "@decompil3d"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"] diff --git a/requirements_all.txt b/requirements_all.txt index 85efc83287a..72b130c1c68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2380,7 +2380,7 @@ vilfo-api-client==0.3.2 volkszaehler==0.2.1 # homeassistant.components.volvooncall -volvooncall==0.9.1 +volvooncall==0.10.0 # homeassistant.components.verisure vsure==1.7.3 diff --git a/script/pip_check b/script/pip_check index 033638ed479..9d5ec6c87ec 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=5 +DEPENDENCY_CONFLICTS=4 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 542c3cbf90a8daca58ba5438b36d2ed3e59cb486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 9 Mar 2022 20:51:39 +0100 Subject: [PATCH 0338/1054] Address late Airzone PR comments and fix Fahrenheit temperatures (#67904) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone: address late PR comments Signed-off-by: Álvaro Fernández Rojas * airzone: fix fahrenheit temperatures Signed-off-by: Álvaro Fernández Rojas * airzone: add changes suggested by @MartinHjelmare Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/__init__.py | 10 ++++++++++ homeassistant/components/airzone/config_flow.py | 15 ++++++++------- homeassistant/components/airzone/const.py | 9 +++++++++ homeassistant/components/airzone/coordinator.py | 2 +- homeassistant/components/airzone/sensor.py | 16 ++++++++-------- tests/components/airzone/test_config_flow.py | 6 +++--- tests/components/airzone/test_init.py | 10 +++++----- tests/components/airzone/util.py | 16 ++++++++-------- 8 files changed, 52 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 183a759122a..687e4426d76 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from aioairzone.common import ConnectionOptions +from aioairzone.const import AZD_ZONES from aioairzone.localapi_device import AirzoneLocalApi from homeassistant.config_entries import ConfigEntry @@ -37,6 +38,15 @@ class AirzoneEntity(CoordinatorEntity): } self.system_zone_id = system_zone_id + def get_zone_value(self, key): + """Return zone value by key.""" + value = None + if self.system_zone_id in self.coordinator.data[AZD_ZONES]: + zone = self.coordinator.data[AZD_ZONES][self.system_zone_id] + if key in zone: + value = zone[key] + return value + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Airzone from a config entry.""" diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index c78f43a7db7..47281deab73 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -34,14 +34,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) + airzone = AirzoneLocalApi( + aiohttp_client.async_get_clientsession(self.hass), + ConnectionOptions( + user_input[CONF_HOST], + user_input[CONF_PORT], + ), + ) + try: - airzone = AirzoneLocalApi( - aiohttp_client.async_get_clientsession(self.hass), - ConnectionOptions( - user_input[CONF_HOST], - user_input[CONF_PORT], - ), - ) await airzone.validate_airzone() except (ClientConnectorError, InvalidHost): errors["base"] = "cannot_connect" diff --git a/homeassistant/components/airzone/const.py b/homeassistant/components/airzone/const.py index 8c48cc1aca1..f1818334914 100644 --- a/homeassistant/components/airzone/const.py +++ b/homeassistant/components/airzone/const.py @@ -2,8 +2,17 @@ from typing import Final +from aioairzone.common import TemperatureUnit + +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + DOMAIN: Final = "airzone" MANUFACTURER: Final = "Airzone" AIOAIRZONE_DEVICE_TIMEOUT_SEC: Final = 10 DEFAULT_LOCAL_API_PORT: Final = 3000 + +TEMP_UNIT_LIB_TO_HASS: Final[dict[TemperatureUnit, str]] = { + TemperatureUnit.CELSIUS: TEMP_CELSIUS, + TemperatureUnit.FAHRENHEIT: TEMP_FAHRENHEIT, +} diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index b12305e7913..40d87a16b27 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -37,6 +37,6 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator): async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): try: await self.airzone.update_airzone() - return self.airzone.data() except ClientConnectorError as error: raise UpdateFailed(error) from error + return self.airzone.data() diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index e860eba1ad1..3d8912641cb 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_ZONES +from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_TEMP_UNIT, AZD_ZONES from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirzoneEntity -from .const import DOMAIN +from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( @@ -84,12 +84,12 @@ class AirzoneSensor(AirzoneEntity, SensorEntity): self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}" self.entity_description = description + if description.key == AZD_TEMP: + self._attr_native_unit_of_measurement = TEMP_UNIT_LIB_TO_HASS.get( + self.get_zone_value(AZD_TEMP_UNIT) + ) + @property def native_value(self): """Return the state.""" - value = None - if self.system_zone_id in self.coordinator.data[AZD_ZONES]: - zone = self.coordinator.data[AZD_ZONES][self.system_zone_id] - if self.entity_description.key in zone: - value = zone[self.entity_description.key] - return value + return self.get_zone_value(self.entity_description.key) diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index bb878ea6fc8..aeea960c554 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -53,13 +53,13 @@ async def test_form(hass): async def test_form_duplicated_id(hass): """Test setting up duplicated entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + with patch( "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): - entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py index 8443148af65..a529907a246 100644 --- a/tests/components/airzone/test_init.py +++ b/tests/components/airzone/test_init.py @@ -13,15 +13,15 @@ from tests.common import MockConfigEntry async def test_unload_entry(hass): """Test unload.""" + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="airzone_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + with patch( "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): - config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="airzone_unique_id", data=CONFIG - ) - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 268a7fd1d2b..bc0332c959c 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -107,17 +107,17 @@ HVAC_MOCK = { API_ZONE_ID: 4, API_NAME: "Despacho", API_ON: 0, - API_MAX_TEMP: 30, - API_MIN_TEMP: 15, - API_SET_POINT: 19.5, - API_ROOM_TEMP: 21.2, + API_MAX_TEMP: 86, + API_MIN_TEMP: 59, + API_SET_POINT: 67.1, + API_ROOM_TEMP: 70.16, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, API_HEAT_STAGES: 1, API_HEAT_STAGE: 1, API_HUMIDITY: 36, - API_UNITS: 0, + API_UNITS: 1, API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, @@ -153,12 +153,12 @@ async def async_init_integration( ): """Set up the Airzone integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + with patch( "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): - entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() From aaeaed4117d12ee2e780dfceed53cb52875eec1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Mar 2022 21:39:43 +0100 Subject: [PATCH 0339/1054] Compatibility for "device_state_attributes" which was deprecated in 2021.4 has been removed (#67837) * Small performance tweaks to _async_write_ha_state - Only call self.available once per cycle - Only call self.device_state_attributes once per update cycle - Do not check for device_state_attributes if extra_state_attributes is not None * drop backcompat * remove prop --- homeassistant/helpers/entity.py | 34 +++++++-------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a554a093c5c..30df64d3e88 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -288,9 +288,6 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False - # If we reported this entity is using deprecated device_state_attributes - _deprecated_device_state_attributes_reported = False - # Protect for multiple updates _update_staged = False @@ -552,9 +549,9 @@ class Entity(ABC): self._async_write_ha_state() - def _stringify_state(self) -> str: + def _stringify_state(self, available: bool) -> str: """Convert state to string.""" - if not self.available: + if not available: return STATE_UNAVAILABLE if (state := self.state) is None: return STATE_UNKNOWN @@ -587,30 +584,13 @@ class Entity(ABC): attr = self.capability_attributes attr = dict(attr) if attr else {} - state = self._stringify_state() - if self.available: + available = self.available # only call self.available once per update cycle + state = self._stringify_state(available) + if available: attr.update(self.state_attributes or {}) - extra_state_attributes = self.extra_state_attributes - # Backwards compatibility for "device_state_attributes" deprecated in 2021.4 - # Warning added in 2021.12, will be removed in 2022.4 - if ( - self.device_state_attributes is not None - and not self._deprecated_device_state_attributes_reported - ): - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Entity %s (%s) implements device_state_attributes. Please %s", - self.entity_id, - type(self), - report_issue, - ) - self._deprecated_device_state_attributes_reported = True - if extra_state_attributes is None: - extra_state_attributes = self.device_state_attributes - attr.update(extra_state_attributes or {}) + attr.update(self.extra_state_attributes or {}) - unit_of_measurement = self.unit_of_measurement - if unit_of_measurement is not None: + if (unit_of_measurement := self.unit_of_measurement) is not None: attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement entry = self.registry_entry From d0afc31063a9713e8f8945e883d2deeef2dd603e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Mar 2022 00:17:10 +0000 Subject: [PATCH 0340/1054] [ci skip] Translation update --- .../components/airzone/translations/ca.json | 19 ++++++ .../components/airzone/translations/de.json | 19 ++++++ .../components/airzone/translations/el.json | 19 ++++++ .../components/airzone/translations/et.json | 19 ++++++ .../components/airzone/translations/hu.json | 19 ++++++ .../components/airzone/translations/id.json | 19 ++++++ .../components/airzone/translations/ja.json | 19 ++++++ .../components/airzone/translations/nl.json | 19 ++++++ .../components/airzone/translations/no.json | 19 ++++++ .../components/airzone/translations/pl.json | 19 ++++++ .../airzone/translations/pt-BR.json | 19 ++++++ .../components/airzone/translations/ru.json | 19 ++++++ .../airzone/translations/zh-Hant.json | 19 ++++++ .../components/fritz/translations/ja.json | 3 +- .../components/group/translations/hu.json | 62 +++++++++++++++++++ .../components/group/translations/ja.json | 62 +++++++++++++++++++ .../components/group/translations/nl.json | 44 ++++++------- .../components/group/translations/no.json | 62 +++++++++++++++++++ .../kaleidescape/translations/hu.json | 27 ++++++++ .../kaleidescape/translations/ja.json | 27 ++++++++ .../kaleidescape/translations/nl.json | 11 +++- .../kaleidescape/translations/no.json | 27 ++++++++ .../components/moon/translations/ja.json | 13 ++++ .../components/mqtt/translations/nl.json | 12 ++-- .../components/netgear/translations/nl.json | 2 +- .../components/onewire/translations/ja.json | 26 ++++++++ .../components/onewire/translations/nl.json | 6 +- .../components/risco/translations/nl.json | 8 +-- .../components/season/translations/hu.json | 14 +++++ .../components/season/translations/ja.json | 14 +++++ .../components/season/translations/nl.json | 7 +++ .../components/season/translations/no.json | 14 +++++ .../components/sensor/translations/nl.json | 4 ++ .../components/sentry/translations/nl.json | 4 +- .../components/switch/translations/ja.json | 10 +++ .../components/switch/translations/no.json | 10 +++ .../components/tuya/translations/nl.json | 2 +- .../tuya/translations/select.id.json | 5 ++ .../components/update/translations/ja.json | 3 + .../wolflink/translations/sensor.id.json | 31 ++++++++-- 40 files changed, 709 insertions(+), 48 deletions(-) create mode 100644 homeassistant/components/airzone/translations/ca.json create mode 100644 homeassistant/components/airzone/translations/de.json create mode 100644 homeassistant/components/airzone/translations/el.json create mode 100644 homeassistant/components/airzone/translations/et.json create mode 100644 homeassistant/components/airzone/translations/hu.json create mode 100644 homeassistant/components/airzone/translations/id.json create mode 100644 homeassistant/components/airzone/translations/ja.json create mode 100644 homeassistant/components/airzone/translations/nl.json create mode 100644 homeassistant/components/airzone/translations/no.json create mode 100644 homeassistant/components/airzone/translations/pl.json create mode 100644 homeassistant/components/airzone/translations/pt-BR.json create mode 100644 homeassistant/components/airzone/translations/ru.json create mode 100644 homeassistant/components/airzone/translations/zh-Hant.json create mode 100644 homeassistant/components/kaleidescape/translations/hu.json create mode 100644 homeassistant/components/kaleidescape/translations/ja.json create mode 100644 homeassistant/components/kaleidescape/translations/no.json create mode 100644 homeassistant/components/moon/translations/ja.json create mode 100644 homeassistant/components/season/translations/hu.json create mode 100644 homeassistant/components/season/translations/ja.json create mode 100644 homeassistant/components/season/translations/no.json create mode 100644 homeassistant/components/update/translations/ja.json diff --git a/homeassistant/components/airzone/translations/ca.json b/homeassistant/components/airzone/translations/ca.json new file mode 100644 index 00000000000..bbff47a9904 --- /dev/null +++ b/homeassistant/components/airzone/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Configura la integraci\u00f3 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/de.json b/homeassistant/components/airzone/translations/de.json new file mode 100644 index 00000000000..7b3f5030f06 --- /dev/null +++ b/homeassistant/components/airzone/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Richte die Airzone-Integration ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/el.json b/homeassistant/components/airzone/translations/el.json new file mode 100644 index 00000000000..7b04fe27743 --- /dev/null +++ b/homeassistant/components/airzone/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/et.json b/homeassistant/components/airzone/translations/et.json new file mode 100644 index 00000000000..307aa0de0a5 --- /dev/null +++ b/homeassistant/components/airzone/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Seadista Airzone'i sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/hu.json b/homeassistant/components/airzone/translations/hu.json new file mode 100644 index 00000000000..88e7449a1c4 --- /dev/null +++ b/homeassistant/components/airzone/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "port": "Port" + }, + "description": "Airzone integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/id.json b/homeassistant/components/airzone/translations/id.json new file mode 100644 index 00000000000..c37058bac34 --- /dev/null +++ b/homeassistant/components/airzone/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Siapkan integrasi Airzone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/ja.json b/homeassistant/components/airzone/translations/ja.json new file mode 100644 index 00000000000..71b8bf1c908 --- /dev/null +++ b/homeassistant/components/airzone/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "Airzone\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json new file mode 100644 index 00000000000..4611c23eb1e --- /dev/null +++ b/homeassistant/components/airzone/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kon niet verbinden" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Airzone integratie instellen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/no.json b/homeassistant/components/airzone/translations/no.json new file mode 100644 index 00000000000..f078016b761 --- /dev/null +++ b/homeassistant/components/airzone/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Sett opp Airzone-integrasjon." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/pl.json b/homeassistant/components/airzone/translations/pl.json new file mode 100644 index 00000000000..38dc359b248 --- /dev/null +++ b/homeassistant/components/airzone/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Skonfiguruj integracj\u0119 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/pt-BR.json b/homeassistant/components/airzone/translations/pt-BR.json new file mode 100644 index 00000000000..f063b3461ae --- /dev/null +++ b/homeassistant/components/airzone/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Configure a integra\u00e7\u00e3o Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/ru.json b/homeassistant/components/airzone/translations/ru.json new file mode 100644 index 00000000000..6032b0bdf00 --- /dev/null +++ b/homeassistant/components/airzone/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/zh-Hant.json b/homeassistant/components/airzone/translations/zh-Hant.json new file mode 100644 index 00000000000..01e2db9730b --- /dev/null +++ b/homeassistant/components/airzone/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Airzone \u6574\u5408\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 84cbdb3af76..afbda92d3fe 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -10,7 +10,8 @@ "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "upnp_not_configured": "\u30c7\u30d0\u30a4\u30b9\u306bUPnP\u306e\u8a2d\u5b9a\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index 7438229f852..13faa9d62f3 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "A csoport tagjai", + "name": "Csoport neve" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "cover_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "fan": { + "data": { + "entities": "A csoport tagjai", + "name": "A csoport elnevez\u00e9se" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "fan_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "init": { + "data": { + "group_type": "A csoport t\u00edpusa" + }, + "description": "A csoport t\u00edpusa" + }, + "light": { + "data": { + "entities": "A csoport tagjai", + "name": "A csoport elnevez\u00e9se" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "light_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "media_player": { + "data": { + "entities": "A csoport tagjai", + "name": "A csoport elnevez\u00e9se" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "media_player_options": { + "data": { + "entities": "A csoport tagjai" + }, + "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + } + } + }, "state": { "_": { "closed": "Z\u00e1rva", diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 02aff2e2b84..39fe05e18ec 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "cover_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "fan": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "fan_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "init": { + "data": { + "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e" + }, + "light": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "light_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "media_player": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "media_player_options": { + "data": { + "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" + }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + } + } + }, "state": { "_": { "closed": "\u9589\u9396", diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index 5bec96110c2..4b1c8ef4db0 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -3,61 +3,61 @@ "step": { "cover": { "data": { - "entities": "Groep leden", - "name": "Groep naam" + "entities": "Groepsleden", + "name": "Groepsnaam" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "cover_options": { "data": { - "entities": "Groep leden" + "entities": "Groepsleden" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "fan": { "data": { - "entities": "Groep leden", - "name": "Groep naam" + "entities": "Groepsleden", + "name": "Groepsnaam" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "fan_options": { "data": { - "entities": "Groep leden" + "entities": "Groepsleden" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "init": { "data": { - "group_type": "Groep type" + "group_type": "Groepstype" }, - "description": "Selecteer groep type" + "description": "Selecteer groepstype" }, "light": { "data": { - "entities": "Groep leden", - "name": "Groep naam" + "entities": "Groepsleden", + "name": "Groepsnaam" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "light_options": { "data": { - "entities": "Groep leden" + "entities": "Groepsleden" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "media_player": { "data": { - "entities": "Groep leden\n", - "name": "Groep naam" + "entities": "Groepsleden", + "name": "Groepsnaam" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" }, "media_player_options": { "data": { - "entities": "Groep leden" + "entities": "Groepsleden" }, - "description": "Selecteer groep opties" + "description": "Selecteer groepsopties" } } }, diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index 763021190c1..70a46ee09c5 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -1,4 +1,66 @@ { + "config": { + "step": { + "cover": { + "data": { + "entities": "Gruppemedlemmer", + "name": "Gruppenavn" + }, + "description": "Velg gruppealternativer" + }, + "cover_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "fan": { + "data": { + "entities": "Gruppemedlemmer", + "name": "Gruppenavn" + }, + "description": "Velg gruppealternativer" + }, + "fan_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "init": { + "data": { + "group_type": "Gruppetype" + }, + "description": "Velg gruppetype" + }, + "light": { + "data": { + "entities": "Gruppemedlemmer", + "name": "Gruppenavn" + }, + "description": "Velg gruppealternativer" + }, + "light_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + }, + "media_player": { + "data": { + "entities": "Gruppemedlemmer", + "name": "Gruppenavn" + }, + "description": "Velg gruppealternativer" + }, + "media_player_options": { + "data": { + "entities": "Gruppemedlemmer" + }, + "description": "Velg gruppealternativer" + } + } + }, "state": { "_": { "closed": "Lukket", diff --git a/homeassistant/components/kaleidescape/translations/hu.json b/homeassistant/components/kaleidescape/translations/hu.json new file mode 100644 index 00000000000..953de8119ed --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unsupported": "Nem t\u00e1mogatott eszk\u00f6z" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unsupported": "Nem t\u00e1mogatott eszk\u00f6z" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {name} nev\u0171, {model} t\u00edpus\u00fa lej\u00e1tsz\u00f3t?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "title": "Kaleidescape be\u00e1ll\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/ja.json b/homeassistant/components/kaleidescape/translations/ja.json new file mode 100644 index 00000000000..368a1565648 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unsupported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unsupported": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "{model} \u30d7\u30ec\u30a4\u30e4\u30fc\u540d {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", + "title": "\u30ab\u30ec\u30a4\u30c9\u30b9\u30b1\u30fc\u30d7(Kaleidescape)" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "title": "\u30ab\u30ec\u30a4\u30c9\u30b9\u30b1\u30fc\u30d7(Kaleidescape)\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/nl.json b/homeassistant/components/kaleidescape/translations/nl.json index bc0facb9dde..2b2491deb85 100644 --- a/homeassistant/components/kaleidescape/translations/nl.json +++ b/homeassistant/components/kaleidescape/translations/nl.json @@ -2,18 +2,25 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "Configuratiestroom is al bezig", "unknown": "Onverwachte fout", - "unsupported": "Niet-ondersteund apparaat\n" + "unsupported": "Niet-ondersteund apparaat" }, "error": { "cannot_connect": "Verbinding mislukt", "unsupported": "Niet-ondersteund apparaat" }, + "flow_title": "{model} ({name})", "step": { + "discovery_confirm": { + "description": "Wil je de speler {model} met de naam {name} instellen?", + "title": "Kaleidescape" + }, "user": { "data": { "host": "Host" - } + }, + "title": "Kaleidescape configuratie" } } } diff --git a/homeassistant/components/kaleidescape/translations/no.json b/homeassistant/components/kaleidescape/translations/no.json new file mode 100644 index 00000000000..c07276eeeb1 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "unknown": "Uventet feil", + "unsupported": "Enhet som ikke st\u00f8ttes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unsupported": "Enhet som ikke st\u00f8ttes" + }, + "flow_title": "{model} ( {name} )", + "step": { + "discovery_confirm": { + "description": "Vil du sette opp {model} -spilleren med navnet {name} ?", + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "Vert" + }, + "title": "Kaleidescape-oppsett" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json new file mode 100644 index 00000000000..6544580781f --- /dev/null +++ b/homeassistant/components/moon/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "title": "\u6708" +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 542ae467e7a..40448942a9f 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -71,14 +71,14 @@ "birth_enable": "Geboortebericht inschakelen", "birth_payload": "Birth message payload", "birth_qos": "Birth message QoS", - "birth_retain": "Birth message behouden", + "birth_retain": "Verbind bericht onthouden", "birth_topic": "Birth message onderwerp", "discovery": "Discovery inschakelen", - "will_enable": "Will message inschakelen", - "will_payload": "Will message payload", - "will_qos": "Will message QoS", - "will_retain": "Will message behouden", - "will_topic": "Will message topic" + "will_enable": "Offline bericht inschakelen", + "will_payload": "Offline bericht inhoud", + "will_qos": "Offline bericht QoS", + "will_retain": "Offline bericht onthouden", + "will_topic": "Offline bericht topic" }, "description": "Detectie - Als detectie is ingeschakeld (aanbevolen), zal Home Assistant automatisch apparaten en entiteiten detecteren die hun configuratie publiceren op de MQTT-broker. Als detectie is uitgeschakeld, moet alle configuratie handmatig worden uitgevoerd.\n Birth message - Het birth message wordt elke keer dat Home Assistant (opnieuw) verbinding maakt met de MQTT-broker, verzonden.\n Will message - Het will message wordt telkens verzonden wanneer Home Assistant de verbinding met de broker verliest, zowel in het geval van een schone (bijv. Home Assistant wordt uitgeschakeld) als in geval van een onjuiste (bijv. Home Assistant crasht of verliest de netwerkverbinding) verbroken verbinding.", "title": "MQTT-opties" diff --git a/homeassistant/components/netgear/translations/nl.json b/homeassistant/components/netgear/translations/nl.json index 7333a0cd0ee..1d0ae357a8f 100644 --- a/homeassistant/components/netgear/translations/nl.json +++ b/homeassistant/components/netgear/translations/nl.json @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "consider_home": "Overweeg thuis tijd (seconden)" + "consider_home": "Aantal seconden dat wordt gewacht voordat een apparaat als afwezig wordt beschouwd" }, "description": "Optionele instellingen opgeven", "title": "Netgear" diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 2aa624fbfb6..c948a9567b0 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -22,5 +22,31 @@ "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } + }, + "options": { + "error": { + "device_not_selected": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "step": { + "ack_no_options": { + "description": "SysBus\u306e\u5b9f\u88c5\u306b\u95a2\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093", + "title": "OneWire SysBus\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + }, + "configure_device": { + "data": { + "precision": "\u30bb\u30f3\u30b5\u30fc\u306e\u7cbe\u5ea6" + }, + "description": "{sensor_id} \u306e\u30bb\u30f3\u30b5\u30fc\u7cbe\u5ea6\u3092\u9078\u629e", + "title": "OneWire\u30bb\u30f3\u30b5\u30fc\u306e\u7cbe\u5ea6" + }, + "device_selection": { + "data": { + "clear_device_options": "\u5168\u30c7\u30d0\u30a4\u30b9\u306e\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30af\u30ea\u30a2\u3059\u308b", + "device_selection": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + }, + "description": "\u3069\u306e\u3088\u3046\u306a\u51e6\u7406\u30b9\u30c6\u30c3\u30d7\u3092\u8e0f\u3080\u306e\u304b\u9078\u629e", + "title": "OneWire\u30c7\u30d0\u30a4\u30b9\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index 19dadbafe16..cc310900d92 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -29,8 +29,8 @@ }, "step": { "ack_no_options": { - "description": "Er zijn geen opties voor de SysBus-implementatie", - "title": "OneWire SysBus-opties" + "description": "Er zijn geen opties voor de SysBus implementatie", + "title": "OneWire SysBus opties" }, "configure_device": { "data": { @@ -44,7 +44,7 @@ "clear_device_options": "Wis alle apparaatconfiguraties", "device_selection": "Selecteer apparaten om te configureren" }, - "description": "Selecteer welke configuratiestappen moeten doorlopen", + "description": "Selecteer welke configuratiestappen moeten worden doorlopen", "title": "OneWire-apparaatopties" } } diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index 6eeb5fff2e9..fdf1ee5ad23 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -28,13 +28,13 @@ "armed_night": "Ingeschakeld nacht" }, "description": "Selecteer in welke staat u uw Risco-alarm wilt instellen wanneer u het Home Assistant-alarm inschakelt", - "title": "Wijs Home Assistant-staten toe aan Risco-staten" + "title": "Wijs Home Assistant statussen toe aan Risco status" }, "init": { "data": { "code_arm_required": "PIN-code vereist om in te schakelen", "code_disarm_required": "PIN-code vereist om uit te schakelen", - "scan_interval": "Polling-interval (in seconden)" + "scan_interval": "Hoe vaak moet Risco worden ververst (in seconden)" }, "title": "Configureer opties" }, @@ -47,8 +47,8 @@ "arm": "Ingeschakeld (AFWEZIG)", "partial_arm": "Gedeeltelijk ingeschakeld (AANWEZIG)" }, - "description": "Selecteer welke staat uw Home Assistant alarm zal melden voor elke staat gemeld door Risco", - "title": "Wijs Risco-staten toe aan Home Assistant-staten" + "description": "Selecteer welke status uw Home Assistant alarm zal melden voor elke status gemeld door Risco", + "title": "Wijs Risco status toe aan Home Assistant statussen" } } } diff --git a/homeassistant/components/season/translations/hu.json b/homeassistant/components/season/translations/hu.json new file mode 100644 index 00000000000..11bbd17ad6c --- /dev/null +++ b/homeassistant/components/season/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "type": "Az \u00e9vszak meghat\u00e1roz\u00e1s\u00e1nak t\u00edpusa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/ja.json b/homeassistant/components/season/translations/ja.json new file mode 100644 index 00000000000..006f6c8146d --- /dev/null +++ b/homeassistant/components/season/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "type": "\u30b7\u30fc\u30ba\u30f3\u5b9a\u7fa9\u306e\u7a2e\u985e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json index 933caddaf13..63067c8a814 100644 --- a/homeassistant/components/season/translations/nl.json +++ b/homeassistant/components/season/translations/nl.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Dienst is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "type": "Type sessie definitie" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/no.json b/homeassistant/components/season/translations/no.json new file mode 100644 index 00000000000..2c177da8227 --- /dev/null +++ b/homeassistant/components/season/translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "type": "Type sesongdefinisjon" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index fea321fe221..69e18061858 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Huidig {entity_name} schijnbaar vermogen", "is_battery_level": "Huidige batterijniveau {entity_name}", "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie", "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", @@ -20,6 +21,7 @@ "is_power": "Huidige {entity_name}\nvermogen", "is_power_factor": "Huidige {entity_name} vermogensfactor", "is_pressure": "Huidige {entity_name} druk", + "is_reactive_power": "Huidig {entity_name} blindvermogen", "is_signal_strength": "Huidige {entity_name} signaalsterkte", "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", @@ -28,6 +30,7 @@ "is_voltage": "Huidige {entity_name} spanning" }, "trigger_type": { + "apparent_power": "{entity_name} schijnbare vermogensveranderingen", "battery_level": "{entity_name} batterijniveau gewijzigd", "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd", "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", @@ -47,6 +50,7 @@ "power": "{entity_name} vermogen gewijzigd", "power_factor": "{entity_name} power factor verandert", "pressure": "{entity_name} druk gewijzigd", + "reactive_power": "{entity_name} blindvermogen veranderingen", "signal_strength": "{entity_name} signaalsterkte gewijzigd", "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json index 53f54ac1968..50af985b47c 100644 --- a/homeassistant/components/sentry/translations/nl.json +++ b/homeassistant/components/sentry/translations/nl.json @@ -26,9 +26,9 @@ "event_handled": "Stuur afgehandelde gebeurtenissen", "event_third_party_packages": "Gebeurtenissen verzenden vanuit pakketten van derden", "logging_event_level": "Het logniveau waarvoor Sentry een gebeurtenis registreert", - "logging_level": "Het logniveau Sentry zal logs opnemen als broodkruimels voor", + "logging_level": "Het logniveau hoe Sentry logs zal opnemen als \"breadcrums\" is", "tracing": "Schakel prestatietracering in", - "tracing_sample_rate": "Tracering van de steekproefsnelheid; tussen 0,0 en 1,0 (1,0 = 100%)" + "tracing_sample_rate": "Steekproefsnelheid voor prestatietracering; tussen 0,0 en 1,0 (1,0 = 100%)" } } } diff --git a/homeassistant/components/switch/translations/ja.json b/homeassistant/components/switch/translations/ja.json index f806c67bb95..d4e335257ad 100644 --- a/homeassistant/components/switch/translations/ja.json +++ b/homeassistant/components/switch/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" + }, + "description": "\u7167\u660e\u30b9\u30a4\u30c3\u30c1\u306e\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + } + } + }, "device_automation": { "action_type": { "toggle": "\u30c8\u30b0\u30eb {entity_name}", diff --git a/homeassistant/components/switch/translations/no.json b/homeassistant/components/switch/translations/no.json index a39787efe79..802df4e6a42 100644 --- a/homeassistant/components/switch/translations/no.json +++ b/homeassistant/components/switch/translations/no.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Bytt enhet" + }, + "description": "Velg bryteren for lysbryteren." + } + } + }, "device_automation": { "action_type": { "toggle": "Veksle {entity_name}", diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 88078f8e6a5..e12c3ba29f2 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -73,7 +73,7 @@ "discovery_interval": "Polling-interval van nieuwe apparaten in seconden", "list_devices": "Selecteer de te configureren apparaten of laat leeg om de configuratie op te slaan", "query_device": "Selecteer apparaat dat query-methode zal gebruiken voor snellere statusupdate", - "query_interval": "Peilinginterval van het apparaat in seconden" + "query_interval": "Ververstijd van het apparaat in seconden" }, "description": "Stel de waarden voor het pollinginterval niet te laag in, anders zullen de oproepen geen foutmelding in het logboek genereren", "title": "Configureer Tuya opties" diff --git a/homeassistant/components/tuya/translations/select.id.json b/homeassistant/components/tuya/translations/select.id.json index 9b404daf612..2427f21f58e 100644 --- a/homeassistant/components/tuya/translations/select.id.json +++ b/homeassistant/components/tuya/translations/select.id.json @@ -109,13 +109,18 @@ "small": "Kecil" }, "tuya__vacuum_mode": { + "bow": "Zig Zag", "chargego": "Kembali ke dock", + "left_bow": "Zig Zag Kiri", "left_spiral": "Spiral Kiri", "mop": "Pel", "part": "Bagian", + "partial_bow": "Zig Zag Sebagian", "pick_zone": "Pilih Zona", "point": "Titik", + "pose": "Area", "random": "Acak", + "right_bow": "Bungkuk Kanan", "right_spiral": "Spiral Kanan", "single": "Tunggal", "smart": "Cerdas", diff --git a/homeassistant/components/update/translations/ja.json b/homeassistant/components/update/translations/ja.json new file mode 100644 index 00000000000..4cb5e4959a1 --- /dev/null +++ b/homeassistant/components/update/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8" +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.id.json b/homeassistant/components/wolflink/translations/sensor.id.json index 3ceb09bf90a..7130716982e 100644 --- a/homeassistant/components/wolflink/translations/sensor.id.json +++ b/homeassistant/components/wolflink/translations/sensor.id.json @@ -1,25 +1,32 @@ { "state": { "wolflink__state": { - "1_x_warmwasser": "1 x DHW", + "1_x_warmwasser": "1 x Air Panas", "abgasklappe": "Katup saluran buang gas", + "absenkbetrieb": "Mode rendah", + "absenkstop": "Mode rendah berhenti", "aktiviert": "Diaktifkan", "antilegionellenfunktion": "Fungsi Anti-legionella", + "at_abschaltung": "Mati suhu luar", + "at_frostschutz": "Perlindungan embun beku suhu luar", "aus": "Dinonaktifkan", "auto": "Otomatis", - "auto_off_cool": "AutoOffCool", - "auto_on_cool": "AutoOnCool", - "automatik_aus": "Otomatis MATI", - "automatik_ein": "Otomatis NYALA", + "auto_off_cool": "Otomatis mati dingin", + "auto_on_cool": "Otomatis nyala dingin", + "automatik_aus": "Otomatis mati", + "automatik_ein": "Otomatis nyala", "bereit_keine_ladung": "Siap, tidak memuat", "betrieb_ohne_brenner": "Bekerja tanpa pembakar", "cooling": "Mendinginkan", "deaktiviert": "Tidak aktif", - "dhw_prior": "DHWPrior", + "dhw_prior": "Air panas sebelumnya", "eco": "Eco", "ein": "Diaktifkan", + "estrichtrocknung": "Pengeringan lapisan lantai", "externe_deaktivierung": "Penonaktifan eksternal", "fernschalter_ein": "Kontrol jarak jauh diaktifkan", + "frost_heizkreis": "Perlindungan beku sirkuit pemanasan", + "frost_warmwasser": "Perlindungan beku air panas", "frostschutz": "Perlindungan beku", "gasdruck": "Tekanan gas", "glt_betrieb": "Mode BMS", @@ -37,26 +44,38 @@ "kombigerat": "Ketel kombinasi", "kombigerat_mit_solareinbindung": "Ketel kombinasi dengan integrasi tenaga surya", "mindest_kombizeit": "Waktu kombi minimum", + "nachlauf_heizkreispumpe": "Pompa berjalan sirkuit pemanasan", + "nachspulen": "Putar ulang", "nur_heizgerat": "Hanya boiler", "parallelbetrieb": "Mode paralel", "partymodus": "Mode pesta", + "perm_cooling": "Pendinginan permanen", "permanent": "Permanen", "permanentbetrieb": "Mode permanen", "reduzierter_betrieb": "Mode terbatas", + "rt_abschaltung": "Mati suhu kamar", + "rt_frostschutz": "Perlindungan embun beku suhu kamar", + "ruhekontakt": "Kontak saat istirahat", "schornsteinfeger": "Uji emisi", "smart_grid": "SmartGrid", "smart_home": "SmartHome", + "softstart": "Mulai rendah", "solarbetrieb": "Mode surya", "sparbetrieb": "Mode ekonomi", "sparen": "Ekonomi", + "spreizung_hoch": "Perbedaan suhu sensor terlalu tinggi", + "spreizung_kf": "Sebaran sensor suhu ketel", "stabilisierung": "Stabilisasi", "standby": "Siaga", "start": "Mulai", "storung": "Kesalahan", + "taktsperre": "Anti-siklus", "telefonfernschalter": "Saklar jarak jauh per telepon", "test": "Pengujian", + "tpw": "Pemantau titik embun", "urlaubsmodus": "Mode liburan", "ventilprufung": "Uji katup", + "vorspulen": "Putar awal", "warmwasser": "Air Panas Domestik", "warmwasser_schnellstart": "Mulai cepat Air Panas Domestik", "warmwasserbetrieb": "Mode Air Panas Domestik", From f803c880aed85bb56b151dac2aad0956af3b4260 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 10 Mar 2022 07:21:22 +0100 Subject: [PATCH 0341/1054] Correct type for convert_time_to_isodate (#67846) --- homeassistant/components/here_travel_time/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 37509a0c03d..8cba2db8bc8 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,7 +1,7 @@ """Support for HERE travel time sensors.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, time, timedelta import logging import herepy @@ -443,9 +443,9 @@ class HERETravelTimeData: return attribution -def convert_time_to_isodate(timestr: str) -> str: - """Take a string like 08:00:00 and combine it with the current date.""" - combined = datetime.combine(dt.start_of_local_day(), dt.parse_time(timestr)) +def convert_time_to_isodate(simple_time: time) -> str: + """Take a time like 08:00:00 and combine it with the current date.""" + combined = datetime.combine(dt.start_of_local_day(), simple_time) if combined < datetime.now(): combined = combined + timedelta(days=1) return combined.isoformat() From e5523ef6b6102923f85773e1b1542d5d0139be33 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 10:32:49 +0100 Subject: [PATCH 0342/1054] Correct local import of paho-mqtt (#67944) * Correct local import of paho-mqtt * Remove MqttClientSetup.mqtt class attribute * Remove reference to MqttClientSetup.mqtt --- homeassistant/components/mqtt/__init__.py | 23 ++++++++++++-------- homeassistant/components/mqtt/config_flow.py | 6 ++++- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8ef62ae8bcd..96074acab37 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -13,7 +13,7 @@ import logging from operator import attrgetter import ssl import time -from typing import Any, Union, cast +from typing import TYPE_CHECKING, Any, Union, cast import uuid import attr @@ -108,6 +108,11 @@ from .models import ( ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +if TYPE_CHECKING: + # Only import for paho-mqtt type checking here, imports are done locally + # because integrations should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -754,23 +759,23 @@ class Subscription: class MqttClientSetup: """Helper class to setup the paho mqtt client from config.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - def __init__(self, config: ConfigType) -> None: """Initialize the MQTT client setup helper.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + if config[CONF_PROTOCOL] == PROTOCOL_31: - proto = self.mqtt.MQTTv31 + proto = mqtt.MQTTv31 else: - proto = self.mqtt.MQTTv311 + proto = mqtt.MQTTv311 if (client_id := config.get(CONF_CLIENT_ID)) is None: # PAHO MQTT relies on the MQTT server to generate random client IDs. # However, that feature is not mandatory so we generate our own. - client_id = self.mqtt.base62(uuid.uuid4().int, padding=22) - self._client = self.mqtt.Client(client_id, protocol=proto) + client_id = mqtt.base62(uuid.uuid4().int, padding=22) + self._client = mqtt.Client(client_id, protocol=proto) # Enable logging self._client.enable_logger() diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 99e7e9718d0..6697b17dfdc 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -319,6 +319,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): def try_connection(hass, broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + # Get the config from configuration.yaml yaml_config = hass.data.get(DATA_MQTT_CONFIG, {}) entry_config = { @@ -334,7 +338,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"): def on_connect(client_, userdata, flags, result_code): """Handle connection result.""" - result.put(result_code == MqttClientSetup.mqtt.CONNACK_ACCEPTED) + result.put(result_code == mqtt.CONNACK_ACCEPTED) client.on_connect = on_connect From 5ae48bcf747ea0d5ce2844e2bcdd8ddcae0ccb3f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 10:39:51 +0100 Subject: [PATCH 0343/1054] Add config flow for binary_sensor group (#67802) * Add config flow for binary_sensor group * Address review comments * Remove device class selection from flow * Update translation strings --- .../components/group/binary_sensor.py | 26 +++++- homeassistant/components/group/config_flow.py | 15 ++++ homeassistant/components/group/cover.py | 8 +- homeassistant/components/group/fan.py | 8 +- homeassistant/components/group/light.py | 6 +- .../components/group/media_player.py | 12 +-- homeassistant/components/group/strings.json | 79 +++++++++++-------- tests/components/group/test_config_flow.py | 43 +++++++--- 8 files changed, 137 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index de0c3d393ca..54a98a68e43 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -20,7 +21,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -49,7 +50,7 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Group Binary Sensor platform.""" + """Set up the Binary Sensor Group platform.""" async_add_entities( [ BinarySensorGroup( @@ -63,6 +64,27 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Binary Sensor Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + mode = config_entry.options[CONF_ALL] + + async_add_entities( + [ + BinarySensorGroup( + config_entry.entry_id, config_entry.title, None, entities, mode + ) + ] + ) + + class BinarySensorGroup(GroupEntity, BinarySensorEntity): """Representation of a BinarySensorGroup.""" diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 0547578b131..d1cd258b7b8 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.helpers.helper_config_entry_flow import ( ) from . import DOMAIN +from .binary_sensor import CONF_ALL def basic_group_options_schema(domain: str) -> vol.Schema: @@ -35,12 +36,24 @@ def basic_group_config_schema(domain: str) -> vol.Schema: ) +BINARY_SENSOR_OPTIONS_SCHEMA = basic_group_options_schema("binary_sensor").extend( + { + vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}), + } +) + +BINARY_SENSOR_CONFIG_SCHEMA = vol.Schema( + {vol.Required("name"): selector.selector({"text": {}})} +).extend(BINARY_SENSOR_OPTIONS_SCHEMA.schema) + + INITIAL_STEP_SCHEMA = vol.Schema( { vol.Required("group_type"): selector.selector( { "select": { "options": [ + "binary_sensor", "cover", "fan", "light", @@ -61,6 +74,7 @@ def choose_config_step(options: dict[str, Any]) -> str: CONFIG_FLOW = { "user": HelperFlowStep(INITIAL_STEP_SCHEMA, next_step=choose_config_step), + "binary_sensor": HelperFlowStep(BINARY_SENSOR_CONFIG_SCHEMA), "cover": HelperFlowStep(basic_group_config_schema("cover")), "fan": HelperFlowStep(basic_group_config_schema("fan")), "light": HelperFlowStep(basic_group_config_schema("light")), @@ -70,6 +84,7 @@ CONFIG_FLOW = { OPTIONS_FLOW = { "init": HelperFlowStep(None, next_step=choose_config_step), + "binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA), "cover": HelperFlowStep(basic_group_options_schema("cover")), "fan": HelperFlowStep(basic_group_options_schema("fan")), "light": HelperFlowStep(basic_group_options_schema("light")), diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 851079ad5e4..c2d263ab8ad 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -76,7 +76,7 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Group Cover platform.""" + """Set up the Cover Group platform.""" async_add_entities( [ CoverGroup( @@ -91,14 +91,14 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Initialize Light Switch config entry.""" + """Initialize Cover Group config entry.""" registry = er.async_get(hass) - entity_id = er.async_validate_entity_ids( + entities = er.async_validate_entity_ids( registry, config_entry.options[CONF_ENTITIES] ) async_add_entities( - [CoverGroup(config_entry.entry_id, config_entry.title, entity_id)] + [CoverGroup(config_entry.entry_id, config_entry.title, entities)] ) diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index a5f7cff127f..0f39e9de974 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -73,7 +73,7 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Group Cover platform.""" + """Set up the Fan Group platform.""" async_add_entities( [FanGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES])] ) @@ -84,13 +84,13 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Initialize Light Switch config entry.""" + """Initialize Fan Group config entry.""" registry = er.async_get(hass) - entity_id = er.async_validate_entity_ids( + entities = er.async_validate_entity_ids( registry, config_entry.options[CONF_ENTITIES] ) - async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entity_id)]) + async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entities)]) class FanGroup(GroupEntity, FanEntity): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index c51baf0ff66..12f316497e6 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -98,14 +98,14 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Initialize Light Switch config entry.""" + """Initialize Light Group config entry.""" registry = er.async_get(hass) - entity_id = er.async_validate_entity_ids( + entities = er.async_validate_entity_ids( registry, config_entry.options[CONF_ENTITIES] ) async_add_entities( - [LightGroup(config_entry.entry_id, config_entry.title, entity_id)] + [LightGroup(config_entry.entry_id, config_entry.title, entities)] ) diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index e1c044fd3e3..97d8f51536c 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -87,10 +87,10 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Media Group platform.""" + """Set up the MediaPlayer Group platform.""" async_add_entities( [ - MediaGroup( + MediaPlayerGroup( config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] ) ] @@ -102,18 +102,18 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Initialize Light Switch config entry.""" + """Initialize MediaPlayer Group config entry.""" registry = er.async_get(hass) - entity_id = er.async_validate_entity_ids( + entities = er.async_validate_entity_ids( registry, config_entry.options[CONF_ENTITIES] ) async_add_entities( - [MediaGroup(config_entry.entry_id, config_entry.title, entity_id)] + [MediaPlayerGroup(config_entry.entry_id, config_entry.title, entities)] ) -class MediaGroup(MediaPlayerEntity): +class MediaPlayerGroup(MediaPlayerEntity): """Representation of a Media Group.""" def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 8f0531ac7e4..e0b1eb1ab23 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -2,62 +2,77 @@ "title": "Group", "config": { "step": { - "init": { - "description": "Select group type", + "user": { + "title": "New Group", "data": { "group_type": "Group type" } }, - "cover": { - "description": "Select group options", + "binary_sensor": { + "title": "[%key:component::group::config::step::user::title%]", + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", "data": { - "entities": "Group members", - "name": "Group name" + "all": "All entities", + "entities": "Members", + "name": "Name" } }, - "cover_options": { - "description": "Select group options", + "cover": { "data": { - "entities": "Group members" + "title": "[%key:component::group::config::step::user::title%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, "fan": { - "description": "Select group options", "data": { - "entities": "Group members", - "name": "Group name" - } - }, - "fan_options": { - "description": "Select group options", - "data": { - "entities": "Group members" + "title": "[%key:component::group::config::step::user::title%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, "light": { - "description": "Select group options", "data": { - "entities": "Group members", - "name": "Group name" - } - }, - "light_options": { - "description": "Select group options", - "data": { - "entities": "Group members" + "title": "[%key:component::group::config::step::user::title%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, "media_player": { - "description": "Select group options", "data": { - "entities": "Group members", - "name": "Group name" + "title": "[%key:component::group::config::step::user::title%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + } + }, + "cover_options": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + } + }, + "fan_options": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + } + }, + "light_options": { + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" } }, "media_player_options": { - "description": "Select group options", "data": { - "entities": "Group members" + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" } } } diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 121ecc717e6..d720b051e53 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -10,16 +10,25 @@ from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ @pytest.mark.parametrize( - "group_type,group_state,member_state,member_attributes", + "group_type,group_state,member_state,member_attributes,extra_input,extra_options,extra_attrs", ( - ("cover", "open", "open", {}), - ("fan", "on", "on", {}), - ("light", "on", "on", {}), - ("media_player", "on", "on", {}), + ("binary_sensor", "on", "on", {}, {}, {"all": False}, {}), + ("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}), + ("cover", "open", "open", {}, {}, {}, {}), + ("fan", "on", "on", {}, {}, {}, {}), + ("light", "on", "on", {}, {}, {}, {}), + ("media_player", "on", "on", {}, {}, {}, {}), ), ) async def test_config_flow( - hass: HomeAssistant, group_type, group_state, member_state, member_attributes + hass: HomeAssistant, + group_type, + group_state, + member_state, + member_attributes, + extra_input, + extra_options, + extra_attrs, ) -> None: """Test the config flow.""" members = [f"{group_type}.one", f"{group_type}.two"] @@ -48,6 +57,7 @@ async def test_config_flow( { "name": "Living Room", "entities": members, + **extra_input, }, ) await hass.async_block_till_done() @@ -59,6 +69,7 @@ async def test_config_flow( "group_type": group_type, "entities": members, "name": "Living Room", + **extra_options, } assert len(mock_setup_entry.mock_calls) == 1 @@ -68,11 +79,14 @@ async def test_config_flow( "group_type": group_type, "name": "Living Room", "entities": members, + **extra_options, } state = hass.states.get(f"{group_type}.living_room") assert state.state == group_state assert state.attributes["entity_id"] == members + for key in extra_attrs: + assert state.attributes[key] == extra_attrs[key] def get_suggested(schema, key): @@ -87,10 +101,18 @@ def get_suggested(schema, key): @pytest.mark.parametrize( - "group_type,member_state", - (("cover", "open"), ("fan", "on"), ("light", "on"), ("media_player", "on")), + "group_type,member_state,extra_options", + ( + ("binary_sensor", "on", {"all": False}), + ("cover", "open", {}), + ("fan", "on", {}), + ("light", "on", {}), + ("media_player", "on", {}), + ), ) -async def test_options(hass: HomeAssistant, group_type, member_state) -> None: +async def test_options( + hass: HomeAssistant, group_type, member_state, extra_options +) -> None: """Test reconfiguring.""" members1 = [f"{group_type}.one", f"{group_type}.two"] members2 = [f"{group_type}.four", f"{group_type}.five"] @@ -138,6 +160,7 @@ async def test_options(hass: HomeAssistant, group_type, member_state) -> None: "group_type": group_type, "entities": members1, "name": "Bed Room", + **extra_options, } result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -157,12 +180,14 @@ async def test_options(hass: HomeAssistant, group_type, member_state) -> None: "group_type": group_type, "entities": members2, "name": "Bed Room", + **extra_options, } assert config_entry.data == {} assert config_entry.options == { "group_type": group_type, "entities": members2, "name": "Bed Room", + **extra_options, } assert config_entry.title == "Bed Room" From cb7e492e815715d13422c239b0ef23f3ed4c6fef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 12:23:01 +0100 Subject: [PATCH 0344/1054] Add switch_as_x integration (#67878) * Add switch_as_x integration * Address review comments * Add translation strings * Rename entity_type option to target_domain * Move LightSwitch class definition to switch_as_x/light.py * Update manifest * Move tests --- CODEOWNERS | 2 + homeassistant/components/switch/__init__.py | 20 -- homeassistant/components/switch/light.py | 110 +---------- homeassistant/components/switch/manifest.json | 4 +- homeassistant/components/switch/strings.json | 10 - .../components/switch_as_x/__init__.py | 43 +++++ .../{switch => switch_as_x}/config_flow.py | 11 +- homeassistant/components/switch_as_x/light.py | 102 ++++++++++ .../components/switch_as_x/manifest.json | 11 ++ .../components/switch_as_x/strings.json | 14 ++ homeassistant/generated/config_flows.py | 2 +- tests/components/switch/test_light.py | 152 ++------------- tests/components/switch_as_x/__init__.py | 1 + .../test_config_flow.py | 63 +++---- tests/components/switch_as_x/test_light.py | 174 ++++++++++++++++++ 15 files changed, 402 insertions(+), 317 deletions(-) create mode 100644 homeassistant/components/switch_as_x/__init__.py rename homeassistant/components/{switch => switch_as_x}/config_flow.py (80%) create mode 100644 homeassistant/components/switch_as_x/light.py create mode 100644 homeassistant/components/switch_as_x/manifest.json create mode 100644 homeassistant/components/switch_as_x/strings.json create mode 100644 tests/components/switch_as_x/__init__.py rename tests/components/{switch => switch_as_x}/test_config_flow.py (69%) create mode 100644 tests/components/switch_as_x/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 3ad23837c43..c2be1901344 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -980,6 +980,8 @@ homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switch/* @home-assistant/core tests/components/switch/* @home-assistant/core +homeassistant/components/switch_as_x/* @home-assistant/core +tests/components/switch_as_x/* @home-assistant/core homeassistant/components/switchbot/* @danielhiversen @RenierM26 tests/components/switchbot/* @danielhiversen @RenierM26 homeassistant/components/switcher_kis/* @tomerfi @thecode diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index b7e0ffac59c..2abb9ff81ca 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,15 +11,12 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, - Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -63,8 +60,6 @@ DEVICE_CLASSES = [cls.value for cls in SwitchDeviceClass] DEVICE_CLASS_OUTLET = SwitchDeviceClass.OUTLET.value DEVICE_CLASS_SWITCH = SwitchDeviceClass.SWITCH.value -PLATFORMS: list[Platform] = [Platform.LIGHT] - @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -91,21 +86,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - if entry.domain == DOMAIN: - registry = er.async_get(hass) - try: - er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) - except vol.Invalid: - # The entity is identified by an unknown entity registry ID - _LOGGER.error( - "Failed to setup light switch for unknown entity %s", - entry.options[CONF_ENTITY_ID], - ) - return False - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - return True - component: EntityComponent = hass.data[DOMAIN] return await component.async_setup_entry(entry) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 7c732d7750d..ad75d229d51 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,39 +1,25 @@ """Light support for switch entities.""" from __future__ import annotations -from typing import Any - import voluptuous as vol -from homeassistant.components import switch -from homeassistant.components.light import ( - COLOR_MODE_ONOFF, - PLATFORM_SCHEMA, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_ENTITY_ID, - CONF_NAME, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.components.light import PLATFORM_SCHEMA +from homeassistant.components.switch_as_x import LightSwitch +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN as SWITCH_DOMAIN + DEFAULT_NAME = "Light Switch" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(switch.DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN), } ) @@ -58,85 +44,3 @@ async def async_setup_platform( ) ] ) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Initialize Light Switch config entry.""" - - registry = er.async_get(hass) - entity_id = er.async_validate_entity_id( - registry, config_entry.options[CONF_ENTITY_ID] - ) - - async_add_entities( - [ - LightSwitch( - config_entry.title, - entity_id, - config_entry.entry_id, - ) - ] - ) - - -class LightSwitch(LightEntity): - """Represents a Switch as a Light.""" - - _attr_color_mode = COLOR_MODE_ONOFF - _attr_should_poll = False - _attr_supported_color_modes = {COLOR_MODE_ONOFF} - - def __init__(self, name: str, switch_entity_id: str, unique_id: str | None) -> None: - """Initialize Light Switch.""" - self._attr_name = name - self._attr_unique_id = unique_id - self._switch_entity_id = switch_entity_id - - async def async_turn_on(self, **kwargs: Any) -> None: - """Forward the turn_on command to the switch in this light switch.""" - await self.hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: self._switch_entity_id}, - blocking=True, - context=self._context, - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Forward the turn_off command to the switch in this light switch.""" - await self.hass.services.async_call( - switch.DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: self._switch_entity_id}, - blocking=True, - context=self._context, - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener(event: Event | None = None) -> None: - """Handle child updates.""" - if ( - state := self.hass.states.get(self._switch_entity_id) - ) is None or state.state == STATE_UNAVAILABLE: - self._attr_available = False - return - - self._attr_available = True - self._attr_is_on = state.state == STATE_ON - self.async_write_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, [self._switch_entity_id], async_state_changed_listener - ) - ) - - # Call once on adding - async_state_changed_listener() diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index f087ace1bce..ea11bc9591a 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -2,9 +2,9 @@ "domain": "switch", "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", + "after_dependencies": ["switch_as_x"], "codeowners": [ "@home-assistant/core" ], - "quality_scale": "internal", - "config_flow": true + "quality_scale": "internal" } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 5cdd0c35936..7ea84e649ef 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -1,15 +1,5 @@ { "title": "Switch", - "config": { - "step": { - "init": { - "description": "Select the switch for the light switch.", - "data": { - "entity_id": "Switch entity" - } - } - } - }, "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py new file mode 100644 index 00000000000..6cfea21a349 --- /dev/null +++ b/homeassistant/components/switch_as_x/__init__.py @@ -0,0 +1,43 @@ +"""Component to wrap switch entities in entities of other domains.""" +from __future__ import annotations + +import logging + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .light import LightSwitch + +__all__ = ["LightSwitch"] + +DOMAIN = "switch_as_x" + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + registry = er.async_get(hass) + try: + er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + except vol.Invalid: + # The entity is identified by an unknown entity registry ID + _LOGGER.error( + "Failed to setup switch_as_x for unknown entity %s", + entry.options[CONF_ENTITY_ID], + ) + return False + + hass.config_entries.async_setup_platforms(entry, (entry.options["target_domain"],)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options["target_domain"],) + ) diff --git a/homeassistant/components/switch/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py similarity index 80% rename from homeassistant/components/switch/config_flow.py rename to homeassistant/components/switch_as_x/config_flow.py index efb3baf363f..90fb2e4bc0c 100644 --- a/homeassistant/components/switch/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Switch integration.""" +"""Config flow for Switch as X integration.""" from __future__ import annotations from collections.abc import Mapping @@ -13,7 +13,7 @@ from homeassistant.helpers import ( selector, ) -from .const import DOMAIN +from . import DOMAIN CONFIG_FLOW = { "user": helper_config_entry_flow.HelperFlowStep( @@ -22,16 +22,19 @@ CONFIG_FLOW = { vol.Required("entity_id"): selector.selector( {"entity": {"domain": "switch"}} ), + vol.Required("target_domain"): selector.selector( + {"select": {"options": ["light"]}} + ), } ) ) } -class SwitchLightConfigFlowHandler( +class SwitchAsXConfigFlowHandler( helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN ): - """Handle a config or options flow for Switch Light.""" + """Handle a config flow for Switch as X.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py new file mode 100644 index 00000000000..8dd863029da --- /dev/null +++ b/homeassistant/components/switch_as_x/light.py @@ -0,0 +1,102 @@ +"""Light support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.light import COLOR_MODE_ONOFF, LightEntity +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + + async_add_entities( + [ + LightSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + ) + ] + ) + + +class LightSwitch(LightEntity): + """Represents a Switch as a Light.""" + + _attr_color_mode = COLOR_MODE_ONOFF + _attr_should_poll = False + _attr_supported_color_modes = {COLOR_MODE_ONOFF} + + def __init__(self, name: str, switch_entity_id: str, unique_id: str | None) -> None: + """Initialize Light Switch.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._switch_entity_id = switch_entity_id + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self._switch_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + + self._attr_available = True + self._attr_is_on = state.state == STATE_ON + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._switch_entity_id], async_state_changed_listener + ) + ) + + # Call once on adding + async_state_changed_listener() diff --git a/homeassistant/components/switch_as_x/manifest.json b/homeassistant/components/switch_as_x/manifest.json new file mode 100644 index 00000000000..358b50bfd1b --- /dev/null +++ b/homeassistant/components/switch_as_x/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "switch_as_x", + "name": "Switch as X", + "documentation": "https://www.home-assistant.io/integrations/switch_as_x", + "codeowners": [ + "@home-assistant/core" + ], + "quality_scale": "internal", + "iot_class": "calculated", + "config_flow": true +} diff --git a/homeassistant/components/switch_as_x/strings.json b/homeassistant/components/switch_as_x/strings.json new file mode 100644 index 00000000000..cc50d1922cf --- /dev/null +++ b/homeassistant/components/switch_as_x/strings.json @@ -0,0 +1,14 @@ +{ + "title": "Switch as X", + "config": { + "step": { + "init": { + "title": "Make a switch a ...", + "data": { + "entity_id": "Switch entity", + "target_domain": "Type" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7e94ae5a165..93592a2e1a9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -323,7 +323,7 @@ FLOWS = [ "stookalert", "subaru", "surepetcare", - "switch", + "switch_as_x", "switchbot", "switcher_kis", "syncthing", diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index 518fd8db20b..e75e0e6d313 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,22 +1,10 @@ """The tests for the Light Switch platform.""" -from homeassistant.components.light import ( - ATTR_COLOR_MODE, - ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_ONOFF, -) -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.components.light import common -from tests.components.switch import common as switch_common - async def test_default_state(hass): - """Test light switch default state.""" + """Test light switch yaml config.""" await async_setup_component( hass, "light", @@ -30,139 +18,21 @@ async def test_default_state(hass): ) await hass.async_block_till_done() - state = hass.states.get("light.christmas_tree_lights") - assert state is not None - assert state.state == "unavailable" - assert state.attributes["supported_features"] == 0 - assert state.attributes.get("brightness") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("effect_list") is None - assert state.attributes.get("effect") is None - assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_ONOFF] - assert state.attributes.get(ATTR_COLOR_MODE) is None + assert hass.states.get("light.christmas_tree_lights") -async def test_light_service_calls(hass): - """Test service calls to light.""" - await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) +async def test_default_state_no_name(hass): + """Test light switch default name.""" await async_setup_component( hass, "light", - {"light": [{"platform": "switch", "entity_id": "switch.decorative_lights"}]}, + { + "light": { + "platform": "switch", + "entity_id": "switch.test", + } + }, ) await hass.async_block_till_done() - assert hass.states.get("light.light_switch").state == "on" - - await common.async_toggle(hass, "light.light_switch") - - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.light_switch").state == "off" - - await common.async_turn_on(hass, "light.light_switch") - - assert hass.states.get("switch.decorative_lights").state == "on" - assert hass.states.get("light.light_switch").state == "on" - assert ( - hass.states.get("light.light_switch").attributes.get(ATTR_COLOR_MODE) - == COLOR_MODE_ONOFF - ) - - await common.async_turn_off(hass, "light.light_switch") - await hass.async_block_till_done() - - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.light_switch").state == "off" - - -async def test_switch_service_calls(hass): - """Test service calls to switch.""" - await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) - await async_setup_component( - hass, - "light", - {"light": [{"platform": "switch", "entity_id": "switch.decorative_lights"}]}, - ) - await hass.async_block_till_done() - - assert hass.states.get("light.light_switch").state == "on" - - await switch_common.async_turn_off(hass, "switch.decorative_lights") - await hass.async_block_till_done() - - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.light_switch").state == "off" - - await switch_common.async_turn_on(hass, "switch.decorative_lights") - await hass.async_block_till_done() - - assert hass.states.get("switch.decorative_lights").state == "on" - assert hass.states.get("light.light_switch").state == "on" - - -async def test_config_entry(hass: HomeAssistant): - """Test light switch setup from config entry.""" - config_entry = MockConfigEntry( - data={}, - domain=SWITCH_DOMAIN, - options={"entity_id": "switch.abc"}, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert SWITCH_DOMAIN in hass.config.components - - state = hass.states.get("light.abc") - assert state.state == "unavailable" - # Name copied from config entry title - assert state.name == "ABC" - - # Check the light is added to the entity registry - registry = er.async_get(hass) - entity_entry = registry.async_get("light.abc") - assert entity_entry.unique_id == config_entry.entry_id - - -async def test_config_entry_uuid(hass: HomeAssistant): - """Test light switch setup from config entry with entity registry id.""" - registry = er.async_get(hass) - registry_entry = registry.async_get_or_create("switch", "test", "unique") - - config_entry = MockConfigEntry( - data={}, - domain=SWITCH_DOMAIN, - options={"entity_id": registry_entry.id}, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert hass.states.get("light.abc") - - -async def test_config_entry_unregistered_uuid(hass: HomeAssistant): - """Test light switch setup from config entry with unknown entity registry id.""" - fake_uuid = "a266a680b608c32770e6c45bfe6b8411" - - config_entry = MockConfigEntry( - data={}, - domain=SWITCH_DOMAIN, - options={"entity_id": fake_uuid}, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 0 + assert hass.states.get("light.light_switch") diff --git a/tests/components/switch_as_x/__init__.py b/tests/components/switch_as_x/__init__.py new file mode 100644 index 00000000000..d7cf944e624 --- /dev/null +++ b/tests/components/switch_as_x/__init__.py @@ -0,0 +1 @@ +"""The tests for Switch as X platforms.""" diff --git a/tests/components/switch/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py similarity index 69% rename from tests/components/switch/test_config_flow.py rename to tests/components/switch_as_x/test_config_flow.py index ca838b7b972..223547dfb75 100644 --- a/tests/components/switch/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -1,17 +1,17 @@ -"""Test the switch light config flow.""" +"""Test the Switch as X config flow.""" from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.switch import async_setup_entry -from homeassistant.components.switch.const import DOMAIN +from homeassistant.components.switch_as_x import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from homeassistant.helpers import entity_registry as er -async def test_config_flow(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_flow(hass: HomeAssistant, target_domain) -> None: """Test the config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -20,13 +20,14 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert result["errors"] is None with patch( - "homeassistant.components.switch.async_setup_entry", + "homeassistant.components.switch_as_x.async_setup_entry", wraps=async_setup_entry, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], { "entity_id": "switch.ceiling", + "target_domain": target_domain, }, ) await hass.async_block_till_done() @@ -34,17 +35,24 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} - assert result["options"] == {"entity_id": "switch.ceiling"} + assert result["options"] == { + "entity_id": "switch.ceiling", + "target_domain": target_domain, + } assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry.data == {} - assert config_entry.options == {"entity_id": "switch.ceiling"} + assert config_entry.options == { + "entity_id": "switch.ceiling", + "target_domain": target_domain, + } - assert hass.states.get("light.ceiling") + assert hass.states.get(f"{target_domain}.ceiling") -async def test_name(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_name(hass: HomeAssistant, target_domain) -> None: """Test the config flow name is copied from registry entry, with fallback to state.""" registry = er.async_get(hass) @@ -55,7 +63,7 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "ceiling" @@ -68,7 +76,7 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "State Name" @@ -90,7 +98,7 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "Original Name" @@ -103,51 +111,34 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "Custom Name" -def get_suggested(schema, key): - """Get suggested value for key in voluptuous schema.""" - for k in schema.keys(): - if k == key: - if k.description is None or "suggested_value" not in k.description: - return None - return k.description["suggested_value"] - - -async def test_options(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_options(hass: HomeAssistant, target_domain) -> None: """Test reconfiguring.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - assert get_suggested(result["data_schema"].schema, "entity_id") is None - assert get_suggested(result["data_schema"].schema, "name") is None with patch( - "homeassistant.components.switch.async_setup_entry", + "homeassistant.components.switch_as_x.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "entity_id": "switch.ceiling", - }, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ceiling" - assert result["data"] == {} - assert result["options"] == {"entity_id": "switch.ceiling"} - assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == {} - assert config_entry.options == {"entity_id": "switch.ceiling"} + assert config_entry # Switch light has no options flow with pytest.raises(data_entry_flow.UnknownHandler): diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py new file mode 100644 index 00000000000..302311b3845 --- /dev/null +++ b/tests/components/switch_as_x/test_light.py @@ -0,0 +1,174 @@ +"""The tests for the Light Switch platform.""" +import pytest + +from homeassistant.components.light import ( + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_ONOFF, +) +from homeassistant.components.switch_as_x import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.light import common +from tests.components.switch import common as switch_common + + +async def test_default_state(hass): + """Test light switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.test", "target_domain": "light"}, + title="Christmas Tree Lights", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("light.christmas_tree_lights") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 0 + assert state.attributes.get("brightness") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("effect_list") is None + assert state.attributes.get("effect") is None + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_ONOFF] + assert state.attributes.get(ATTR_COLOR_MODE) is None + + +async def test_light_service_calls(hass): + """Test service calls to light.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.decorative_lights", "target_domain": "light"}, + title="decorative_lights", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("light.decorative_lights").state == "on" + + await common.async_toggle(hass, "light.decorative_lights") + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.decorative_lights").state == "off" + + await common.async_turn_on(hass, "light.decorative_lights") + + assert hass.states.get("switch.decorative_lights").state == "on" + assert hass.states.get("light.decorative_lights").state == "on" + assert ( + hass.states.get("light.decorative_lights").attributes.get(ATTR_COLOR_MODE) + == COLOR_MODE_ONOFF + ) + + await common.async_turn_off(hass, "light.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.decorative_lights").state == "off" + + +async def test_switch_service_calls(hass): + """Test service calls to switch.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.decorative_lights", "target_domain": "light"}, + title="decorative_lights", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("light.decorative_lights").state == "on" + + await switch_common.async_turn_off(hass, "switch.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.decorative_lights").state == "off" + + await switch_common.async_turn_on(hass, "switch.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "on" + assert hass.states.get("light.decorative_lights").state == "on" + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_entry(hass: HomeAssistant, target_domain): + """Test light switch setup from config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.abc", "target_domain": target_domain}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert DOMAIN in hass.config.components + + state = hass.states.get(f"{target_domain}.abc") + assert state.state == "unavailable" + # Name copied from config entry title + assert state.name == "ABC" + + # Check the light is added to the entity registry + registry = er.async_get(hass) + entity_entry = registry.async_get(f"{target_domain}.abc") + assert entity_entry.unique_id == config_entry.entry_id + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_entry_uuid(hass: HomeAssistant, target_domain): + """Test light switch setup from config entry with entity registry id.""" + registry = er.async_get(hass) + registry_entry = registry.async_get_or_create("switch", "test", "unique") + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": registry_entry.id, "target_domain": target_domain}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{target_domain}.abc") + + +async def test_config_entry_unregistered_uuid(hass: HomeAssistant): + """Test light switch setup from config entry with unknown entity registry id.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": fake_uuid}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 From 86b775e46a5dd705ac187cbe5428326d3e548854 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 10 Mar 2022 14:48:30 +0100 Subject: [PATCH 0345/1054] Use generic SamsungTVBridge for SamsungTV type hints (#67942) Co-authored-by: epenet --- homeassistant/components/samsungtv/__init__.py | 12 +++--------- homeassistant/components/samsungtv/bridge.py | 6 +++--- homeassistant/components/samsungtv/config_flow.py | 10 ++-------- homeassistant/components/samsungtv/diagnostics.py | 6 ++---- homeassistant/components/samsungtv/media_player.py | 4 ++-- 5 files changed, 12 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 9f605291dda..c619dd4be09 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -25,13 +25,7 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .bridge import ( - SamsungTVBridge, - SamsungTVLegacyBridge, - SamsungTVWSBridge, - async_get_device_info, - mac_from_device_info, -) +from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_MODEL, CONF_ON_ACTION, @@ -101,7 +95,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def _async_get_device_bridge( hass: HomeAssistant, data: dict[str, Any] -) -> SamsungTVLegacyBridge | SamsungTVWSBridge: +) -> SamsungTVBridge: """Get device bridge.""" return SamsungTVBridge.get_bridge( hass, @@ -144,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry -) -> SamsungTVLegacyBridge | SamsungTVWSBridge: +) -> SamsungTVBridge: """Create a bridge object and update any missing data in the config entry.""" updated_data: dict[str, str | int] = {} host: str = entry.data[CONF_HOST] diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 62c94e17f4d..52776242911 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -57,7 +57,7 @@ def mac_from_device_info(info: dict[str, Any]) -> str | None: async def async_get_device_info( hass: HomeAssistant, - bridge: SamsungTVWSBridge | SamsungTVLegacyBridge | None, + bridge: SamsungTVBridge | None, host: str, ) -> tuple[int | None, str | None, dict[str, Any] | None]: """Fetch the port, method, and device info.""" @@ -87,7 +87,7 @@ class SamsungTVBridge(ABC): host: str, port: int | None = None, token: str | None = None, - ) -> SamsungTVLegacyBridge | SamsungTVWSBridge: + ) -> SamsungTVBridge: """Get Bridge instance.""" if method == METHOD_LEGACY or port == LEGACY_PORT: return SamsungTVLegacyBridge(hass, method, host, port) @@ -114,7 +114,7 @@ class SamsungTVBridge(ABC): self._new_token_callback = func @abstractmethod - async def async_try_connect(self) -> str | None: + async def async_try_connect(self) -> str: """Try to connect to the TV.""" @abstractmethod diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 12ff47869d6..812916a4654 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -23,13 +23,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac -from .bridge import ( - SamsungTVBridge, - SamsungTVLegacyBridge, - SamsungTVWSBridge, - async_get_device_info, - mac_from_device_info, -) +from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_MANUFACTURER, CONF_MODEL, @@ -77,7 +71,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._name: str | None = None self._title: str = "" self._id: int | None = None - self._bridge: SamsungTVLegacyBridge | SamsungTVWSBridge | None = None + self._bridge: SamsungTVBridge | None = None self._device_info: dict[str, Any] | None = None def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index 007ab283cfd..ff792fff3e3 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant -from .bridge import SamsungTVLegacyBridge, SamsungTVWSBridge +from .bridge import SamsungTVBridge from .const import DOMAIN TO_REDACT = {CONF_TOKEN} @@ -18,9 +18,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - bridge: SamsungTVLegacyBridge | SamsungTVWSBridge = hass.data[DOMAIN][ - entry.entry_id - ] + bridge: SamsungTVBridge = hass.data[DOMAIN][entry.entry_id] return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": await bridge.async_device_info(), diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a154399725f..baea046c3dc 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -36,7 +36,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util -from .bridge import SamsungTVLegacyBridge, SamsungTVWSBridge +from .bridge import SamsungTVBridge, SamsungTVWSBridge from .const import ( CONF_MANUFACTURER, CONF_MODEL, @@ -92,7 +92,7 @@ class SamsungTVDevice(MediaPlayerEntity): def __init__( self, - bridge: SamsungTVLegacyBridge | SamsungTVWSBridge, + bridge: SamsungTVBridge, config_entry: ConfigEntry, on_script: Script | None, ) -> None: From 245f55edf452ba6b44127b0efaf4babd50ef60f8 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Thu, 10 Mar 2022 06:30:11 -0800 Subject: [PATCH 0346/1054] Removed unused const (#67932) --- homeassistant/components/kaleidescape/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/kaleidescape/config_flow.py b/homeassistant/components/kaleidescape/config_flow.py index a6127e89a77..5454f29f5cb 100644 --- a/homeassistant/components/kaleidescape/config_flow.py +++ b/homeassistant/components/kaleidescape/config_flow.py @@ -18,7 +18,6 @@ if TYPE_CHECKING: from homeassistant.data_entry_flow import FlowResult ERROR_CANNOT_CONNECT = "cannot_connect" -ERROR_UNKNOWN = "unknown" ERROR_UNSUPPORTED = "unsupported" From ab4aa835d13871671626d4594d458b01240280d5 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Thu, 10 Mar 2022 07:15:53 -0800 Subject: [PATCH 0347/1054] Add sensor platform to Kaleidescape (#67884) --- .../components/kaleidescape/__init__.py | 2 +- .../components/kaleidescape/manifest.json | 2 +- .../components/kaleidescape/sensor.py | 186 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/kaleidescape/test_sensor.py | 48 +++++ 6 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/kaleidescape/sensor.py create mode 100644 tests/components/kaleidescape/test_sensor.py diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py index 574e74a3e14..64205ecf838 100644 --- a/homeassistant/components/kaleidescape/__init__.py +++ b/homeassistant/components/kaleidescape/__init__.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kaleidescape/manifest.json b/homeassistant/components/kaleidescape/manifest.json index 88d5c7726f0..4fe29dd031c 100644 --- a/homeassistant/components/kaleidescape/manifest.json +++ b/homeassistant/components/kaleidescape/manifest.json @@ -9,7 +9,7 @@ } ], "documentation": "https://www.home-assistant.io/integrations/kaleidescape", - "requirements": ["pykaleidescape==2022.2.6"], + "requirements": ["pykaleidescape==1.0.1"], "codeowners": [ "@SteveEasley" ], diff --git a/homeassistant/components/kaleidescape/sensor.py b/homeassistant/components/kaleidescape/sensor.py new file mode 100644 index 00000000000..468b7ced0e8 --- /dev/null +++ b/homeassistant/components/kaleidescape/sensor.py @@ -0,0 +1,186 @@ +"""Sensor platform for Kaleidescape integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.const import PERCENTAGE +from homeassistant.helpers.entity import EntityCategory + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .entity import KaleidescapeEntity + +if TYPE_CHECKING: + from collections.abc import Callable + + from kaleidescape import Device as KaleidescapeDevice + + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import StateType + + +@dataclass +class BaseEntityDescriptionMixin: + """Mixin for required descriptor keys.""" + + value_fn: Callable[[KaleidescapeDevice], StateType] + + +@dataclass +class KaleidescapeSensorEntityDescription( + SensorEntityDescription, BaseEntityDescriptionMixin +): + """Describes Kaleidescape sensor entity.""" + + +SENSOR_TYPES: tuple[KaleidescapeSensorEntityDescription, ...] = ( + KaleidescapeSensorEntityDescription( + key="media_location", + name="Media Location", + icon="mdi:monitor", + value_fn=lambda device: device.automation.movie_location, + ), + KaleidescapeSensorEntityDescription( + key="play_status", + name="Play Status", + icon="mdi:monitor", + value_fn=lambda device: device.movie.play_status, + ), + KaleidescapeSensorEntityDescription( + key="play_speed", + name="Play Speed", + icon="mdi:monitor", + value_fn=lambda device: device.movie.play_speed, + ), + KaleidescapeSensorEntityDescription( + key="video_mode", + name="Video Mode", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_mode, + ), + KaleidescapeSensorEntityDescription( + key="video_color_eotf", + name="Video Color EOTF", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_eotf, + ), + KaleidescapeSensorEntityDescription( + key="video_color_space", + name="Video Color Space", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_space, + ), + KaleidescapeSensorEntityDescription( + key="video_color_depth", + name="Video Color Depth", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_depth, + ), + KaleidescapeSensorEntityDescription( + key="video_color_sampling", + name="Video Color Sampling", + icon="mdi:monitor-eye", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.video_color_sampling, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_ratio", + name="Screen Mask Ratio", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.screen_mask_ratio, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_top_trim_rel", + name="Screen Mask Top Trim Rel", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_top_trim_rel / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_bottom_trim_rel", + name="Screen Mask Bottom Trim Rel", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_bottom_trim_rel / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_conservative_ratio", + name="Screen Mask Conservative Ratio", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.screen_mask_conservative_ratio, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_top_mask_abs", + name="Screen Mask Top Mask Abs", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_top_mask_abs / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="screen_mask_bottom_mask_abs", + name="Screen Mask Bottom Mask Abs", + icon="mdi:monitor-screenshot", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda device: device.automation.screen_mask_bottom_mask_abs / 10.0, + ), + KaleidescapeSensorEntityDescription( + key="cinemascape_mask", + name="Cinemascape Mask", + icon="mdi:monitor-star", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.cinemascape_mask, + ), + KaleidescapeSensorEntityDescription( + key="cinemascape_mode", + name="Cinemascape Mode", + icon="mdi:monitor-star", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.automation.cinemascape_mode, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the platform from a config entry.""" + device: KaleidescapeDevice = hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id] + async_add_entities( + KaleidescapeSensor(device, description) for description in SENSOR_TYPES + ) + + +class KaleidescapeSensor(KaleidescapeEntity, SensorEntity): + """Representation of a Kaleidescape sensor.""" + + entity_description: KaleidescapeSensorEntityDescription + + def __init__( + self, + device: KaleidescapeDevice, + entity_description: KaleidescapeSensorEntityDescription, + ) -> None: + """Initialize sensor.""" + super().__init__(device) + self.entity_description = entity_description + self._attr_unique_id = f"{self._attr_unique_id}-{entity_description.key}" + self._attr_name = f"{self._attr_name} {entity_description.name}" + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.entity_description.value_fn(self._device) diff --git a/requirements_all.txt b/requirements_all.txt index 72b130c1c68..be836f024f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1564,7 +1564,7 @@ pyisy==3.0.1 pyitachip2ir==0.0.7 # homeassistant.components.kaleidescape -pykaleidescape==2022.2.6 +pykaleidescape==1.0.1 # homeassistant.components.kira pykira==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff0e0c48d9b..dd1152e6919 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ pyiss==1.0.1 pyisy==3.0.1 # homeassistant.components.kaleidescape -pykaleidescape==2022.2.6 +pykaleidescape==1.0.1 # homeassistant.components.kira pykira==0.1.1 diff --git a/tests/components/kaleidescape/test_sensor.py b/tests/components/kaleidescape/test_sensor.py new file mode 100644 index 00000000000..0ae2dc15619 --- /dev/null +++ b/tests/components/kaleidescape/test_sensor.py @@ -0,0 +1,48 @@ +"""Tests for Kaleidescape sensor platform.""" + +from unittest.mock import MagicMock + +from kaleidescape import const as kaleidescape_const + +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + +ENTITY_ID = f"sensor.kaleidescape_device_{MOCK_SERIAL}" +FRIENDLY_NAME = f"Kaleidescape Device {MOCK_SERIAL}" + + +async def test_sensors( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test sensors.""" + entity = hass.states.get(f"{ENTITY_ID}_media_location") + entry = er.async_get(hass).async_get(f"{ENTITY_ID}_media_location") + assert entity + assert entity.state == "none" + assert ( + entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Media Location" + ) + assert entry + assert entry.unique_id == f"{MOCK_SERIAL}-media_location" + + entity = hass.states.get(f"{ENTITY_ID}_play_status") + entry = er.async_get(hass).async_get(f"{ENTITY_ID}_play_status") + assert entity + assert entity.state == "none" + assert entity.attributes.get(ATTR_FRIENDLY_NAME) == f"{FRIENDLY_NAME} Play Status" + assert entry + assert entry.unique_id == f"{MOCK_SERIAL}-play_status" + + mock_device.movie.play_status = kaleidescape_const.PLAY_STATUS_PLAYING + mock_device.dispatcher.send(kaleidescape_const.PLAY_STATUS) + await hass.async_block_till_done() + entity = hass.states.get(f"{ENTITY_ID}_play_status") + assert entity is not None + assert entity.state == "playing" From 4e7d4db7ae672a17178a621f62d86bad1f659256 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 16:17:59 +0100 Subject: [PATCH 0348/1054] Align selectors with frontend updates (#67906) * Align selectors with frontend updates * Drop metadata from MediaSelector selection * Adjust blueprint tests * Address review comments * Add tests for new selectors * Don't stringify input * Require min+max for number selector in slider mode * vol.Schema does not like static methods * Tweak --- homeassistant/helpers/selector.py | 225 +++++++++++++++++--- tests/components/blueprint/test_importer.py | 2 +- tests/helpers/test_selector.py | 168 ++++++++++++++- 3 files changed, 366 insertions(+), 29 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 861b2143cb9..3359deb9b09 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Callable -from datetime import time as time_sys +from datetime import time as time_sys, timedelta from typing import Any, cast import voluptuous as vol @@ -142,10 +142,11 @@ class DeviceSelector(Selector): def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" if not self.config["multiple"]: - return cv.string(data) + device_id: str = vol.Schema(str)(data) + return device_id if not isinstance(data, list): raise vol.Invalid("Value should be a list") - return [cv.string(val) for val in data] + return [vol.Schema(str)(val) for val in data] @SELECTORS.register("area") @@ -165,10 +166,22 @@ class AreaSelector(Selector): def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" if not self.config["multiple"]: - return cv.string(data) + area_id: str = vol.Schema(str)(data) + return area_id if not isinstance(data, list): raise vol.Invalid("Value should be a list") - return [cv.string(val) for val in data] + return [vol.Schema(str)(val) for val in data] + + +def has_min_max_if_slider(data: Any) -> Any: + """Validate configuration.""" + if data["mode"] == "box": + return data + + if "min" not in data or "max" not in data: + raise vol.Invalid("min and max are required in slider mode") + + return data @SELECTORS.register("number") @@ -177,24 +190,32 @@ class NumberSelector(Selector): selector_type = "number" - CONFIG_SCHEMA = vol.Schema( - { - vol.Required("min"): vol.Coerce(float), - vol.Required("max"): vol.Coerce(float), - vol.Optional("step", default=1): vol.All( - vol.Coerce(float), vol.Range(min=1e-3) - ), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, - vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]), - } + CONFIG_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional("min"): vol.Coerce(float), + vol.Optional("max"): vol.Coerce(float), + # Controls slider steps, and up/down keyboard binding for the box + # user input is not rounded + vol.Optional("step", default=1): vol.All( + vol.Coerce(float), vol.Range(min=1e-3) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, + vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]), + } + ), + has_min_max_if_slider, ) def __call__(self, data: Any) -> float: """Validate the passed selection.""" value: float = vol.Coerce(float)(data) - if not self.config["min"] <= value <= self.config["max"]: - raise vol.Invalid(f"Value {value} is too small or too large") + if "min" in self.config and value < self.config["min"]: + raise vol.Invalid(f"Value {value} is too small") + + if "max" in self.config and value > self.config["max"]: + raise vol.Invalid(f"Value {value} is too large") return value @@ -205,11 +226,17 @@ class AddonSelector(Selector): selector_type = "addon" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("name"): str, + vol.Optional("slug"): str, + } + ) def __call__(self, data: Any) -> str: """Validate the passed selection.""" - return cv.string(data) + addon: str = vol.Schema(str)(data) + return addon @SELECTORS.register("boolean") @@ -250,7 +277,7 @@ class TargetSelector(Selector): CONFIG_SCHEMA = vol.Schema( { - vol.Optional("entity"): EntitySelector.CONFIG_SCHEMA, + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, } ) @@ -295,14 +322,48 @@ class StringSelector(Selector): selector_type = "text" - CONFIG_SCHEMA = vol.Schema({vol.Optional("multiline", default=False): bool}) + STRING_TYPES = [ + "number", + "text", + "search", + "tel", + "url", + "email", + "password", + "date", + "month", + "week", + "time", + "datetime-local", + "color", + ] + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("multiline", default=False): bool, + vol.Optional("suffix"): str, + # The "type" controls the input field in the browser, the resulting + # data can be any string so we don't validate it. + vol.Optional("type"): vol.In(STRING_TYPES), + } + ) def __call__(self, data: Any) -> str: """Validate the passed selection.""" - text = cv.string(data) + text: str = vol.Schema(str)(data) return text +select_option = vol.All( + dict, + vol.Schema( + { + vol.Required("value"): str, + vol.Required("label"): str, + } + ), +) + + @SELECTORS.register("select") class SelectSelector(Selector): """Selector for an single-choice input select.""" @@ -310,10 +371,124 @@ class SelectSelector(Selector): selector_type = "select" CONFIG_SCHEMA = vol.Schema( - {vol.Required("options"): vol.All([str], vol.Length(min=1))} + { + vol.Required("options"): vol.All( + vol.Any([str], [select_option]), vol.Length(min=1) + ) + } ) def __call__(self, data: Any) -> Any: """Validate the passed selection.""" - selected_option = vol.In(self.config["options"])(cv.string(data)) - return selected_option + if isinstance(self.config["options"][0], str): + options = self.config["options"] + else: + options = [option["value"] for option in self.config["options"]] + return vol.In(options)(vol.Schema(str)(data)) + + +@SELECTORS.register("attribute") +class AttributeSelector(Selector): + """Selector for an entity attribute.""" + + selector_type = "attribute" + + CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + attribute: str = vol.Schema(str)(data) + return attribute + + +@SELECTORS.register("duration") +class DurationSelector(Selector): + """Selector for a duration.""" + + selector_type = "duration" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> timedelta: + """Validate the passed selection.""" + duration: timedelta = cv.time_period_dict(data) + return duration + + +@SELECTORS.register("icon") +class IconSelector(Selector): + """Selector for an icon.""" + + selector_type = "icon" + + CONFIG_SCHEMA = vol.Schema( + {vol.Optional("placeholder"): str} + # Frontend also has a fallbackPath option, this is not used by core + ) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + icon: str = vol.Schema(str)(data) + return icon + + +@SELECTORS.register("theme") +class ThemeSelector(Selector): + """Selector for an theme.""" + + selector_type = "theme" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + theme: str = vol.Schema(str)(data) + return theme + + +@SELECTORS.register("media") +class MediaSelector(Selector): + """Selector for media.""" + + selector_type = "media" + + CONFIG_SCHEMA = vol.Schema({}) + DATA_SCHEMA = vol.Schema( + { + # Although marked as optional in frontend, this field is required + vol.Required("entity_id"): cv.entity_id_or_uuid, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_id"): str, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_type"): str, + vol.Remove("metadata"): dict, + } + ) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + media: dict[str, float] = self.DATA_SCHEMA(data) + return media + + +@SELECTORS.register("location") +class LocationSelector(Selector): + """Selector for a location.""" + + selector_type = "location" + + CONFIG_SCHEMA = vol.Schema( + {vol.Optional("radius"): bool, vol.Optional("icon"): str} + ) + DATA_SCHEMA = vol.Schema( + { + vol.Required("latitude"): float, + vol.Required("longitude"): float, + vol.Optional("radius"): float, + } + ) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + location: dict[str, float] = self.DATA_SCHEMA(data) + return location diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 623d1e9ebbf..0e1e66405e6 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -32,7 +32,7 @@ COMMUNITY_POST_INPUTS = { "light": { "name": "Light(s)", "description": "The light(s) to control", - "selector": {"target": {"entity": {"domain": "light", "multiple": False}}}, + "selector": {"target": {"entity": {"domain": "light"}}}, }, "force_brightness": { "name": "Force turn on brightness", diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 0af8a050ce8..7031f16d249 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1,4 +1,6 @@ """Test selectors.""" +from datetime import timedelta + import pytest import voluptuous as vol @@ -206,6 +208,7 @@ def test_area_selector_schema(schema, valid_selections, invalid_selections): (), ), ({"min": 10, "max": 1000, "mode": "slider", "step": 0.5}, (), ()), + ({"mode": "box"}, (10,), ()), ), ) def test_number_selector_schema(schema, valid_selections, invalid_selections): @@ -213,6 +216,19 @@ def test_number_selector_schema(schema, valid_selections, invalid_selections): _test_selector("number", schema, valid_selections, invalid_selections) +@pytest.mark.parametrize( + "schema", + ( + {}, # Must have mandatory fields + {"mode": "slider"}, # Must have min+max in slider mode + ), +) +def test_number_selector_schema_error(schema): + """Test select selector.""" + with pytest.raises(vol.Invalid): + selector.validate_selector({"number": schema}) + + @pytest.mark.parametrize( "schema,valid_selections,invalid_selections", (({}, ("abc123",), (None,)),), @@ -315,6 +331,16 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): ("red", "green", "blue"), ("cat", 0, None), ), + ( + { + "options": [ + {"value": "red", "label": "Ruby Red"}, + {"value": "green", "label": "Emerald Green"}, + ] + }, + ("red", "green"), + ("cat", 0, None), + ), ), ) def test_select_selector_schema(schema, valid_selections, invalid_selections): @@ -325,12 +351,148 @@ def test_select_selector_schema(schema, valid_selections, invalid_selections): @pytest.mark.parametrize( "schema", ( - {}, - {"options": {"hello": "World"}}, - {"options": []}, + {}, # Must have options + {"options": {"hello": "World"}}, # Options must be a list + {"options": []}, # Must have at least option + # Options must be strings or value / label pairs + {"options": [{"hello": "World"}]}, + # Options must all be of the same type + {"options": ["red", {"value": "green", "label": "Emerald Green"}]}, ), ) def test_select_selector_schema_error(schema): """Test select selector.""" with pytest.raises(vol.Invalid): selector.validate_selector({"select": schema}) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {"entity_id": "sensor.abc"}, + ("friendly_name", "device_class"), + (None,), + ), + ), +) +def test_attribute_selector_schema(schema, valid_selections, invalid_selections): + """Test attribute selector.""" + _test_selector("attribute", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ({"seconds": 10},), + (None, {}), + ), + ), +) +def test_duration_selector_schema(schema, valid_selections, invalid_selections): + """Test duration selector.""" + _test_selector( + "duration", + schema, + valid_selections, + invalid_selections, + lambda x: timedelta(**x), + ) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ("mdi:abc",), + (None,), + ), + ), +) +def test_icon_selector_schema(schema, valid_selections, invalid_selections): + """Test icon selector.""" + _test_selector("icon", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ("abc",), + (None,), + ), + ), +) +def test_theme_selector_schema(schema, valid_selections, invalid_selections): + """Test theme selector.""" + _test_selector("theme", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ( + { + "entity_id": "sensor.abc", + "media_content_id": "abc", + "media_content_type": "def", + }, + { + "entity_id": "sensor.abc", + "media_content_id": "abc", + "media_content_type": "def", + "metadata": {}, + }, + ), + (None, "abc", {}), + ), + ), +) +def test_media_selector_schema(schema, valid_selections, invalid_selections): + """Test media selector.""" + + def drop_metadata(data): + """Drop metadata key from the input.""" + data.pop("metadata", None) + return data + + _test_selector("media", schema, valid_selections, invalid_selections, drop_metadata) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ( + { + "latitude": 1.0, + "longitude": 2.0, + }, + { + "latitude": 1.0, + "longitude": 2.0, + "radius": 3.0, + }, + ), + ( + None, + "abc", + {}, + {"latitude": 1.0}, + {"longitude": 1.0}, + {"latitude": 1.0, "longitude": "1.0"}, + ), + ), + ), +) +def test_location_selector_schema(schema, valid_selections, invalid_selections): + """Test location selector.""" + + _test_selector("location", schema, valid_selections, invalid_selections) From ee38dbd6986590c3ea36cd5d7c4e6dcdbb18ed31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 16:18:26 +0100 Subject: [PATCH 0349/1054] Add helper to set name of helper config entries (#67950) --- .../components/switch_as_x/config_flow.py | 19 ++--- .../helpers/helper_config_entry_flow.py | 26 ++++++- .../switch_as_x/test_config_flow.py | 71 +++---------------- tests/components/switch_as_x/test_light.py | 28 -------- .../helpers/test_helper_config_entry_flow.py | 38 ++++++++++ 5 files changed, 75 insertions(+), 107 deletions(-) create mode 100644 tests/helpers/test_helper_config_entry_flow.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 90fb2e4bc0c..d59aecbc060 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -6,12 +6,7 @@ from typing import Any import voluptuous as vol -from homeassistant.core import split_entity_id -from homeassistant.helpers import ( - entity_registry as er, - helper_config_entry_flow, - selector, -) +from homeassistant.helpers import helper_config_entry_flow, selector from . import DOMAIN @@ -40,12 +35,6 @@ class SwitchAsXConfigFlowHandler( def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" - registry = er.async_get(self.hass) - object_id = split_entity_id(options["entity_id"])[1] - entry = registry.async_get(options["entity_id"]) - if entry: - return entry.name or entry.original_name or object_id - state = self.hass.states.get(options["entity_id"]) - if state: - return state.name or object_id - return object_id + return helper_config_entry_flow.wrapped_entity_config_entry_title( + self.hass, options["entity_id"] + ) diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 7ef69b7f360..2e9c55baedd 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -10,9 +10,11 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.data_entry_flow import FlowResult, UnknownHandler +from . import entity_registry as er + @dataclass class HelperFlowStep: @@ -181,3 +183,25 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Finish config flow and create a config entry.""" return super().async_create_entry(title="", **kwargs) + + +@callback +def wrapped_entity_config_entry_title( + hass: HomeAssistant, entity_id_or_uuid: str +) -> str: + """Generate title for a config entry wrapping a single entity. + + If the entity is registered, use the registry entry's name. + If the entity is in the state machine, use the name from the state. + Otherwise, fall back to the object ID. + """ + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id(registry, entity_id_or_uuid) + object_id = split_entity_id(entity_id)[1] + entry = registry.async_get(entity_id) + if entry: + return entry.name or entry.original_name or object_id + state = hass.states.get(entity_id) + if state: + return state.name or object_id + return object_id diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 223547dfb75..98770610f48 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -48,72 +48,17 @@ async def test_config_flow(hass: HomeAssistant, target_domain) -> None: "target_domain": target_domain, } - assert hass.states.get(f"{target_domain}.ceiling") + # Check the wrapped switch has a state and is added to the registry + state = hass.states.get(f"{target_domain}.ceiling") + assert state.state == "unavailable" + # Name copied from config entry title + assert state.name == "ceiling" -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_name(hass: HomeAssistant, target_domain) -> None: - """Test the config flow name is copied from registry entry, with fallback to state.""" + # Check the light is added to the entity registry registry = er.async_get(hass) - - # No entry or state, use Object ID - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"entity_id": "switch.ceiling", "target_domain": target_domain}, - ) - assert result["title"] == "ceiling" - - # State set, use name from state - hass.states.async_set("switch.ceiling", "on", {"friendly_name": "State Name"}) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"entity_id": "switch.ceiling", "target_domain": target_domain}, - ) - assert result["title"] == "State Name" - - # Entity registered, use original name from registry entry - hass.states.async_remove("switch.ceiling") - entry = registry.async_get_or_create( - "switch", - "test", - "unique", - suggested_object_id="ceiling", - original_name="Original Name", - ) - assert entry.entity_id == "switch.ceiling" - hass.states.async_set("switch.ceiling", "on", {"friendly_name": "State Name"}) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"entity_id": "switch.ceiling", "target_domain": target_domain}, - ) - assert result["title"] == "Original Name" - - # Entity has customized name - registry.async_update_entity("switch.ceiling", name="Custom Name") - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"entity_id": "switch.ceiling", "target_domain": target_domain}, - ) - assert result["title"] == "Custom Name" + entity_entry = registry.async_get(f"{target_domain}.ceiling") + assert entity_entry.unique_id == config_entry.entry_id @pytest.mark.parametrize("target_domain", ("light",)) diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 302311b3845..26b6199cbc6 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -106,34 +106,6 @@ async def test_switch_service_calls(hass): assert hass.states.get("light.decorative_lights").state == "on" -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_config_entry(hass: HomeAssistant, target_domain): - """Test light switch setup from config entry.""" - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={"entity_id": "switch.abc", "target_domain": target_domain}, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert DOMAIN in hass.config.components - - state = hass.states.get(f"{target_domain}.abc") - assert state.state == "unavailable" - # Name copied from config entry title - assert state.name == "ABC" - - # Check the light is added to the entity registry - registry = er.async_get(hass) - entity_entry = registry.async_get(f"{target_domain}.abc") - assert entity_entry.unique_id == config_entry.entry_id - - @pytest.mark.parametrize("target_domain", ("light",)) async def test_config_entry_uuid(hass: HomeAssistant, target_domain): """Test light switch setup from config entry with entity registry id.""" diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py new file mode 100644 index 00000000000..a92a0cd3b36 --- /dev/null +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -0,0 +1,38 @@ +"""Test helper_config_entry_flow.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.helper_config_entry_flow import ( + wrapped_entity_config_entry_title, +) + + +async def test_name(hass: HomeAssistant) -> None: + """Test the config flow name is copied from registry entry, with fallback to state.""" + registry = er.async_get(hass) + entity_id = "switch.ceiling" + + # No entry or state, use Object ID + assert wrapped_entity_config_entry_title(hass, entity_id) == "ceiling" + + # State set, use name from state + hass.states.async_set(entity_id, "on", {"friendly_name": "State Name"}) + assert wrapped_entity_config_entry_title(hass, entity_id) == "State Name" + + # Entity registered, use original name from registry entry + hass.states.async_remove(entity_id) + entry = registry.async_get_or_create( + "switch", + "test", + "unique", + suggested_object_id="ceiling", + original_name="Original Name", + ) + hass.states.async_set(entity_id, "on", {"friendly_name": "State Name"}) + assert entry.entity_id == entity_id + assert wrapped_entity_config_entry_title(hass, entity_id) == "Original Name" + assert wrapped_entity_config_entry_title(hass, entry.id) == "Original Name" + + # Entity has customized name + registry.async_update_entity("switch.ceiling", name="Custom Name") + assert wrapped_entity_config_entry_title(hass, entity_id) == "Custom Name" + assert wrapped_entity_config_entry_title(hass, entry.id) == "Custom Name" From 14b5847da8b08acd70388f05e128f08c8110c1b4 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 10 Mar 2022 10:50:36 -0500 Subject: [PATCH 0350/1054] Rollback pyinsteon (#67956) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 7abff39113b..b069f3b18a8 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,7 +3,7 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ - "pyinsteon==1.0.16" + "pyinsteon==1.0.13" ], "codeowners": [ "@teharris1" diff --git a/requirements_all.txt b/requirements_all.txt index be836f024f4..95115a03bbe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1537,7 +1537,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.0.16 +pyinsteon==1.0.13 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd1152e6919..ff49f97cb9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -999,7 +999,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.0.16 +pyinsteon==1.0.13 # homeassistant.components.ipma pyipma==2.0.5 From 3d212b868ef746181d85a93ee430f4fc23f309d6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 17:23:16 +0100 Subject: [PATCH 0351/1054] Address review comments on switch_as_x tests (#67951) * Address review comments on switch_as_x tests * Correct test * Move test instead of duplicating it --- tests/components/switch_as_x/test_init.py | 27 ++++++++++++ tests/components/switch_as_x/test_light.py | 49 +++++++++++++--------- 2 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 tests/components/switch_as_x/test_init.py diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py new file mode 100644 index 00000000000..5797c66fab5 --- /dev/null +++ b/tests/components/switch_as_x/test_init.py @@ -0,0 +1,27 @@ +"""Tests for the Switch as X.""" +import pytest + +from homeassistant.components.switch_as_x import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_entry_unregistered_uuid(hass: HomeAssistant, target_domain): + """Test light switch setup from config entry with unknown entity registry id.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": fake_uuid, "target_domain": target_domain}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 26b6199cbc6..9a480776510 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -1,4 +1,4 @@ -"""The tests for the Light Switch platform.""" +"""Tests for the Switch as X Light platform.""" import pytest from homeassistant.components.light import ( @@ -106,6 +106,34 @@ async def test_switch_service_calls(hass): assert hass.states.get("light.decorative_lights").state == "on" +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_entry_entity_id(hass: HomeAssistant, target_domain): + """Test light switch setup from config entry with entity id.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.abc", "target_domain": target_domain}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert DOMAIN in hass.config.components + + state = hass.states.get(f"{target_domain}.abc") + assert state.state == "unavailable" + # Name copied from config entry title + assert state.name == "ABC" + + # Check the light is added to the entity registry + registry = er.async_get(hass) + entity_entry = registry.async_get(f"{target_domain}.abc") + assert entity_entry.unique_id == config_entry.entry_id + + @pytest.mark.parametrize("target_domain", ("light",)) async def test_config_entry_uuid(hass: HomeAssistant, target_domain): """Test light switch setup from config entry with entity registry id.""" @@ -125,22 +153,3 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain): await hass.async_block_till_done() assert hass.states.get(f"{target_domain}.abc") - - -async def test_config_entry_unregistered_uuid(hass: HomeAssistant): - """Test light switch setup from config entry with unknown entity registry id.""" - fake_uuid = "a266a680b608c32770e6c45bfe6b8411" - - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={"entity_id": fake_uuid}, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 0 From 65fbcfa0ba225bd8af292b337484bebece758e88 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 19:28:00 +0100 Subject: [PATCH 0352/1054] Prevent recursive script calls from deadlocking (#67861) * Prevent recursive script calls from deadlocking * Address review comments, improve tests * Tweak comment --- homeassistant/helpers/script.py | 23 +++++ tests/components/script/test_init.py | 125 +++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1eabc33b89d..07a89c8cddb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Sequence from contextlib import asynccontextmanager, suppress +from contextvars import ContextVar from datetime import datetime, timedelta from functools import partial import itertools @@ -126,6 +127,8 @@ SCRIPT_BREAKPOINT_HIT = "script_breakpoint_hit" SCRIPT_DEBUG_CONTINUE_STOP = "script_debug_continue_stop_{}_{}" SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all" +script_stack_cv: ContextVar[list[int] | None] = ContextVar("script_stack", default=None) + def action_trace_append(variables, path): """Append a TraceElement to trace[path].""" @@ -340,6 +343,12 @@ class _ScriptRun: async def async_run(self) -> None: """Run script.""" + # Push the script to the script execution stack + if (script_stack := script_stack_cv.get()) is None: + script_stack = [] + script_stack_cv.set(script_stack) + script_stack.append(id(self._script)) + try: self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): @@ -355,6 +364,8 @@ class _ScriptRun: script_execution_set("error") raise finally: + # Pop the script from the script execution stack + script_stack.pop() self._finish() async def _async_step(self, log_exceptions): @@ -1218,6 +1229,18 @@ class Script: else: variables = cast(dict, run_variables) + # Prevent non-allowed recursive calls which will cause deadlocks when we try to + # stop (restart) or wait for (queued) our own script run. + script_stack = script_stack_cv.get() + if ( + self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED) + and (script_stack := script_stack_cv.get()) is not None + and id(self) in script_stack + ): + script_execution_set("disallowed_recursion_detected") + _LOGGER.warning("Disallowed recursion detected") + return + if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun else: diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index a6923c88aa2..35875c6da12 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -27,6 +27,13 @@ from homeassistant.core import ( from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import template from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.script import ( + SCRIPT_MODE_CHOICES, + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, +) from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -790,3 +797,121 @@ async def test_script_restore_last_triggered(hass: HomeAssistant) -> None: state = hass.states.get("script.last_triggered") assert state assert state.attributes["last_triggered"] == time + + +@pytest.mark.parametrize( + "script_mode,warning_msg", + ( + (SCRIPT_MODE_PARALLEL, "Maximum number of runs exceeded"), + (SCRIPT_MODE_QUEUED, "Disallowed recursion detected"), + (SCRIPT_MODE_RESTART, "Disallowed recursion detected"), + (SCRIPT_MODE_SINGLE, "Already running"), + ), +) +async def test_recursive_script(hass, script_mode, warning_msg, caplog): + """Test recursive script calls does not deadlock.""" + # Make sure we cover all script modes + assert SCRIPT_MODE_CHOICES == [ + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + ] + + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "mode": script_mode, + "sequence": [ + {"service": "script.script1"}, + {"service": "test.script"}, + ], + }, + } + }, + ) + + service_called = asyncio.Event() + + async def async_service_handler(service): + service_called.set() + + hass.services.async_register("test", "script", async_service_handler) + hass.states.async_set("input_boolean.test", "on") + hass.states.async_set("input_boolean.test2", "off") + + await hass.services.async_call("script", "script1") + await asyncio.wait_for(service_called.wait(), 1) + + assert warning_msg in caplog.text + + +@pytest.mark.parametrize( + "script_mode,warning_msg", + ( + (SCRIPT_MODE_PARALLEL, "Maximum number of runs exceeded"), + (SCRIPT_MODE_QUEUED, "Disallowed recursion detected"), + (SCRIPT_MODE_RESTART, "Disallowed recursion detected"), + (SCRIPT_MODE_SINGLE, "Already running"), + ), +) +async def test_recursive_script_indirect(hass, script_mode, warning_msg, caplog): + """Test recursive script calls does not deadlock.""" + # Make sure we cover all script modes + assert SCRIPT_MODE_CHOICES == [ + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + ] + + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "mode": script_mode, + "sequence": [ + {"service": "script.script2"}, + ], + }, + "script2": { + "mode": script_mode, + "sequence": [ + {"service": "script.script3"}, + ], + }, + "script3": { + "mode": script_mode, + "sequence": [ + {"service": "script.script4"}, + ], + }, + "script4": { + "mode": script_mode, + "sequence": [ + {"service": "script.script1"}, + {"service": "test.script"}, + ], + }, + } + }, + ) + + service_called = asyncio.Event() + + async def async_service_handler(service): + service_called.set() + + hass.services.async_register("test", "script", async_service_handler) + hass.states.async_set("input_boolean.test", "on") + hass.states.async_set("input_boolean.test2", "off") + + await hass.services.async_call("script", "script1") + await asyncio.wait_for(service_called.wait(), 1) + + assert warning_msg in caplog.text From 62e3563752c237898fa240659fb0122bc26e1a91 Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Thu, 10 Mar 2022 19:43:43 +0100 Subject: [PATCH 0353/1054] Bumped to boschshcpy==0.2.30 (#67965) --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index f4fd65748f1..a5927162e50 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -3,7 +3,7 @@ "name": "Bosch SHC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bosch_shc", - "requirements": ["boschshcpy==0.2.29"], + "requirements": ["boschshcpy==0.2.30"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }], "iot_class": "local_push", "codeowners": ["@tschamm"], diff --git a/requirements_all.txt b/requirements_all.txt index 95115a03bbe..376b70a7ae0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ blockchain==1.4.4 bond-api==0.1.16 # homeassistant.components.bosch_shc -boschshcpy==0.2.29 +boschshcpy==0.2.30 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff49f97cb9a..893530eab42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -301,7 +301,7 @@ blinkpy==0.18.0 bond-api==0.1.16 # homeassistant.components.bosch_shc -boschshcpy==0.2.29 +boschshcpy==0.2.30 # homeassistant.components.braviatv bravia-tv==1.0.11 From e43c8b513ef932519923c1832ea4cff14887af32 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 21:26:35 +0100 Subject: [PATCH 0354/1054] Listen to entity registry events for wrapped switch in switch_as_x (#67962) * Listen to entity registry events for wrapped switch in switch_as_x * Simplify test --- .../components/switch_as_x/__init__.py | 23 +++++++- tests/components/switch_as_x/test_init.py | 57 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 6cfea21a349..65b95c59c6d 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -7,8 +7,9 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.event import async_track_entity_registry_updated_event from .light import LightSwitch @@ -23,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" registry = er.async_get(hass) try: - er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) except vol.Invalid: # The entity is identified by an unknown entity registry ID _LOGGER.error( @@ -32,6 +33,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False + async def async_registry_updated(event: Event) -> None: + """Handle entity registry update.""" + data = event.data + if data["action"] == "remove": + await hass.config_entries.async_remove(entry.entry_id) + + if data["action"] != "update" or "entity_id" not in data["changes"]: + return + + # Entity_id changed, reload the config entry + await hass.config_entries.async_reload(entry.entry_id) + + entry.async_on_unload( + async_track_entity_registry_updated_event( + hass, entity_id, async_registry_updated + ) + ) + hass.config_entries.async_setup_platforms(entry, (entry.options["target_domain"],)) return True diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 5797c66fab5..a8875def0ad 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -1,8 +1,11 @@ """Tests for the Switch as X.""" +from unittest.mock import patch + import pytest from homeassistant.components.switch_as_x import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -25,3 +28,57 @@ async def test_config_entry_unregistered_uuid(hass: HomeAssistant, target_domain await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_entity_registry_events(hass: HomeAssistant, target_domain): + """Test entity registry events are tracked.""" + registry = er.async_get(hass) + registry_entry = registry.async_get_or_create("switch", "test", "unique") + switch_entity_id = registry_entry.entity_id + hass.states.async_set(switch_entity_id, "on") + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": registry_entry.id, "target_domain": target_domain}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{target_domain}.abc").state == "on" + + # Change entity_id + new_switch_entity_id = f"{switch_entity_id}_new" + registry.async_update_entity(switch_entity_id, new_entity_id=new_switch_entity_id) + hass.states.async_set(new_switch_entity_id, "off") + await hass.async_block_till_done() + + # Check tracking the new entity_id + await hass.async_block_till_done() + assert hass.states.get(f"{target_domain}.abc").state == "off" + + # The old entity_id should no longer be tracked + hass.states.async_set(switch_entity_id, "on") + await hass.async_block_till_done() + assert hass.states.get(f"{target_domain}.abc").state == "off" + + # Check changing name does not reload the config entry + with patch( + "homeassistant.components.switch_as_x.async_unload_entry", + ) as mock_setup_entry: + registry.async_update_entity(new_switch_entity_id, name="New name") + await hass.async_block_till_done() + mock_setup_entry.assert_not_called() + + # Check removing the entity removes the config entry + registry.async_remove(new_switch_entity_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{target_domain}.abc") is None + assert registry.async_get(f"{target_domain}.abc") is None + assert len(hass.config_entries.async_entries("switch_as_x")) == 0 From 66d757115cdb49b728b4e270a2b8359422a374c1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 11 Mar 2022 00:21:48 +0000 Subject: [PATCH 0355/1054] [ci skip] Translation update --- .../components/abode/translations/fr.json | 4 +- .../accuweather/translations/fr.json | 2 +- .../components/adax/translations/fr.json | 6 +- .../components/aemet/translations/fr.json | 2 +- .../components/airly/translations/fr.json | 2 +- .../components/airvisual/translations/fr.json | 4 +- .../airvisual/translations/sensor.fr.json | 2 +- .../components/airzone/translations/bg.json | 18 +++++ .../components/airzone/translations/fr.json | 18 +++++ .../components/almond/translations/fr.json | 2 +- .../components/ambee/translations/fr.json | 2 +- .../ambient_station/translations/fr.json | 2 +- .../components/androidtv/translations/fr.json | 2 +- .../aseko_pool_live/translations/fr.json | 2 +- .../components/asuswrt/translations/fr.json | 2 +- .../automation/translations/fr.json | 4 +- .../components/awair/translations/fr.json | 4 +- .../binary_sensor/translations/fr.json | 78 +++++++++---------- .../components/braviatv/translations/fr.json | 2 +- .../components/broadlink/translations/fr.json | 4 +- .../components/calendar/translations/fr.json | 4 +- .../components/camera/translations/fr.json | 2 +- .../components/cast/translations/fr.json | 2 +- .../components/climacell/translations/fr.json | 2 +- .../components/climate/translations/fr.json | 2 +- .../components/cpuspeed/translations/fr.json | 2 +- .../crownstone/translations/fr.json | 2 +- .../devolo_home_control/translations/fr.json | 4 +- .../devolo_home_network/translations/fr.json | 2 +- .../components/dlna_dmr/translations/fr.json | 2 +- .../components/dlna_dms/translations/fr.json | 7 ++ .../components/dunehd/translations/fr.json | 2 +- .../components/econet/translations/fr.json | 2 +- .../evil_genius_labs/translations/fr.json | 2 +- .../components/ezviz/translations/fr.json | 2 +- .../components/fan/translations/fr.json | 4 +- .../components/flipr/translations/fr.json | 2 +- .../components/fronius/translations/fr.json | 2 +- .../components/goalzero/translations/fr.json | 4 +- .../components/gree/translations/fr.json | 2 +- .../components/group/translations/ca.json | 65 ++++++++++++++-- .../components/group/translations/el.json | 57 +++++++++++++- .../components/group/translations/en.json | 65 ++++++++++++++-- .../components/group/translations/et.json | 65 ++++++++++++++-- .../components/group/translations/fr.json | 4 +- .../components/group/translations/hu.json | 57 +++++++++++++- .../components/group/translations/id.json | 65 ++++++++++++++-- .../components/group/translations/ja.json | 56 ++++++++++++- .../components/group/translations/pl.json | 69 +++++++++++++--- .../components/group/translations/pt-BR.json | 65 ++++++++++++++-- .../components/group/translations/ru.json | 65 ++++++++++++++-- .../components/hangouts/translations/fr.json | 2 +- .../home_connect/translations/fr.json | 2 +- .../home_plus_control/translations/fr.json | 4 +- .../humidifier/translations/fr.json | 4 +- .../components/icloud/translations/fr.json | 2 +- .../input_boolean/translations/fr.json | 4 +- .../intellifire/translations/fr.json | 2 +- .../components/ios/translations/fr.json | 2 +- .../components/iss/translations/fr.json | 4 +- .../components/isy994/translations/fr.json | 2 +- .../components/izone/translations/fr.json | 2 +- .../kaleidescape/translations/fr.json | 26 +++++++ .../components/kraken/translations/fr.json | 8 -- .../components/kulersky/translations/fr.json | 2 +- .../components/light/translations/fr.json | 4 +- .../components/local_ip/translations/fr.json | 2 +- .../components/locative/translations/fr.json | 2 +- .../logi_circle/translations/fr.json | 2 +- .../components/lyric/translations/fr.json | 2 +- .../components/mazda/translations/fr.json | 2 +- .../media_player/translations/fr.json | 6 +- .../components/melcloud/translations/fr.json | 2 +- .../modern_forms/translations/fr.json | 2 +- .../components/neato/translations/fr.json | 6 +- .../components/nest/translations/fr.json | 4 +- .../components/netatmo/translations/fr.json | 4 +- .../ondilo_ico/translations/fr.json | 2 +- .../components/openuv/translations/fr.json | 2 +- .../openweathermap/translations/fr.json | 2 +- .../components/plaato/translations/fr.json | 2 +- .../plum_lightpad/translations/fr.json | 2 +- .../components/point/translations/fr.json | 4 +- .../components/poolsense/translations/fr.json | 4 +- .../components/profiler/translations/fr.json | 2 +- .../components/remote/translations/fr.json | 4 +- .../components/renault/translations/fr.json | 2 +- .../translations/fr.json | 2 +- .../components/rpi_power/translations/fr.json | 2 +- .../components/script/translations/fr.json | 4 +- .../components/season/translations/fr.json | 7 ++ .../components/sense/translations/fr.json | 2 +- .../components/sensibo/translations/fr.json | 5 ++ .../components/sensor/translations/fr.json | 4 +- .../simplisafe/translations/fr.json | 2 +- .../components/smappee/translations/fr.json | 4 +- .../components/smarthab/translations/fr.json | 2 +- .../components/smarttub/translations/fr.json | 2 +- .../components/solaredge/translations/fr.json | 2 +- .../components/soma/translations/fr.json | 2 +- .../components/somfy/translations/fr.json | 4 +- .../speedtestdotnet/translations/fr.json | 2 +- .../components/spotify/translations/fr.json | 2 +- .../components/switch/translations/fr.json | 4 +- .../switch_as_x/translations/ca.json | 14 ++++ .../switch_as_x/translations/el.json | 14 ++++ .../switch_as_x/translations/en.json | 14 ++++ .../switch_as_x/translations/et.json | 14 ++++ .../switch_as_x/translations/hu.json | 14 ++++ .../switch_as_x/translations/id.json | 14 ++++ .../switch_as_x/translations/ja.json | 14 ++++ .../switch_as_x/translations/nl.json | 12 +++ .../switch_as_x/translations/pl.json | 14 ++++ .../switch_as_x/translations/pt-BR.json | 14 ++++ .../switch_as_x/translations/ru.json | 14 ++++ .../switcher_kis/translations/fr.json | 2 +- .../tellduslive/translations/fr.json | 2 +- .../components/tile/translations/fr.json | 2 +- .../components/timer/translations/fr.json | 2 +- .../components/tolo/translations/fr.json | 2 +- .../components/toon/translations/fr.json | 4 +- .../components/tractive/translations/fr.json | 2 +- .../tuya/translations/select.fr.json | 14 ++-- .../uptimerobot/translations/fr.json | 2 +- .../components/vacuum/translations/fr.json | 6 +- .../components/verisure/translations/fr.json | 4 +- .../components/vesync/translations/fr.json | 2 +- .../water_heater/translations/fr.json | 2 +- .../components/withings/translations/fr.json | 4 +- .../components/wiz/translations/fr.json | 2 +- .../wled/translations/select.fr.json | 4 +- .../components/xbox/translations/fr.json | 2 +- .../yamaha_musiccast/translations/fr.json | 2 +- .../components/zerproc/translations/fr.json | 2 +- 134 files changed, 979 insertions(+), 265 deletions(-) create mode 100644 homeassistant/components/airzone/translations/bg.json create mode 100644 homeassistant/components/airzone/translations/fr.json create mode 100644 homeassistant/components/kaleidescape/translations/fr.json create mode 100644 homeassistant/components/season/translations/fr.json create mode 100644 homeassistant/components/switch_as_x/translations/ca.json create mode 100644 homeassistant/components/switch_as_x/translations/el.json create mode 100644 homeassistant/components/switch_as_x/translations/en.json create mode 100644 homeassistant/components/switch_as_x/translations/et.json create mode 100644 homeassistant/components/switch_as_x/translations/hu.json create mode 100644 homeassistant/components/switch_as_x/translations/id.json create mode 100644 homeassistant/components/switch_as_x/translations/ja.json create mode 100644 homeassistant/components/switch_as_x/translations/nl.json create mode 100644 homeassistant/components/switch_as_x/translations/pl.json create mode 100644 homeassistant/components/switch_as_x/translations/pt-BR.json create mode 100644 homeassistant/components/switch_as_x/translations/ru.json diff --git a/homeassistant/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json index fb5a079d405..d7b2935bbb9 100644 --- a/homeassistant/components/abode/translations/fr.json +++ b/homeassistant/components/abode/translations/fr.json @@ -19,14 +19,14 @@ "reauth_confirm": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Remplissez vos informations de connexion Abode" }, "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Remplissez vos informations de connexion Abode" } diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 7c04e51da23..d1e545f5c68 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API invalide", "requests_exceeded": "Le nombre autoris\u00e9 de requ\u00eates adress\u00e9es \u00e0 l'API AccuWeather a \u00e9t\u00e9 d\u00e9pass\u00e9. Vous devez attendre ou modifier la cl\u00e9 API." }, "step": { diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index eefd0693e24..dc704a4053e 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -14,13 +14,13 @@ "cloud": { "data": { "account_id": "Identifiant de compte", - "password": "Mot der passe" + "password": "Mot de passe" } }, "local": { "data": { - "wifi_pswd": "Mot de passe WiFi", - "wifi_ssid": "identifiant Wifi" + "wifi_pswd": "Mot de passe Wi-Fi", + "wifi_ssid": "R\u00e9seau Wi-Fi" }, "description": "R\u00e9initialisez le radiateur en appuyant sur + et OK jusqu'\u00e0 ce que l'\u00e9cran affiche \u00ab\u00a0Reset\u00a0\u00bb. Appuyez ensuite sur le bouton OK du radiateur et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que le voyant bleu commence \u00e0 clignoter avant d'appuyer sur Soumettre. La configuration du chauffage peut prendre quelques minutes." }, diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index 4ad76320f03..5740048fc48 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API invalide" }, "step": { "user": { diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 945de28e07a..132cea46fbf 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API invalide", "wrong_location": "Aucune station de mesure Airly dans cette zone." }, "step": { diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 4817a720225..a52c2ad3d14 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "general_error": "Erreur inattendue", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API invalide", "location_not_found": "Emplacement introuvable" }, "step": { @@ -25,7 +25,7 @@ "api_key": "Cl\u00e9 d'API", "city": "Ville", "country": "Pays", - "state": "Etat" + "state": "\u00c9tat" }, "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.", "title": "Configurer un lieu g\u00e9ographique" diff --git a/homeassistant/components/airvisual/translations/sensor.fr.json b/homeassistant/components/airvisual/translations/sensor.fr.json index 3050d6fb158..47feff6bd79 100644 --- a/homeassistant/components/airvisual/translations/sensor.fr.json +++ b/homeassistant/components/airvisual/translations/sensor.fr.json @@ -11,7 +11,7 @@ "airvisual__pollutant_level": { "good": "Bon", "hazardous": "Hasardeux", - "moderate": "Mod\u00e9rer", + "moderate": "Mod\u00e9r\u00e9", "unhealthy": "Malsain", "unhealthy_sensitive": "Malsain pour les groupes sensibles", "very_unhealthy": "Tr\u00e8s malsain" diff --git a/homeassistant/components/airzone/translations/bg.json b/homeassistant/components/airzone/translations/bg.json new file mode 100644 index 00000000000..cc5f200ef95 --- /dev/null +++ b/homeassistant/components/airzone/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/fr.json b/homeassistant/components/airzone/translations/fr.json new file mode 100644 index 00000000000..4cf889ff02d --- /dev/null +++ b/homeassistant/components/airzone/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json index a464e8b56e9..6c38df7dec1 100644 --- a/homeassistant/components/almond/translations/fr.json +++ b/homeassistant/components/almond/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u00c9chec de connexion", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "step": { diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json index dc968329b49..211a4f87366 100644 --- a/homeassistant/components/ambee/translations/fr.json +++ b/homeassistant/components/ambee/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API invalide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/ambient_station/translations/fr.json b/homeassistant/components/ambient_station/translations/fr.json index 1877a0af4ff..2991d0a966b 100644 --- a/homeassistant/components/ambient_station/translations/fr.json +++ b/homeassistant/components/ambient_station/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_key": "Cl\u00e9 API invalide", + "invalid_key": "Cl\u00e9 d'API invalide", "no_devices": "Aucun appareil trouv\u00e9 dans le compte" }, "step": { diff --git a/homeassistant/components/androidtv/translations/fr.json b/homeassistant/components/androidtv/translations/fr.json index ea79fddb98a..c5e85b21781 100644 --- a/homeassistant/components/androidtv/translations/fr.json +++ b/homeassistant/components/androidtv/translations/fr.json @@ -7,7 +7,7 @@ "error": { "adbkey_not_file": "Fichier de cl\u00e9 ADB introuvable", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "key_and_server": "Fournissez uniquement la cl\u00e9 ADB ou le serveur ADB", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/aseko_pool_live/translations/fr.json b/homeassistant/components/aseko_pool_live/translations/fr.json index d28b22f8d98..31df206cdda 100644 --- a/homeassistant/components/aseko_pool_live/translations/fr.json +++ b/homeassistant/components/aseko_pool_live/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } } diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json index 0d53f3f24cf..02ef17a4f33 100644 --- a/homeassistant/components/asuswrt/translations/fr.json +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "pwd_and_ssh": "Fournissez uniquement le mot de passe ou le fichier de cl\u00e9 SSH", "pwd_or_ssh": "Veuillez fournir un mot de passe ou un fichier de cl\u00e9 SSH", "ssh_not_file": "Fichier cl\u00e9 SSH non trouv\u00e9", diff --git a/homeassistant/components/automation/translations/fr.json b/homeassistant/components/automation/translations/fr.json index 30426582414..62731da356a 100644 --- a/homeassistant/components/automation/translations/fr.json +++ b/homeassistant/components/automation/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Automatisation" diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 65d550b52a6..7182117fa53 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -13,14 +13,14 @@ "reauth": { "data": { "access_token": "Jeton d'acc\u00e8s", - "email": "Email" + "email": "Courriel" }, "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." }, "user": { "data": { "access_token": "Jeton d'acc\u00e8s", - "email": "Email" + "email": "Courriel" }, "description": "Vous devez vous inscrire pour un jeton d'acc\u00e8s d\u00e9veloppeur Awair sur: https://developer.getawair.com/onboard/login" } diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index 9c1eaf871b0..b6ffe195cd1 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} batterie faible", + "is_bat_low": "{entity_name} est faible en batterie", "is_co": "{entity_name} d\u00e9tecte du monoxyde de carbone", "is_cold": "{entity_name} est froid", "is_connected": "{entity_name} est connect\u00e9", @@ -12,7 +12,7 @@ "is_moist": "{entity_name} est humide", "is_motion": "{entity_name} d\u00e9tecte du mouvement", "is_moving": "{entity_name} se d\u00e9place", - "is_no_co": "{entity_name} ne d\u00e9tecte pas le monoxyde de carbone", + "is_no_co": "{entity_name} ne d\u00e9tecte pas de monoxyde de carbone", "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", @@ -21,7 +21,7 @@ "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", "is_no_update": "{entity_name} est \u00e0 jour", "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", - "is_not_bat_low": "{entity_name} batterie normale", + "is_not_bat_low": "{entity_name} n'est pas faible en batterie", "is_not_cold": "{entity_name} n'est pas froid", "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", "is_not_hot": "{entity_name} n'est pas chaud", @@ -34,7 +34,7 @@ "is_not_powered": "{entity_name} n'est pas aliment\u00e9", "is_not_present": "{entity_name} n'est pas pr\u00e9sent", "is_not_running": "{entity_name} n'est pas en cours d'ex\u00e9cution", - "is_not_tampered": "{entity_name} ne d\u00e9tecte pas la falsification", + "is_not_tampered": "{entity_name} ne d\u00e9tecte pas de manipulation", "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", "is_occupied": "{entity_name} est occup\u00e9", "is_off": "{entity_name} est d\u00e9sactiv\u00e9", @@ -47,43 +47,43 @@ "is_running": "{entity_name} est en cours d'ex\u00e9cution", "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", "is_sound": "{entity_name} d\u00e9tecte du son", - "is_tampered": "{entity_name} d\u00e9tecte une falsification", + "is_tampered": "{entity_name} d\u00e9tecte une manipulation", "is_unsafe": "{entity_name} est dangereux", "is_update": "{entity_name} a une mise \u00e0 jour disponible", - "is_vibration": "{entity_name} d\u00e9tecte des vibrations" + "is_vibration": "{entity_name} d\u00e9tecte une vibration" }, "trigger_type": { - "bat_low": "{entity_name} batterie faible", - "co": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter du monoxyde de carbone", + "bat_low": "{entity_name} est devenu faible en batterie", + "co": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du monoxyde de carbone", "cold": "{entity_name} est devenu froid", - "connected": "{entity_name} connect\u00e9", + "connected": "{entity_name} s'est connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", "hot": "{entity_name} est devenu chaud", - "is_not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter la falsification", - "is_tampered": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter une falsification", - "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", - "locked": "{entity_name} verrouill\u00e9", + "is_not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter une manipulation", + "is_tampered": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter une manipulation", + "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter de la lumi\u00e8re", + "locked": "{entity_name} s'est verrouill\u00e9", "moist": "{entity_name} est devenu humide", "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement", "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer", - "no_co": "{entity_name} cess\u00e9 de d\u00e9tecter le monoxyde de carbone", - "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz", - "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter la lumi\u00e8re", - "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le mouvement", + "no_co": "{entity_name} a cess\u00e9 de d\u00e9tecter du monoxyde de carbone", + "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter du gaz", + "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter de la lumi\u00e8re", + "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter du mouvement", "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me", "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e", "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit", "no_update": "{entity_name} a \u00e9t\u00e9 mis \u00e0 jour", - "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", - "not_bat_low": "{entity_name} batterie normale", + "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter une vibration", + "not_bat_low": "{entity_name} n'est plus faible en batterie", "not_cold": "{entity_name} n'est plus froid", - "not_connected": "{entity_name} d\u00e9connect\u00e9", + "not_connected": "{entity_name} s'est d\u00e9connect\u00e9", "not_hot": "{entity_name} n'est plus chaud", - "not_locked": "{entity_name} d\u00e9verrouill\u00e9", + "not_locked": "{entity_name} s'est d\u00e9verrouill\u00e9", "not_moist": "{entity_name} est devenu sec", "not_moving": "{entity_name} a cess\u00e9 de bouger", "not_occupied": "{entity_name} est devenu non occup\u00e9", - "not_opened": "{entity_name} ferm\u00e9", + "not_opened": "{entity_name} s'est ferm\u00e9", "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", "not_powered": "{entity_name} non aliment\u00e9", "not_present": "{entity_name} non pr\u00e9sent", @@ -111,23 +111,23 @@ "co": "monoxyde de carbone", "cold": "froid", "gas": "gaz", - "heat": "Chauffer", + "heat": "chaleur", "moisture": "humidit\u00e9", "motion": "mouvement", "occupancy": "occupation", - "power": "Puissance", - "problem": "Probl\u00e8me", + "power": "puissance", + "problem": "probl\u00e8me", "smoke": "fum\u00e9e", "sound": "son", "vibration": "vibration" }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" }, "battery": { - "off": "Normal", + "off": "Normale", "on": "Faible" }, "battery_charging": { @@ -135,15 +135,15 @@ "on": "En charge" }, "carbon_monoxide": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, "co": { - "off": "Clair", - "on": "D\u00e9tect\u00e9e" + "off": "Non d\u00e9tect\u00e9", + "on": "D\u00e9tect\u00e9" }, "cold": { - "off": "Normale", + "off": "Normal", "on": "Froid" }, "connectivity": { @@ -163,7 +163,7 @@ "on": "D\u00e9tect\u00e9" }, "heat": { - "off": "Normale", + "off": "Normal", "on": "Chaud" }, "light": { @@ -179,7 +179,7 @@ "on": "Humide" }, "motion": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, "moving": { @@ -187,8 +187,8 @@ "on": "En mouvement" }, "occupancy": { - "off": "RAS", - "on": "D\u00e9tect\u00e9" + "off": "Non d\u00e9tect\u00e9e", + "on": "D\u00e9tect\u00e9e" }, "opening": { "off": "Ferm\u00e9", @@ -215,8 +215,8 @@ "on": "Dangereux" }, "smoke": { - "off": "Non d\u00e9tect\u00e9", - "on": "D\u00e9tect\u00e9" + "off": "Non d\u00e9tect\u00e9e", + "on": "D\u00e9tect\u00e9e" }, "sound": { "off": "Non d\u00e9tect\u00e9", @@ -227,7 +227,7 @@ "on": "Mise \u00e0 jour disponible" }, "vibration": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9e", "on": "D\u00e9tect\u00e9e" }, "window": { diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index d609f1a2fa1..0b0a0959b3e 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "unsupported_model": "Votre mod\u00e8le de t\u00e9l\u00e9viseur n'est pas pris en charge." }, "step": { diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json index e39b722d8c9..665e62b77a5 100644 --- a/homeassistant/components/broadlink/translations/fr.json +++ b/homeassistant/components/broadlink/translations/fr.json @@ -4,13 +4,13 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "not_supported": "Dispositif non pris en charge", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "unknown": "Erreur inattendue" }, "flow_title": "{name} ( {model} \u00e0 {host} )", diff --git a/homeassistant/components/calendar/translations/fr.json b/homeassistant/components/calendar/translations/fr.json index 70aaa6f0292..18d131e6143 100644 --- a/homeassistant/components/calendar/translations/fr.json +++ b/homeassistant/components/calendar/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Calendrier" diff --git a/homeassistant/components/camera/translations/fr.json b/homeassistant/components/camera/translations/fr.json index d4f5cd31afc..01482ff57cc 100644 --- a/homeassistant/components/camera/translations/fr.json +++ b/homeassistant/components/camera/translations/fr.json @@ -1,7 +1,7 @@ { "state": { "_": { - "idle": "En veille", + "idle": "Inactif", "recording": "Enregistrement", "streaming": "Diffusion en cours" } diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index c07122f820f..6cc720e5630 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -15,7 +15,7 @@ "title": "Google Cast" }, "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 38182d67c4a..0492c8a4d46 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API invalide", "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/climate/translations/fr.json b/homeassistant/components/climate/translations/fr.json index 913c2579478..58de5fc4e76 100644 --- a/homeassistant/components/climate/translations/fr.json +++ b/homeassistant/components/climate/translations/fr.json @@ -22,7 +22,7 @@ "fan_only": "Ventilateur seul", "heat": "Chauffe", "heat_cool": "Chaud/Froid", - "off": "Inactif" + "off": "D\u00e9sactiv\u00e9" } }, "title": "Thermostat" diff --git a/homeassistant/components/cpuspeed/translations/fr.json b/homeassistant/components/cpuspeed/translations/fr.json index 20b9ebbb35c..5de49927c49 100644 --- a/homeassistant/components/cpuspeed/translations/fr.json +++ b/homeassistant/components/cpuspeed/translations/fr.json @@ -7,7 +7,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Vitesse CPU" } } diff --git a/homeassistant/components/crownstone/translations/fr.json b/homeassistant/components/crownstone/translations/fr.json index 783cd25bd49..331add3d2a8 100644 --- a/homeassistant/components/crownstone/translations/fr.json +++ b/homeassistant/components/crownstone/translations/fr.json @@ -34,7 +34,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "title": "Compte Crownstone" diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 020b469092d..387edb9598d 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -13,14 +13,14 @@ "data": { "mydevolo_url": "URL mydevolo", "password": "Mot de passe", - "username": "Email / devolo ID" + "username": "Courriel / devolo ID" } }, "zeroconf_confirm": { "data": { "mydevolo_url": "mydevolo URL", "password": "Mot de passe", - "username": "Email / devolo ID" + "username": "Courriel / devolo ID" } } } diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json index 49dc24e0db1..d777ba890b1 100644 --- a/homeassistant/components/devolo_home_network/translations/fr.json +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -14,7 +14,7 @@ "data": { "ip_address": "Adresse IP" }, - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "zeroconf_confirm": { "description": "Voulez-vous ajouter le p\u00e9riph\u00e9rique r\u00e9seau domestique devolo avec le nom d'h\u00f4te ` {host_name} ` \u00e0 Home Assistant\u00a0?", diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index f7a1b9cd71c..6d9b294d69b 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -18,7 +18,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "import_turn_on": { "description": "Veuillez allumer l'appareil et cliquer sur soumettre pour continuer la migration" diff --git a/homeassistant/components/dlna_dms/translations/fr.json b/homeassistant/components/dlna_dms/translations/fr.json index dfa510250ac..3908dd082ea 100644 --- a/homeassistant/components/dlna_dms/translations/fr.json +++ b/homeassistant/components/dlna_dms/translations/fr.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/dunehd/translations/fr.json b/homeassistant/components/dunehd/translations/fr.json index 0e8cb6d6ff8..4d19f87ad45 100644 --- a/homeassistant/components/dunehd/translations/fr.json +++ b/homeassistant/components/dunehd/translations/fr.json @@ -6,7 +6,7 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" }, "step": { "user": { diff --git a/homeassistant/components/econet/translations/fr.json b/homeassistant/components/econet/translations/fr.json index e6081bef90a..69b15c3857b 100644 --- a/homeassistant/components/econet/translations/fr.json +++ b/homeassistant/components/econet/translations/fr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "title": "Configurer le compte Rheem EcoNet" diff --git a/homeassistant/components/evil_genius_labs/translations/fr.json b/homeassistant/components/evil_genius_labs/translations/fr.json index 80b3d803851..e7f5b200894 100644 --- a/homeassistant/components/evil_genius_labs/translations/fr.json +++ b/homeassistant/components/evil_genius_labs/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion", + "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ezviz/translations/fr.json b/homeassistant/components/ezviz/translations/fr.json index ddce689a2ba..2d1e4b5cb04 100644 --- a/homeassistant/components/ezviz/translations/fr.json +++ b/homeassistant/components/ezviz/translations/fr.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" }, "flow_title": "{serial}", "step": { diff --git a/homeassistant/components/fan/translations/fr.json b/homeassistant/components/fan/translations/fr.json index e1c9567dc0f..e9cf666b725 100644 --- a/homeassistant/components/fan/translations/fr.json +++ b/homeassistant/components/fan/translations/fr.json @@ -17,8 +17,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Ventilateur" diff --git a/homeassistant/components/flipr/translations/fr.json b/homeassistant/components/flipr/translations/fr.json index ec9260aa8a7..faa7c920577 100644 --- a/homeassistant/components/flipr/translations/fr.json +++ b/homeassistant/components/flipr/translations/fr.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Connectez-vous \u00e0 votre compte Flipr.", diff --git a/homeassistant/components/fronius/translations/fr.json b/homeassistant/components/fronius/translations/fr.json index e7ea85962bd..3eba668c232 100644 --- a/homeassistant/components/fronius/translations/fr.json +++ b/homeassistant/components/fronius/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 7def0d06402..8b3c3514606 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/gree/translations/fr.json b/homeassistant/components/gree/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/gree/translations/fr.json +++ b/homeassistant/components/gree/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index bbf9a1657dd..b17d98b5084 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Totes les entitats", + "entities": "Membres", + "name": "Nom" + }, + "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre.", + "title": "Nou grup" + }, "cover": { "data": { - "entities": "Membres del grup", - "name": "Nom del grup" + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" }, "description": "Selecciona les opcions del grup" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Membres del grup", - "name": "Nom del grup" + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" }, "description": "Selecciona les opcions del grup" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Membres del grup", - "name": "Nom del grup" + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" }, "description": "Selecciona les opcions del grup" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Membres del grup", - "name": "Nom del grup" + "entities": "Membres", + "name": "Nom", + "title": "Nou grup" }, "description": "Selecciona les opcions del grup" }, @@ -58,6 +71,42 @@ "entities": "Membres del grup" }, "description": "Selecciona les opcions del grup" + }, + "user": { + "data": { + "group_type": "Tipus de grup" + }, + "title": "Nou grup" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Totes les entitats", + "entities": "Membres" + } + }, + "cover_options": { + "data": { + "entities": "Membres" + } + }, + "fan_options": { + "data": { + "entities": "Membres" + } + }, + "light_options": { + "data": { + "entities": "Membres" + } + }, + "media_player_options": { + "data": { + "entities": "Membres" + } } } }, diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index 8cecb56bc9f..bebfb8f1cb8 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf.", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, "cover": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, @@ -17,7 +27,8 @@ "fan": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, @@ -36,7 +47,8 @@ "light": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, @@ -49,7 +61,8 @@ "media_player": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, @@ -58,6 +71,42 @@ "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "user": { + "data": { + "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "cover_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "fan_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "light_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } + }, + "media_player_options": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7" + } } } }, diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index e0b55e89620..b2b5750795e 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "All entities", + "entities": "Members", + "name": "Name" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", + "title": "New Group" + }, "cover": { "data": { - "entities": "Group members", - "name": "Group name" + "entities": "Members", + "name": "Name", + "title": "New Group" }, "description": "Select group options" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Group members", - "name": "Group name" + "entities": "Members", + "name": "Name", + "title": "New Group" }, "description": "Select group options" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Group members", - "name": "Group name" + "entities": "Members", + "name": "Name", + "title": "New Group" }, "description": "Select group options" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Group members", - "name": "Group name" + "entities": "Members", + "name": "Name", + "title": "New Group" }, "description": "Select group options" }, @@ -58,6 +71,42 @@ "entities": "Group members" }, "description": "Select group options" + }, + "user": { + "data": { + "group_type": "Group type" + }, + "title": "New Group" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "All entities", + "entities": "Members" + } + }, + "cover_options": { + "data": { + "entities": "Members" + } + }, + "fan_options": { + "data": { + "entities": "Members" + } + }, + "light_options": { + "data": { + "entities": "Members" + } + }, + "media_player_options": { + "data": { + "entities": "Members" + } } } }, diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index b6ba60ead88..c45094aeedd 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Liikmed", + "name": "Nimi" + }, + "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud.", + "title": "Uus r\u00fchm" + }, "cover": { "data": { - "entities": "R\u00fchma liikmed", - "name": "R\u00fchma nimi" + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" }, "description": "R\u00fchmasuvandite valimine" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "R\u00fchma liikmed", - "name": "R\u00fchma nimi" + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" }, "description": "R\u00fchmasuvandite valimine" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "R\u00fchma liikmed", - "name": "R\u00fchma nimi" + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" }, "description": "R\u00fchmasuvandite valimine" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "R\u00fchma liikmed", - "name": "R\u00fchma nimi" + "entities": "Liikmed", + "name": "Nimi", + "title": "Uus r\u00fchm" }, "description": "R\u00fchmasuvandite valimine" }, @@ -58,6 +71,42 @@ "entities": "R\u00fchma liikmed" }, "description": "R\u00fchmasuvandite valimine" + }, + "user": { + "data": { + "group_type": "R\u00fchma t\u00fc\u00fcp" + }, + "title": "Uus r\u00fchm" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Liikmed" + } + }, + "cover_options": { + "data": { + "entities": "Liikmed" + } + }, + "fan_options": { + "data": { + "entities": "Liikmed" + } + }, + "light_options": { + "data": { + "entities": "Liikmed" + } + }, + "media_player_options": { + "data": { + "entities": "Liikmed" + } } } }, diff --git a/homeassistant/components/group/translations/fr.json b/homeassistant/components/group/translations/fr.json index f1ade09f650..97d2ed91941 100644 --- a/homeassistant/components/group/translations/fr.json +++ b/homeassistant/components/group/translations/fr.json @@ -5,9 +5,9 @@ "home": "Pr\u00e9sent", "locked": "Verrouill\u00e9", "not_home": "Absent", - "off": "Inactif", + "off": "D\u00e9sactiv\u00e9", "ok": "OK", - "on": "Actif", + "on": "Activ\u00e9", "open": "Ouvert", "problem": "Probl\u00e8me", "unlocked": "D\u00e9verrouill\u00e9" diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index 13faa9d62f3..92d9c7b06fe 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "Csoporttagok", + "name": "N\u00e9v" + }, + "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van.", + "title": "\u00daj csoport" + }, "cover": { "data": { "entities": "A csoport tagjai", - "name": "Csoport neve" + "name": "Csoport neve", + "title": "\u00daj csoport" }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" }, @@ -17,7 +27,8 @@ "fan": { "data": { "entities": "A csoport tagjai", - "name": "A csoport elnevez\u00e9se" + "name": "A csoport elnevez\u00e9se", + "title": "\u00daj csoport" }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" }, @@ -36,7 +47,8 @@ "light": { "data": { "entities": "A csoport tagjai", - "name": "A csoport elnevez\u00e9se" + "name": "A csoport elnevez\u00e9se", + "title": "\u00daj csoport" }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" }, @@ -49,7 +61,8 @@ "media_player": { "data": { "entities": "A csoport tagjai", - "name": "A csoport elnevez\u00e9se" + "name": "A csoport elnevez\u00e9se", + "title": "\u00daj csoport" }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" }, @@ -58,6 +71,42 @@ "entities": "A csoport tagjai" }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + }, + "user": { + "data": { + "group_type": "Csoport t\u00edpusa" + }, + "title": "\u00daj csoport" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "Csoporttagok" + } + }, + "cover_options": { + "data": { + "entities": "Csoporttagok" + } + }, + "fan_options": { + "data": { + "entities": "Csoporttagok" + } + }, + "light_options": { + "data": { + "entities": "Csoporttagok" + } + }, + "media_player_options": { + "data": { + "entities": "Csoporttagok" + } } } }, diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json index b42b663e546..2a92526d644 100644 --- a/homeassistant/components/group/translations/id.json +++ b/homeassistant/components/group/translations/id.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Semua entitas", + "entities": "Anggota", + "name": "Nama" + }, + "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala.", + "title": "Grup Baru" + }, "cover": { "data": { - "entities": "Anggota grup", - "name": "Nama grup" + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" }, "description": "Pilih opsi grup" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Anggota grup", - "name": "Nama grup" + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" }, "description": "Pilih opsi grup" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Anggota grup", - "name": "Nama grup" + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" }, "description": "Pilih opsi grup" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Anggota grup", - "name": "Nama grup" + "entities": "Anggota", + "name": "Nama", + "title": "Grup Baru" }, "description": "Pilih opsi grup" }, @@ -58,6 +71,42 @@ "entities": "Anggota grup" }, "description": "Pilih opsi grup" + }, + "user": { + "data": { + "group_type": "Jenis grup" + }, + "title": "Grup Baru" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Semua entitas", + "entities": "Anggota" + } + }, + "cover_options": { + "data": { + "entities": "Anggota" + } + }, + "fan_options": { + "data": { + "entities": "Anggota" + } + }, + "light_options": { + "data": { + "entities": "Anggota" + } + }, + "media_player_options": { + "data": { + "entities": "Anggota" + } } } }, diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 39fe05e18ec..f71c8607a7f 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -1,10 +1,19 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc", + "name": "\u540d\u524d" + }, + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, "cover": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" }, @@ -17,7 +26,8 @@ "fan": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" }, @@ -36,7 +46,8 @@ "light": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" }, @@ -49,7 +60,8 @@ "media_player": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" }, @@ -58,6 +70,42 @@ "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + }, + "user": { + "data": { + "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" + }, + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "cover_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "fan_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "light_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } + }, + "media_player_options": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc" + } } } }, diff --git a/homeassistant/components/group/translations/pl.json b/homeassistant/components/group/translations/pl.json index fe6653cfd5e..08595dd8a59 100644 --- a/homeassistant/components/group/translations/pl.json +++ b/homeassistant/components/group/translations/pl.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje", + "name": "Nazwa" + }, + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d s\u0105 wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona.", + "title": "Nowa grupa" + }, "cover": { "data": { - "entities": "Encje w grupie", - "name": "Nazwa grupy" + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" }, "description": "Wybierz opcje grupy" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Encje w grupie", - "name": "Nazwa grupy" + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" }, "description": "Wybierz opcje grupy" }, @@ -29,14 +40,15 @@ }, "init": { "data": { - "group_type": "Typ grupy" + "group_type": "Rodzaj grupy" }, - "description": "Wybierz typ grupy" + "description": "Wybierz rodzaj grupy" }, "light": { "data": { - "entities": "Encje w grupie", - "name": "Nazwa grupy" + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" }, "description": "Wybierz opcje grupy" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Encje w grupie", - "name": "Nazwa grupy" + "entities": "Encje", + "name": "Nazwa", + "title": "Nowa grupa" }, "description": "Wybierz opcje grupy" }, @@ -58,6 +71,42 @@ "entities": "Encje w grupie" }, "description": "Wybierz opcje grupy" + }, + "user": { + "data": { + "group_type": "Rodzaj grupy" + }, + "title": "Nowa grupa" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje" + } + }, + "cover_options": { + "data": { + "entities": "Encje" + } + }, + "fan_options": { + "data": { + "entities": "Encje" + } + }, + "light_options": { + "data": { + "entities": "Encje" + } + }, + "media_player_options": { + "data": { + "entities": "Encje" + } } } }, diff --git a/homeassistant/components/group/translations/pt-BR.json b/homeassistant/components/group/translations/pt-BR.json index bceaaa4fb43..5959ba66da7 100644 --- a/homeassistant/components/group/translations/pt-BR.json +++ b/homeassistant/components/group/translations/pt-BR.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Todas as entidades", + "entities": "Membros", + "name": "Nome" + }, + "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado.", + "title": "Novo grupo" + }, "cover": { "data": { - "entities": "Membros do grupo", - "name": "Nome do grupo" + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" }, "description": "Selecione as op\u00e7\u00f5es do grupo" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Membros do grupo", - "name": "Nome do grupo" + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" }, "description": "Selecione as op\u00e7\u00f5es do grupo" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Membros do grupo", - "name": "Nome do grupo" + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" }, "description": "Selecione as op\u00e7\u00f5es do grupo" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Membros do grupo", - "name": "Nome do grupo" + "entities": "Membros", + "name": "Nome", + "title": "Novo grupo" }, "description": "Selecione as op\u00e7\u00f5es do grupo" }, @@ -58,6 +71,42 @@ "entities": "Membros do grupo" }, "description": "Selecione as op\u00e7\u00f5es do grupo" + }, + "user": { + "data": { + "group_type": "Tipo de grupo" + }, + "title": "Novo grupo" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Todas as entidades", + "entities": "Membros" + } + }, + "cover_options": { + "data": { + "entities": "Membros" + } + }, + "fan_options": { + "data": { + "entities": "Membros" + } + }, + "light_options": { + "data": { + "entities": "Membros" + } + }, + "media_player_options": { + "data": { + "entities": "Membros" + } } } }, diff --git a/homeassistant/components/group/translations/ru.json b/homeassistant/components/group/translations/ru.json index 3ec3b2b0c97..baba258b654 100644 --- a/homeassistant/components/group/translations/ru.json +++ b/homeassistant/components/group/translations/ru.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432.", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + }, "cover": { "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b" + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." }, @@ -58,6 +71,42 @@ "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + }, + "user": { + "data": { + "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" + }, + "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "cover_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "fan_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "light_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } + }, + "media_player_options": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" + } } } }, diff --git a/homeassistant/components/hangouts/translations/fr.json b/homeassistant/components/hangouts/translations/fr.json index ab2d2fc5168..da674b1c775 100644 --- a/homeassistant/components/hangouts/translations/fr.json +++ b/homeassistant/components/hangouts/translations/fr.json @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Code d'autorisation (requis pour l'authentification manuelle)", - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Vide", diff --git a/homeassistant/components/home_connect/translations/fr.json b/homeassistant/components/home_connect/translations/fr.json index 5eba6fa03c1..87a2af8c10a 100644 --- a/homeassistant/components/home_connect/translations/fr.json +++ b/homeassistant/components/home_connect/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json index 489e0499324..4e250b743b7 100644 --- a/homeassistant/components/home_plus_control/translations/fr.json +++ b/homeassistant/components/home_plus_control/translations/fr.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index 3b1b60ebae3..84b24b96904 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -22,8 +22,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Humidificateur" diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index f9c0ceb3db9..1978eb58232 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -28,7 +28,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email", + "username": "Courriel", "with_family": "Avec la famille" }, "description": "Entrez vos identifiants", diff --git a/homeassistant/components/input_boolean/translations/fr.json b/homeassistant/components/input_boolean/translations/fr.json index 1d83af839c1..77bd8bc9e7a 100644 --- a/homeassistant/components/input_boolean/translations/fr.json +++ b/homeassistant/components/input_boolean/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Entr\u00e9e logique" diff --git a/homeassistant/components/intellifire/translations/fr.json b/homeassistant/components/intellifire/translations/fr.json index 88a0aeb68c8..3b0762be825 100644 --- a/homeassistant/components/intellifire/translations/fr.json +++ b/homeassistant/components/intellifire/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "unknown": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/ios/translations/fr.json b/homeassistant/components/ios/translations/fr.json index 85000f60a49..d3d7df17992 100644 --- a/homeassistant/components/ios/translations/fr.json +++ b/homeassistant/components/ios/translations/fr.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/iss/translations/fr.json b/homeassistant/components/iss/translations/fr.json index fd0b2adba42..c51a38d131b 100644 --- a/homeassistant/components/iss/translations/fr.json +++ b/homeassistant/components/iss/translations/fr.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "show_on_map": "Afficher sur la carte?" + "show_on_map": "Afficher sur la carte\u00a0?" }, "description": "Voulez-vous configurer la Station spatiale internationale?" } @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "show_on_map": "Montrer sur la carte" + "show_on_map": "Afficher sur la carte" } } } diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index 654df08431d..e7e330b4e3a 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -16,7 +16,7 @@ "host": "URL", "password": "Mot de passe", "tls": "La version TLS du contr\u00f4leur ISY.", - "username": "Username" + "username": "Nom d'utilisateur" }, "description": "L'entr\u00e9e d'h\u00f4te doit \u00eatre au format URL complet, par exemple, http://192.168.10.100:80", "title": "Connect\u00e9 \u00e0 votre ISY994" diff --git a/homeassistant/components/izone/translations/fr.json b/homeassistant/components/izone/translations/fr.json index eb86962e11b..89abc8a4616 100644 --- a/homeassistant/components/izone/translations/fr.json +++ b/homeassistant/components/izone/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer iZone?" + "description": "Voulez-vous configurer iZone\u00a0?" } } } diff --git a/homeassistant/components/kaleidescape/translations/fr.json b/homeassistant/components/kaleidescape/translations/fr.json new file mode 100644 index 00000000000..83083352c56 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "unknown": "Erreur inattendue", + "unsupported": "Appareil non pris en charge" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unsupported": "Appareil non pris en charge" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "title": "Kaleidescape" + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "title": "Configuration de Kaleidescape" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/fr.json b/homeassistant/components/kraken/translations/fr.json index 1aa7fdfbf54..a307e16a3cc 100644 --- a/homeassistant/components/kraken/translations/fr.json +++ b/homeassistant/components/kraken/translations/fr.json @@ -3,16 +3,8 @@ "abort": { "already_configured": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, - "error": { - "one": "UN", - "other": "AUTRE" - }, "step": { "user": { - "data": { - "one": "UN", - "other": "AUTRE" - }, "description": "Voulez-vous commencer la configuration\u00a0?" } } diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/kulersky/translations/fr.json +++ b/homeassistant/components/kulersky/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index 7863f5ad5eb..7a58a267ed7 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -21,8 +21,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Lumi\u00e8re" diff --git a/homeassistant/components/local_ip/translations/fr.json b/homeassistant/components/local_ip/translations/fr.json index d7d0eef17c1..a527127ccdf 100644 --- a/homeassistant/components/local_ip/translations/fr.json +++ b/homeassistant/components/local_ip/translations/fr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Adresse IP locale" } } diff --git a/homeassistant/components/locative/translations/fr.json b/homeassistant/components/locative/translations/fr.json index 45a6f6fe594..3cb74185f97 100644 --- a/homeassistant/components/locative/translations/fr.json +++ b/homeassistant/components/locative/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Configurer le Locative Webhook" } } diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 6bd22f473e7..61898cd6ca8 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -7,7 +7,7 @@ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "error": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", "invalid_auth": "Authentification invalide" }, diff --git a/homeassistant/components/lyric/translations/fr.json b/homeassistant/components/lyric/translations/fr.json index 25992620652..694f777ca9a 100644 --- a/homeassistant/components/lyric/translations/fr.json +++ b/homeassistant/components/lyric/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json index 1f6f442a3ae..4ce19467267 100644 --- a/homeassistant/components/mazda/translations/fr.json +++ b/homeassistant/components/mazda/translations/fr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe", "region": "R\u00e9gion" }, diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index bcda6d770a3..d34d4fc9da5 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -18,9 +18,9 @@ }, "state": { "_": { - "idle": "En veille", - "off": "Inactif", - "on": "Actif", + "idle": "Inactif", + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9", "paused": "En pause", "playing": "Lecture en cours", "standby": "En veille" diff --git a/homeassistant/components/melcloud/translations/fr.json b/homeassistant/components/melcloud/translations/fr.json index f7f40c68bc5..9846f14fe7e 100644 --- a/homeassistant/components/melcloud/translations/fr.json +++ b/homeassistant/components/melcloud/translations/fr.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "description": "Se connecter en utilisant votre MELCloud compte.", "title": "Se connecter \u00e0 MELCloud" diff --git a/homeassistant/components/modern_forms/translations/fr.json b/homeassistant/components/modern_forms/translations/fr.json index cde37b5251b..d68f4a7f680 100644 --- a/homeassistant/components/modern_forms/translations/fr.json +++ b/homeassistant/components/modern_forms/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index 4348ce3d6f5..df805121f6b 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "create_entry": { @@ -15,7 +15,7 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" }, "reauth_confirm": { - "title": "Voulez-vous commencer la configuration ?" + "title": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index adb7999d772..54c9c700d3e 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 5de581d301b..4cd25a6cace 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, diff --git a/homeassistant/components/ondilo_ico/translations/fr.json b/homeassistant/components/ondilo_ico/translations/fr.json index 540d3e1e6c2..f84926d676f 100644 --- a/homeassistant/components/ondilo_ico/translations/fr.json +++ b/homeassistant/components/ondilo_ico/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index 6acd3144a10..93bf5e8a108 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API invalide" }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json index f8879a04d32..92283d8dff0 100644 --- a/homeassistant/components/openweathermap/translations/fr.json +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide" + "invalid_api_key": "Cl\u00e9 d'API invalide" }, "step": { "user": { diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json index 370796c18f3..f750645dc91 100644 --- a/homeassistant/components/plaato/translations/fr.json +++ b/homeassistant/components/plaato/translations/fr.json @@ -28,7 +28,7 @@ "device_name": "Nommez votre appareil", "device_type": "Type d'appareil Plaato" }, - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Configurer le Webhook Plaato" }, "webhook": { diff --git a/homeassistant/components/plum_lightpad/translations/fr.json b/homeassistant/components/plum_lightpad/translations/fr.json index 20c633e8d0f..53819587be4 100644 --- a/homeassistant/components/plum_lightpad/translations/fr.json +++ b/homeassistant/components/plum_lightpad/translations/fr.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" } } } diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 0d05e0a5363..440bd076bef 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." @@ -23,7 +23,7 @@ "data": { "flow_impl": "Fournisseur" }, - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } diff --git a/homeassistant/components/poolsense/translations/fr.json b/homeassistant/components/poolsense/translations/fr.json index bfe2ecf1bd2..0d0d3069e2b 100644 --- a/homeassistant/components/poolsense/translations/fr.json +++ b/homeassistant/components/poolsense/translations/fr.json @@ -9,10 +9,10 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, - "description": "Voulez-vous commencer la configuration ?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "PoolSense" } } diff --git a/homeassistant/components/profiler/translations/fr.json b/homeassistant/components/profiler/translations/fr.json index 4d10b4f4ecc..1bd91ac8a21 100644 --- a/homeassistant/components/profiler/translations/fr.json +++ b/homeassistant/components/profiler/translations/fr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/remote/translations/fr.json b/homeassistant/components/remote/translations/fr.json index c2052edaab8..1560c52cba0 100644 --- a/homeassistant/components/remote/translations/fr.json +++ b/homeassistant/components/remote/translations/fr.json @@ -18,8 +18,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "T\u00e9l\u00e9commande" diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index 406339b0445..0ad6ec9d85d 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -26,7 +26,7 @@ "data": { "locale": "Lieu", "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "D\u00e9finir les informations d'identification de Renault" } diff --git a/homeassistant/components/rituals_perfume_genie/translations/fr.json b/homeassistant/components/rituals_perfume_genie/translations/fr.json index 2a1fb9c8bb8..223028898c7 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/fr.json +++ b/homeassistant/components/rituals_perfume_genie/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "title": "Connectez-vous \u00e0 votre compte Rituals" diff --git a/homeassistant/components/rpi_power/translations/fr.json b/homeassistant/components/rpi_power/translations/fr.json index 7e4fd715ee0..d6226259b9d 100644 --- a/homeassistant/components/rpi_power/translations/fr.json +++ b/homeassistant/components/rpi_power/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/script/translations/fr.json b/homeassistant/components/script/translations/fr.json index 910192a5f37..fe1481f5a7c 100644 --- a/homeassistant/components/script/translations/fr.json +++ b/homeassistant/components/script/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Script" diff --git a/homeassistant/components/season/translations/fr.json b/homeassistant/components/season/translations/fr.json new file mode 100644 index 00000000000..9c74e1b5026 --- /dev/null +++ b/homeassistant/components/season/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index 0b0d886a335..91fcbeb94bc 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -16,7 +16,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe", "timeout": "D\u00e9lai expir\u00e9" }, diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 79e06ca81a1..60df120d77e 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -10,6 +10,11 @@ "no_devices": "Aucun appareil d\u00e9couvert" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index 3ef16bd17ec..8914886b61b 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -61,8 +61,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Capteur" diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index d0ff1347bbd..912f3e05c01 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -35,7 +35,7 @@ "auth_code": "Code d'autorisation", "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "description": "SimpliSafe s'authentifie avec Home Assistant via l'application Web SimpliSafe. En raison de limitations techniques, il y a une \u00e9tape manuelle \u00e0 la fin de ce processus ; veuillez vous assurer de lire la [documentation]( {docs_url} ) avant de commencer. \n\n 1. Cliquez sur [ici]( {url} ) pour ouvrir l'application Web SimpliSafe et saisissez vos informations d'identification. \n\n 2. Une fois le processus de connexion termin\u00e9, revenez ici et saisissez le code d'autorisation ci-dessous.", "title": "Veuillez saisir vos informations" diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index ba57f8c36c4..d43b5382de8 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -3,11 +3,11 @@ "abort": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_configured_local_device": "Le ou les p\u00e9riph\u00e9riques locaux sont d\u00e9j\u00e0 configur\u00e9s. Veuillez les supprimer avant de configurer un appareil cloud.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "cannot_connect": "\u00c9chec de connexion", "invalid_mdns": "Appareil non pris en charge pour l'int\u00e9gration Smappee.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/smarthab/translations/fr.json b/homeassistant/components/smarthab/translations/fr.json index efbbfe25818..5b74c827287 100644 --- a/homeassistant/components/smarthab/translations/fr.json +++ b/homeassistant/components/smarthab/translations/fr.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Pour des raisons techniques, utilisez un compte sp\u00e9cifique \u00e0 Home Assistant. Vous pouvez cr\u00e9er un compte secondaire depuis l'application SmartHab.", diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index bb481048fff..ec219736281 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" }, "description": "Entrez votre adresse e-mail et votre mot de passe SmartTub pour vous connecter", diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index 36b283a9145..369bac39a1a 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -6,7 +6,7 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "could_not_connect": "Impossible de se connecter \u00e0 l'API solaredge", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API invalide", "site_not_active": "The site n'est pas actif" }, "step": { diff --git a/homeassistant/components/soma/translations/fr.json b/homeassistant/components/soma/translations/fr.json index c9c9bb07643..4feed187711 100644 --- a/homeassistant/components/soma/translations/fr.json +++ b/homeassistant/components/soma/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "connection_error": "\u00c9chec de connexion", "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", "result_error": "SOMA Connect a r\u00e9pondu avec l'\u00e9tat d'erreur." diff --git a/homeassistant/components/somfy/translations/fr.json b/homeassistant/components/somfy/translations/fr.json index 08b978e3f12..0c7a25831bc 100644 --- a/homeassistant/components/somfy/translations/fr.json +++ b/homeassistant/components/somfy/translations/fr.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index 41a1ae7dc2d..4ad47bb6a3e 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } }, diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json index 4422ddef176..6cfcb3486fe 100644 --- a/homeassistant/components/spotify/translations/fr.json +++ b/homeassistant/components/spotify/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", "missing_configuration": "L'int\u00e9gration Spotify n'est pas configur\u00e9e. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "reauth_account_mismatch": "Le compte Spotify authentifi\u00e9 ne correspond pas au compte requis pour la r\u00e9-authentification." }, "create_entry": { diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index e2abc370909..08d14c21f13 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -18,8 +18,8 @@ }, "state": { "_": { - "off": "Inactif", - "on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" } }, "title": "Interrupteur" diff --git a/homeassistant/components/switch_as_x/translations/ca.json b/homeassistant/components/switch_as_x/translations/ca.json new file mode 100644 index 00000000000..07d6288d33e --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitat d'interruptor", + "target_domain": "Tipus" + }, + "title": "Converteix un interruptor en \u2026" + } + } + }, + "title": "Interruptor com a X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/el.json b/homeassistant/components/switch_as_x/translations/el.json new file mode 100644 index 00000000000..1fb546c13b7 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/el.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7", + "target_domain": "\u03a4\u03cd\u03c0\u03bf\u03c2" + }, + "title": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03ad\u03bd\u03b1 ..." + } + } + }, + "title": "Switch as X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/en.json b/homeassistant/components/switch_as_x/translations/en.json new file mode 100644 index 00000000000..5c6fb30b9f2 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch entity", + "target_domain": "Type" + }, + "title": "Make a switch a ..." + } + } + }, + "title": "Switch as X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/et.json b/homeassistant/components/switch_as_x/translations/et.json new file mode 100644 index 00000000000..9e18ddced09 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/et.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "L\u00fcliti olem", + "target_domain": "T\u00fc\u00fcp" + }, + "title": "Tee l\u00fcliti ..." + } + } + }, + "title": "L\u00fclita kui X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/hu.json b/homeassistant/components/switch_as_x/translations/hu.json new file mode 100644 index 00000000000..b3ea0af39fd --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Kapcsol\u00f3 entit\u00e1s", + "target_domain": "T\u00edpus" + }, + "title": "Kapcsol\u00f3 mint..." + } + } + }, + "title": "Kapcsol\u00f3 mint X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/id.json b/homeassistant/components/switch_as_x/translations/id.json new file mode 100644 index 00000000000..1a5cce08f01 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entitas saklar", + "target_domain": "Jenis" + }, + "title": "Jadikan saklar sebagai\u2026" + } + } + }, + "title": "Saklar sebagai X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/ja.json b/homeassistant/components/switch_as_x/translations/ja.json new file mode 100644 index 00000000000..44ceaecdd75 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "target_domain": "\u30bf\u30a4\u30d7" + }, + "title": "\u30b9\u30a4\u30c3\u30c1\u3092..." + } + } + }, + "title": "X\u3068\u3057\u3066\u5207\u308a\u66ff\u3048\u308b" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/nl.json b/homeassistant/components/switch_as_x/translations/nl.json new file mode 100644 index 00000000000..d434790fc6b --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "target_domain": "Type" + }, + "title": "Schakel een..." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pl.json b/homeassistant/components/switch_as_x/translations/pl.json new file mode 100644 index 00000000000..c3f9af2601d --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Encja prze\u0142\u0105cznika", + "target_domain": "Rodzaj" + }, + "title": "Zmie\u0144 prze\u0142\u0105cznik na ..." + } + } + }, + "title": "Prze\u0142\u0105cznik jako \"X\"" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pt-BR.json b/homeassistant/components/switch_as_x/translations/pt-BR.json new file mode 100644 index 00000000000..e8ccabe7841 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entidade de interruptor", + "target_domain": "Tipo" + }, + "title": "Fa\u00e7a um interruptor um ..." + } + } + }, + "title": "Switch as X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/ru.json b/homeassistant/components/switch_as_x/translations/ru.json new file mode 100644 index 00000000000..b4136768f03 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c", + "target_domain": "\u0422\u0438\u043f" + }, + "title": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043a\u0430\u043a \u2026" + } + } + }, + "title": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043a\u0430\u043a \u2026" +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/fr.json b/homeassistant/components/switcher_kis/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/switcher_kis/translations/fr.json +++ b/homeassistant/components/switcher_kis/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index 9dd1a8cd3f8..63416018e78 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "unknown": "Erreur inattendue", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, diff --git a/homeassistant/components/tile/translations/fr.json b/homeassistant/components/tile/translations/fr.json index c71349619b9..a700249d3f7 100644 --- a/homeassistant/components/tile/translations/fr.json +++ b/homeassistant/components/tile/translations/fr.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Configurer Tile" } diff --git a/homeassistant/components/timer/translations/fr.json b/homeassistant/components/timer/translations/fr.json index 6f9194f9bbf..fc43202fbfc 100644 --- a/homeassistant/components/timer/translations/fr.json +++ b/homeassistant/components/timer/translations/fr.json @@ -2,7 +2,7 @@ "state": { "_": { "active": "Actif", - "idle": "En veille", + "idle": "Inactif", "paused": "En pause" } } diff --git a/homeassistant/components/tolo/translations/fr.json b/homeassistant/components/tolo/translations/fr.json index 95951419dc4..40b61d012a2 100644 --- a/homeassistant/components/tolo/translations/fr.json +++ b/homeassistant/components/tolo/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index 2b70c85dc8f..3f6fab8d775 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "L'accord s\u00e9lectionn\u00e9 est d\u00e9j\u00e0 configur\u00e9.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_agreements": "Ce compte n'a pas d'affichages Toon.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "step": { diff --git a/homeassistant/components/tractive/translations/fr.json b/homeassistant/components/tractive/translations/fr.json index 7e53cba0d74..ef1a08c15e9 100644 --- a/homeassistant/components/tractive/translations/fr.json +++ b/homeassistant/components/tractive/translations/fr.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } } diff --git a/homeassistant/components/tuya/translations/select.fr.json b/homeassistant/components/tuya/translations/select.fr.json index ab67be514bc..31199b930bf 100644 --- a/homeassistant/components/tuya/translations/select.fr.json +++ b/homeassistant/components/tuya/translations/select.fr.json @@ -7,8 +7,8 @@ }, "tuya__basic_nightvision": { "0": "automatique", - "1": "Inactif", - "2": "Actif" + "1": "D\u00e9sactiv\u00e9", + "2": "Activ\u00e9" }, "tuya__countdown": { "1h": "1 heure", @@ -76,7 +76,7 @@ "led": "LED" }, "tuya__light_mode": { - "none": "Inactif", + "none": "D\u00e9sactiv\u00e9", "pos": "Indiquer l'emplacement du commutateur", "relay": "Indiquer l\u2019\u00e9tat marche/arr\u00eat de l\u2019interrupteur" }, @@ -92,10 +92,10 @@ "tuya__relay_status": { "last": "Se souvenir du dernier \u00e9tat", "memory": "Se souvenir du dernier \u00e9tat", - "off": "Inactif", - "on": "Actif", - "power_off": "Inactif", - "power_on": "Actif" + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9", + "power_off": "D\u00e9sactiv\u00e9", + "power_on": "Activ\u00e9" }, "tuya__vacuum_cistern": { "closed": "Ferm\u00e9", diff --git a/homeassistant/components/uptimerobot/translations/fr.json b/homeassistant/components/uptimerobot/translations/fr.json index 6d20816632f..85393f5dc30 100644 --- a/homeassistant/components/uptimerobot/translations/fr.json +++ b/homeassistant/components/uptimerobot/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 API invalide", + "invalid_api_key": "Cl\u00e9 d'API invalide", "reauth_failed_matching_account": "La cl\u00e9 API que vous avez fournie ne correspond pas \u00e0 l\u2019ID de compte pour la configuration existante.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/vacuum/translations/fr.json b/homeassistant/components/vacuum/translations/fr.json index 7bd851a3a8f..d520f1b4291 100644 --- a/homeassistant/components/vacuum/translations/fr.json +++ b/homeassistant/components/vacuum/translations/fr.json @@ -18,9 +18,9 @@ "cleaning": "Nettoyage", "docked": "Sur la base", "error": "Erreur", - "idle": "En veille", - "off": "Inactif", - "on": "Actif", + "idle": "Inactif", + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9", "paused": "En pause", "returning": "Retourne \u00e0 la base" } diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json index 4909fec8894..f0272878190 100644 --- a/homeassistant/components/verisure/translations/fr.json +++ b/homeassistant/components/verisure/translations/fr.json @@ -18,14 +18,14 @@ "reauth_confirm": { "data": { "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.", - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } }, "user": { "data": { "description": "Connectez-vous avec votre compte Verisure My Pages.", - "email": "Email", + "email": "Courriel", "password": "Mot de passe" } } diff --git a/homeassistant/components/vesync/translations/fr.json b/homeassistant/components/vesync/translations/fr.json index 80bb38b615d..00128122217 100644 --- a/homeassistant/components/vesync/translations/fr.json +++ b/homeassistant/components/vesync/translations/fr.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Email" + "username": "Courriel" }, "title": "Entrez vos identifiants" } diff --git a/homeassistant/components/water_heater/translations/fr.json b/homeassistant/components/water_heater/translations/fr.json index 84c9b730b22..689647e02ba 100644 --- a/homeassistant/components/water_heater/translations/fr.json +++ b/homeassistant/components/water_heater/translations/fr.json @@ -12,7 +12,7 @@ "gas": "Gaz", "heat_pump": "Pompe \u00e0 chaleur", "high_demand": "Demande \u00e9lev\u00e9e", - "off": "Inactif", + "off": "D\u00e9sactiv\u00e9", "performance": "Performance" } } diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index a506d491a74..f59b8e2e714 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Configuration mise \u00e0 jour pour le profil.", - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." diff --git a/homeassistant/components/wiz/translations/fr.json b/homeassistant/components/wiz/translations/fr.json index e6123a1d715..82a29f54bfc 100644 --- a/homeassistant/components/wiz/translations/fr.json +++ b/homeassistant/components/wiz/translations/fr.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({host})", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "discovery_confirm": { "description": "Voulez-vous configurer {name} ({host}) ?" diff --git a/homeassistant/components/wled/translations/select.fr.json b/homeassistant/components/wled/translations/select.fr.json index 47a1a07989b..972cc1f7fae 100644 --- a/homeassistant/components/wled/translations/select.fr.json +++ b/homeassistant/components/wled/translations/select.fr.json @@ -1,8 +1,8 @@ { "state": { "wled__live_override": { - "0": "Inactif", - "1": "Actif", + "0": "D\u00e9sactiv\u00e9", + "1": "Activ\u00e9", "2": "Jusqu'au red\u00e9marrage de l'appareil" } } diff --git a/homeassistant/components/xbox/translations/fr.json b/homeassistant/components/xbox/translations/fr.json index a0dc75cc214..5b37704cec7 100644 --- a/homeassistant/components/xbox/translations/fr.json +++ b/homeassistant/components/xbox/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, diff --git a/homeassistant/components/yamaha_musiccast/translations/fr.json b/homeassistant/components/yamaha_musiccast/translations/fr.json index 14cbec9e877..0a8671dc2aa 100644 --- a/homeassistant/components/yamaha_musiccast/translations/fr.json +++ b/homeassistant/components/yamaha_musiccast/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/zerproc/translations/fr.json b/homeassistant/components/zerproc/translations/fr.json index e9ae4e0b644..3d447c0cbb0 100644 --- a/homeassistant/components/zerproc/translations/fr.json +++ b/homeassistant/components/zerproc/translations/fr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous commencer la configuration ?" + "description": "Voulez-vous commencer la configuration\u00a0?" } } } From 8948bada58ef185d084e96b3edd3615f39af6a14 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 11 Mar 2022 09:46:32 +0100 Subject: [PATCH 0356/1054] Add switch_as_x entity to wrapped switch's device (#67961) --- .../components/switch_as_x/__init__.py | 50 ++++++++- homeassistant/components/switch_as_x/light.py | 16 ++- tests/components/switch_as_x/test_init.py | 101 +++++++++++++++++- tests/components/switch_as_x/test_light.py | 34 +++++- 4 files changed, 193 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 65b95c59c6d..2c647b1e953 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import Event, HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event from .light import LightSwitch @@ -20,9 +20,31 @@ DOMAIN = "switch_as_x" _LOGGER = logging.getLogger(__name__) +@callback +def async_add_to_device( + hass: HomeAssistant, entry: ConfigEntry, entity_id: str +) -> str | None: + """Add our config entry to the tracked entity's device.""" + registry = er.async_get(hass) + device_registry = dr.async_get(hass) + device_id = None + + if ( + not (wrapped_switch := registry.async_get(entity_id)) + or not (device_id := wrapped_switch.device_id) + or not (device_registry.async_get(device_id)) + ): + return device_id + + device_registry.async_update_device(device_id, add_config_entry_id=entry.entry_id) + + return device_id + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" registry = er.async_get(hass) + device_registry = dr.async_get(hass) try: entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) except vol.Invalid: @@ -39,11 +61,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if data["action"] == "remove": await hass.config_entries.async_remove(entry.entry_id) - if data["action"] != "update" or "entity_id" not in data["changes"]: + if data["action"] != "update": return - # Entity_id changed, reload the config entry - await hass.config_entries.async_reload(entry.entry_id) + if "entity_id" in data["changes"]: + # Entity_id changed, reload the config entry + await hass.config_entries.async_reload(entry.entry_id) + + if device_id and "device_id" in data["changes"]: + # If the tracked switch is no longer in the device, remove our config entry + # from the device + if ( + not (entity_entry := registry.async_get(data["entity_id"])) + or not device_registry.async_get(device_id) + or entity_entry.device_id == device_id + ): + # No need to do any cleanup + return + + device_registry.async_update_device( + device_id, remove_config_entry_id=entry.entry_id + ) entry.async_on_unload( async_track_entity_registry_updated_event( @@ -51,6 +89,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) + device_id = async_add_to_device(hass, entry, entity_id) + hass.config_entries.async_setup_platforms(entry, (entry.options["target_domain"],)) return True diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py index 8dd863029da..e6fc334caef 100644 --- a/homeassistant/components/switch_as_x/light.py +++ b/homeassistant/components/switch_as_x/light.py @@ -31,6 +31,8 @@ async def async_setup_entry( entity_id = er.async_validate_entity_id( registry, config_entry.options[CONF_ENTITY_ID] ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None async_add_entities( [ @@ -38,6 +40,7 @@ async def async_setup_entry( config_entry.title, entity_id, config_entry.entry_id, + device_id, ) ] ) @@ -50,8 +53,15 @@ class LightSwitch(LightEntity): _attr_should_poll = False _attr_supported_color_modes = {COLOR_MODE_ONOFF} - def __init__(self, name: str, switch_entity_id: str, unique_id: str | None) -> None: + def __init__( + self, + name: str, + switch_entity_id: str, + unique_id: str | None, + device_id: str | None = None, + ) -> None: """Initialize Light Switch.""" + self._device_id = device_id self._attr_name = name self._attr_unique_id = unique_id self._switch_entity_id = switch_entity_id @@ -100,3 +110,7 @@ class LightSwitch(LightEntity): # Call once on adding async_state_changed_listener() + + # Add this entity to the wrapped switch's device + registry = er.async_get(self.hass) + registry.async_update_entity(self.entity_id, device_id=self._device_id) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index a8875def0ad..8eafc417c04 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -5,7 +5,7 @@ import pytest from homeassistant.components.switch_as_x import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -82,3 +82,102 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain): assert hass.states.get(f"{target_domain}.abc") is None assert registry.async_get(f"{target_domain}.abc") is None assert len(hass.config_entries.async_entries("switch_as_x")) == 0 + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_device_registry_config_entry_1(hass: HomeAssistant, target_domain): + """Test we add our config entry to the tracked switch's device.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + switch_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=device_entry.id, + ) + # Add another config entry to the same device + device_registry.async_update_device( + device_entry.id, add_config_entry_id=MockConfigEntry().entry_id + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": switch_entity_entry.id, "target_domain": target_domain}, + title="ABC", + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry.device_id == switch_entity_entry.device_id + + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id in device_entry.config_entries + + # Remove the wrapped switch's config entry from the device + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=switch_config_entry.entry_id + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + # Check that the switch_as_x config entry is removed from the device + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_device_registry_config_entry_2(hass: HomeAssistant, target_domain): + """Test we add our config entry to the tracked switch's device.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + switch_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", + "test", + "unique", + config_entry=switch_config_entry, + device_id=device_entry.id, + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": switch_entity_entry.id, "target_domain": target_domain}, + title="ABC", + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry.device_id == switch_entity_entry.device_id + + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id in device_entry.config_entries + + # Remove the wrapped switch from the device + entity_registry.async_update_entity(switch_entity_entry.entity_id, device_id=None) + await hass.async_block_till_done() + # Check that the switch_as_x config entry is removed from the device + device_entry = device_registry.async_get(device_entry.id) + assert switch_as_x_config_entry.entry_id not in device_entry.config_entries diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 9a480776510..13ae058f8d9 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -8,7 +8,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.switch_as_x import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -153,3 +153,35 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain): await hass.async_block_till_done() assert hass.states.get(f"{target_domain}.abc") + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_device(hass: HomeAssistant, target_domain): + """Test the entity is added to the wrapped entity's device.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + test_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=test_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", "test", "unique", device_id=device_entry.id + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": switch_entity_entry.id, "target_domain": target_domain}, + title="ABC", + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry.device_id == switch_entity_entry.device_id From 49b642a6ba74a0c9392828a90566b19734d0af52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Mar 2022 07:50:44 -0800 Subject: [PATCH 0357/1054] Log device IP sending local msg (#67987) --- homeassistant/components/google_assistant/helpers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index b1096d44d74..fd6aecd5d42 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -331,7 +331,12 @@ class AbstractConfig(ABC): payload = await request.json() if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload)) + _LOGGER.debug( + "Received local message from %s (JS %s):\n%s\n", + request.remote, + request.headers.get("HA-Cloud-Version", "unknown"), + pprint.pformat(payload), + ) if not self.enabled: return json_response(smart_home.turned_off_response(payload)) From bc2aaedcec7a613fc2033fe571a69d449146ca25 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 11 Mar 2022 19:59:45 +0300 Subject: [PATCH 0358/1054] Add DeviceClass TV to LG Netcast (#67999) --- homeassistant/components/lg_netcast/media_player.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 5faf6941aeb..a76d74481d9 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -8,7 +8,11 @@ from requests import RequestException import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, + MediaPlayerDeviceClass, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -91,6 +95,8 @@ def setup_platform( class LgTVDevice(MediaPlayerEntity): """Representation of a LG TV.""" + _attr_device_class = MediaPlayerDeviceClass.TV + def __init__(self, client, name, on_action_script): """Initialize the LG TV device.""" self._client = client From 306498378cae1acc4df00b6803ec84f392b534ee Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 11 Mar 2022 18:14:40 +0100 Subject: [PATCH 0359/1054] Coverage 100% of Modbus climate (#67396) * Coverage 100% of climate.py * Allow 100% test. --- .coveragerc | 1 - homeassistant/components/modbus/__init__.py | 2 + homeassistant/components/modbus/climate.py | 23 ++--- tests/components/modbus/test_climate.py | 93 ++++++++++++++++++++- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/.coveragerc b/.coveragerc index b892dd1f25d..1f1efb96e7b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -706,7 +706,6 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py homeassistant/components/mochad/* - homeassistant/components/modbus/climate.py homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/moehlenhoff_alpha2/__init__.py diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index eb55066390b..41a1e37425e 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -336,6 +336,8 @@ def get_hub(hass: HomeAssistant, name: str) -> ModbusHub: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Modbus component.""" + if DOMAIN not in config: + return True return await async_modbus_setup( hass, config, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 0ecfc7b43b6..7db416cfef3 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -28,7 +28,6 @@ from . import get_hub from .base_platform import BaseStructPlatform from .const import ( CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_WRITE_REGISTER, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, CONF_MAX_TEMP, @@ -103,8 +102,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if ATTR_TEMPERATURE not in kwargs: - return target_temperature = ( float(kwargs[ATTR_TEMPERATURE]) - self._offset ) / self._scale @@ -124,20 +121,12 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): ] registers = self._swap_registers(raw_regs) - if isinstance(registers, list): - result = await self._hub.async_pymodbus_call( - self._slave, - self._target_temperature_register, - [int(float(i)) for i in registers], - CALL_TYPE_WRITE_REGISTERS, - ) - else: - result = await self._hub.async_pymodbus_call( - self._slave, - self._target_temperature_register, - target_temperature, - CALL_TYPE_WRITE_REGISTER, - ) + result = await self._hub.async_pymodbus_call( + self._slave, + self._target_temperature_register, + [int(float(i)) for i in registers], + CALL_TYPE_WRITE_REGISTERS, + ) self._attr_available = result is not None await self.async_update() diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index e453e3d4d44..54888e069ed 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -8,6 +8,7 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_LAZY_ERROR, CONF_TARGET_TEMP, + MODBUS_DOMAIN, DataType, ) from homeassistant.const import ( @@ -16,10 +17,12 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component -from .conftest import TEST_ENTITY_NAME, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") @@ -222,3 +225,91 @@ async def test_restore_state_climate(hass, mock_test_state, mock_modbus): state = hass.states.get(ENTITY_ID) assert state.state == HVAC_MODE_AUTO assert state.attributes[ATTR_TEMPERATURE] == 37 + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_LAZY_ERROR: 1, + } + ], + }, + ], +) +@pytest.mark.parametrize( + "register_words,do_exception,start_expect,end_expect", + [ + ( + [0x8000], + True, + "17", + STATE_UNAVAILABLE, + ), + ], +) +async def test_lazy_error_climate(hass, mock_do_cycle, start_expect, end_expect): + """Run test for sensor.""" + hass.states.async_set(ENTITY_ID, 17) + await hass.async_block_till_done() + now = mock_do_cycle + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == end_expect + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + } + ], + }, + ], +) +@pytest.mark.parametrize( + "config_addon,register_words", + [ + ( + { + CONF_DATA_TYPE: DataType.INT16, + }, + [7, 9], + ), + ( + { + CONF_DATA_TYPE: DataType.INT32, + }, + [7], + ), + ], +) +async def test_wrong_unpack_climate(hass, mock_do_cycle): + """Run test for sensor.""" + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE + + +async def test_no_discovery_info(hass, caplog): + """Test setup without discovery info.""" + assert CLIMATE_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + CLIMATE_DOMAIN, + {CLIMATE_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert CLIMATE_DOMAIN in hass.config.components From 4de59b065a7ed4a5eb35275a9980c0fb6c021b6c Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 11 Mar 2022 18:24:08 +0100 Subject: [PATCH 0360/1054] Bump pysabnzbd to 1.1.1 (#67971) --- homeassistant/components/sabnzbd/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 08fb1388b38..f6cbd958206 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -2,7 +2,7 @@ "domain": "sabnzbd", "name": "SABnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd", - "requirements": ["pysabnzbd==1.1.0"], + "requirements": ["pysabnzbd==1.1.1"], "dependencies": ["configurator"], "after_dependencies": ["discovery"], "codeowners": [], diff --git a/requirements_all.txt b/requirements_all.txt index 376b70a7ae0..a73ecbdbac7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1764,7 +1764,7 @@ pyrituals==0.0.6 pyruckus==0.12 # homeassistant.components.sabnzbd -pysabnzbd==1.1.0 +pysabnzbd==1.1.1 # homeassistant.components.saj pysaj==0.0.16 From d2e5c85429a8a258a4aeb81c029535e4dec17851 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 11 Mar 2022 18:58:18 +0100 Subject: [PATCH 0361/1054] Remove pragma from discover check. (#68002) --- homeassistant/components/modbus/binary_sensor.py | 2 +- homeassistant/components/modbus/cover.py | 2 +- homeassistant/components/modbus/fan.py | 2 +- homeassistant/components/modbus/light.py | 2 +- homeassistant/components/modbus/sensor.py | 2 +- homeassistant/components/modbus/switch.py | 2 +- tests/components/modbus/test_binary_sensor.py | 14 ++++++++++++++ tests/components/modbus/test_climate.py | 2 +- tests/components/modbus/test_cover.py | 14 ++++++++++++++ tests/components/modbus/test_fan.py | 13 +++++++++++++ tests/components/modbus/test_light.py | 13 +++++++++++++ tests/components/modbus/test_sensor.py | 14 ++++++++++++++ tests/components/modbus/test_switch.py | 13 +++++++++++++ 13 files changed, 88 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 50281bd2b29..c432c102492 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -39,7 +39,7 @@ async def async_setup_platform( ) -> None: """Set up the Modbus binary sensors.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return sensors: list[ModbusBinarySensor | SlaveSensor] = [] diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 1a9a7a82e9c..a01f732ef13 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -45,7 +45,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Read configuration and create Modbus cover.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return covers = [] diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 6e2bf101de2..a986b243c1b 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -24,7 +24,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Read configuration and create Modbus fans.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return fans = [] diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index cc5936050e8..2313dd9bacb 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -23,7 +23,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Read configuration and create Modbus lights.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return lights = [] diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index d4f3d1f28b6..7e9295fdb14 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -34,7 +34,7 @@ async def async_setup_platform( ) -> None: """Set up the Modbus sensors.""" - if discovery_info is None: # pragma: no cover + if discovery_info is None: return sensors: list[ModbusRegisterSensor | SlaveSensor] = [] diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 5844daf648e..beb84096006 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -25,7 +25,7 @@ async def async_setup_platform( """Read configuration and create Modbus switches.""" switches = [] - if discovery_info is None: # pragma: no cover + if discovery_info is None: return for entry in discovery_info[CONF_SWITCHES]: diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index e307d1b6149..bca63321597 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.modbus.const import ( CONF_INPUT_TYPE, CONF_LAZY_ERROR, CONF_SLAVE_COUNT, + MODBUS_DOMAIN, ) from homeassistant.const import ( CONF_ADDRESS, @@ -22,6 +23,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -377,3 +379,15 @@ async def test_slave_binary_sensor(hass, expected, slaves, mock_do_cycle): for i in range(len(slaves)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") assert hass.states.get(entity_id).state == slaves[i] + + +async def test_no_discovery_info_binary_sensor(hass, caplog): + """Test setup without discovery info.""" + assert SENSOR_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert SENSOR_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 54888e069ed..a2c83a60640 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -303,7 +303,7 @@ async def test_wrong_unpack_climate(hass, mock_do_cycle): assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE -async def test_no_discovery_info(hass, caplog): +async def test_no_discovery_info_climate(hass, caplog): """Test setup without discovery info.""" assert CLIMATE_DOMAIN not in hass.config.components assert await async_setup_component( diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 6797dc8713c..3e545f1c1ab 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -15,6 +15,7 @@ from homeassistant.components.modbus.const import ( CONF_STATE_OPENING, CONF_STATUS_REGISTER, CONF_STATUS_REGISTER_TYPE, + MODBUS_DOMAIN, ) from homeassistant.const import ( CONF_ADDRESS, @@ -29,6 +30,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -307,3 +309,15 @@ async def test_service_cover_move(hass, mock_modbus, mock_ha): "cover", "close_cover", {"entity_id": ENTITY_ID2}, blocking=True ) assert hass.states.get(ENTITY_ID2).state == STATE_UNAVAILABLE + + +async def test_no_discovery_info_cover(hass, caplog): + """Test setup without discovery info.""" + assert COVER_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + COVER_DOMAIN, + {COVER_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert COVER_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index 9b0564504d9..3baa23b2791 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult @@ -309,3 +310,15 @@ async def test_service_fan_update(hass, mock_modbus, mock_ha): "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_ON + + +async def test_no_discovery_info_fan(hass, caplog): + """Test setup without discovery info.""" + assert FAN_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + FAN_DOMAIN, + {FAN_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert FAN_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index f98f1105fa0..7ef13c0c712 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult @@ -309,3 +310,15 @@ async def test_service_light_update(hass, mock_modbus, mock_ha): "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == STATE_ON + + +async def test_no_discovery_info_light(hass, caplog): + """Test setup without discovery info.""" + assert LIGHT_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert LIGHT_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4e4e2e284cf..b1432876a97 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.modbus.const import ( CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + MODBUS_DOMAIN, DataType, ) from homeassistant.components.sensor import ( @@ -36,6 +37,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -820,3 +822,15 @@ async def test_service_sensor_update(hass, mock_modbus, mock_ha): "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True ) assert hass.states.get(ENTITY_ID).state == "32" + + +async def test_no_discovery_info_sensor(hass, caplog): + """Test setup without discovery info.""" + assert SENSOR_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert SENSOR_DOMAIN in hass.config.components diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 006e7ee8d15..4d7a48d120f 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -34,6 +34,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle @@ -395,3 +396,15 @@ async def test_delay_switch(hass, mock_modbus): async_fire_time_changed(hass, now) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ON + + +async def test_no_discovery_info_switch(hass, caplog): + """Test setup without discovery info.""" + assert SWITCH_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + {SWITCH_DOMAIN: {"platform": MODBUS_DOMAIN}}, + ) + await hass.async_block_till_done() + assert SWITCH_DOMAIN in hass.config.components From 380f04277e136852e677fb35ce3b9f855c5eb59d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 11 Mar 2022 23:45:29 +0000 Subject: [PATCH 0362/1054] Bump pymediaroom (#68016) --- homeassistant/components/mediaroom/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 63007f88bbb..c49368d2e27 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -2,7 +2,7 @@ "domain": "mediaroom", "name": "Mediaroom", "documentation": "https://www.home-assistant.io/integrations/mediaroom", - "requirements": ["pymediaroom==0.6.4.1"], + "requirements": ["pymediaroom==0.6.5.4"], "codeowners": ["@dgomes"], "iot_class": "local_polling", "loggers": ["pymediaroom"] diff --git a/requirements_all.txt b/requirements_all.txt index a73ecbdbac7..744a1973215 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1621,7 +1621,7 @@ pymata-express==1.19 pymazda==0.3.2 # homeassistant.components.mediaroom -pymediaroom==0.6.4.1 +pymediaroom==0.6.5.4 # homeassistant.components.melcloud pymelcloud==2.5.6 From 41df79837551ff35441ecb605f6920338ae64f72 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 12 Mar 2022 00:57:38 +0100 Subject: [PATCH 0363/1054] Bump mypy to 0.940 (#68007) --- homeassistant/components/co2signal/sensor.py | 2 +- homeassistant/components/energy/data.py | 2 +- homeassistant/components/flux_led/discovery.py | 4 ++-- homeassistant/components/unifiprotect/entity.py | 1 - homeassistant/components/unifiprotect/models.py | 3 +-- homeassistant/helpers/entity_platform.py | 2 +- mypy.ini | 1 + requirements_test.txt | 2 +- script/hassfest/mypy_config.py | 1 + 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index b10cd054ff9..0c637506447 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -98,7 +98,7 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE @property def native_value(self) -> StateType: """Return sensor state.""" - if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[misc] + if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[literal-required] return None return round(value, 2) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d07d3406073..d33f915628d 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -291,7 +291,7 @@ class EnergyManager: "device_consumption", ): if key in update: - data[key] = update[key] # type: ignore[misc] + data[key] = update[key] # type: ignore[literal-required] self.data = data self._store.async_delay_save(lambda: cast(dict, self.data), 60) diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 62b80243c8b..a522d133827 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -102,9 +102,9 @@ def async_populate_data_from_discovery( device.get(discovery_key) is not None and conf_key not in data_updates # Prefer the model num from TCP instead of UDP - and current_data.get(conf_key) != device[discovery_key] # type: ignore[misc] + and current_data.get(conf_key) != device[discovery_key] # type: ignore[literal-required] ): - data_updates[conf_key] = device[discovery_key] # type: ignore[misc] + data_updates[conf_key] = device[discovery_key] # type: ignore[literal-required] @callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 27b74d2b303..045a9c4fd6d 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -44,7 +44,6 @@ def _async_device_entities( for device in data.get_by_types({model_type}): assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock)) for description in descs: - assert isinstance(description, EntityDescription) if description.ufp_required_field: required_field = get_nested_attr(device, description.ufp_required_field) if not required_field: diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index df11c46a5d9..ecc3e4210ff 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -18,7 +18,7 @@ T = TypeVar("T", bound=ProtectDeviceModel) @dataclass -class ProtectRequiredKeysMixin(Generic[T]): +class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): """Mixin for required keys.""" ufp_required_field: str | None = None @@ -54,7 +54,6 @@ class ProtectSetableKeysMixin(ProtectRequiredKeysMixin, Generic[T]): async def ufp_set(self, obj: T, value: Any) -> None: """Set value for UniFi Protect device.""" - assert isinstance(self, EntityDescription) _LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.name) if self.ufp_set_method is not None: await getattr(obj, self.ufp_set_method)(value) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index cc252f82782..eaf0ae6d6bb 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -478,7 +478,7 @@ class EntityPlatform: "via_device", ): if key in device_info: - processed_dev_info[key] = device_info[key] # type: ignore[misc] + processed_dev_info[key] = device_info[key] # type: ignore[literal-required] if "configuration_url" in device_info: if device_info["configuration_url"] is None: diff --git a/mypy.ini b/mypy.ini index 6316ce32abf..8beb6183d01 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,6 +12,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true +enable_error_code = ignore-without-code check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/requirements_test.txt b/requirements_test.txt index 3896de74630..0e78d456e95 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.3.2 freezegun==1.1.0 mock-open==1.4.0 -mypy==0.931 +mypy==0.940 pre-commit==2.17.0 pylint==2.12.2 pipdeptree==2.2.1 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 5df83f1e527..9882c016597 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -247,6 +247,7 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", + "enable_error_code": "ignore-without-code", } # This is basically the list of checks which is enabled for "strict=true". From 80ff497cfc94a0a8813da272d1bbc7374cbf88ae Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 11 Mar 2022 17:57:57 -0600 Subject: [PATCH 0364/1054] Rework available Sonos sources (#67931) --- homeassistant/components/sonos/const.py | 14 +++++++++++ .../components/sonos/media_player.py | 24 +++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 6c4bdd07b31..7b85828377a 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -171,6 +171,20 @@ SOURCE_LINEIN = "Line-in" SOURCE_SPOTIFY_CONNECT = "Spotify Connect" SOURCE_TV = "TV" +MODELS_LINEIN_ONLY = ( + "CONNECT", + "CONNECT:AMP", + "PORT", + "PLAY:5", +) +MODELS_TV_ONLY = ( + "ARC", + "BEAM", + "PLAYBAR", + "PLAYBASE", +) +MODELS_LINEIN_AND_TV = ("AMP",) + AVAILABILITY_CHECK_INTERVAL = datetime.timedelta(minutes=1) AVAILABILITY_TIMEOUT = AVAILABILITY_CHECK_INTERVAL.total_seconds() * 4.5 BATTERY_SCAN_INTERVAL = datetime.timedelta(minutes=15) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 65e8c4111ae..0968f5d024c 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -64,6 +64,9 @@ from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, MEDIA_TYPES_TO_SONOS, + MODELS_LINEIN_AND_TV, + MODELS_LINEIN_ONLY, + MODELS_TV_ONLY, PLAYABLE_MEDIA_TYPES, SONOS_CREATE_MEDIA_PLAYER, SONOS_MEDIA_UPDATED, @@ -474,20 +477,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.add_to_queue(favorite.reference) soco.play_from_queue(0) - @property # type: ignore[misc] + @property def source_list(self) -> list[str]: """List of available input sources.""" - sources = [fav.title for fav in self.speaker.favorites] - - model = self.coordinator.model_name.upper() - if "PLAY:5" in model or "CONNECT" in model: - sources += [SOURCE_LINEIN] - elif "PLAYBAR" in model: - sources += [SOURCE_LINEIN, SOURCE_TV] - elif "BEAM" in model or "PLAYBASE" in model: - sources += [SOURCE_TV] - - return sources + model = self.coordinator.model_name.split()[-1].upper() + if model in MODELS_LINEIN_ONLY: + return [SOURCE_LINEIN] + if model in MODELS_TV_ONLY: + return [SOURCE_TV] + if model in MODELS_LINEIN_AND_TV: + return [SOURCE_LINEIN, SOURCE_TV] + return [] @soco_error(UPNP_ERRORS_TO_IGNORE) def media_play(self) -> None: From dc31f420ede196db13367990ac8214167557b887 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 12 Mar 2022 00:17:47 +0000 Subject: [PATCH 0365/1054] [ci skip] Translation update --- .../components/adax/translations/zh-Hant.json | 4 +- .../airvisual/translations/zh-Hant.json | 2 +- .../components/airzone/translations/fr.json | 3 +- .../airzone/translations/pt-BR.json | 4 +- .../alarmdecoder/translations/zh-Hant.json | 2 +- .../androidtv/translations/zh-Hant.json | 2 +- .../aurora_abb_powerone/translations/fr.json | 2 +- .../aussie_broadband/translations/fr.json | 11 +- .../azure_event_hub/translations/fr.json | 2 +- .../binary_sensor/translations/it.json | 6 +- .../brother/translations/zh-Hant.json | 4 +- .../components/cast/translations/fr.json | 2 +- .../climacell/translations/zh-Hant.json | 2 +- .../components/climate/translations/bg.json | 2 +- .../components/daikin/translations/fr.json | 2 +- .../deconz/translations/zh-Hant.json | 2 +- .../devolo_home_control/translations/fr.json | 6 +- .../devolo_home_network/translations/fr.json | 2 +- .../diagnostics/translations/fr.json | 2 +- .../dialogflow/translations/fr.json | 2 +- .../components/dlna_dmr/translations/fr.json | 8 +- .../components/dlna_dms/translations/fr.json | 8 +- .../components/dnsip/translations/fr.json | 8 +- .../components/dnsip/translations/it.json | 8 +- .../components/dsmr/translations/zh-Hant.json | 4 +- .../components/elmax/translations/fr.json | 2 +- .../components/flux_led/translations/fr.json | 2 +- .../flux_led/translations/zh-Hant.json | 2 +- .../components/fritz/translations/fr.json | 5 +- .../components/geofency/translations/fr.json | 2 +- .../components/goodwe/translations/fr.json | 2 +- .../translations/zh-Hant.json | 2 +- .../components/gpslogger/translations/fr.json | 2 +- .../components/group/translations/fr.json | 111 ++++++++++++++++++ .../components/group/translations/it.json | 65 ++++++++-- .../components/group/translations/ja.json | 1 + .../components/group/translations/nl.json | 57 ++++++++- .../components/group/translations/no.json | 65 ++++++++-- .../components/group/translations/sv.json | 68 +++++++++++ .../group/translations/zh-Hant.json | 69 +++++++++-- .../components/habitica/translations/fr.json | 2 +- .../homeassistant/translations/zh-Hant.json | 2 +- .../components/homekit/translations/fr.json | 2 +- .../translations/select.fr.json | 2 +- .../homematicip_cloud/translations/fr.json | 2 +- .../homewizard/translations/fr.json | 4 +- .../components/hyperion/translations/fr.json | 2 +- .../components/hyperion/translations/it.json | 2 +- .../components/ifttt/translations/fr.json | 2 +- .../insteon/translations/zh-Hant.json | 4 +- .../components/iss/translations/fr.json | 2 +- .../kaleidescape/translations/fr.json | 1 + .../kaleidescape/translations/pt-BR.json | 12 +- .../kaleidescape/translations/sv.json | 5 + .../components/knx/translations/fr.json | 2 +- .../components/knx/translations/zh-Hant.json | 10 +- .../konnected/translations/zh-Hant.json | 4 +- .../components/locative/translations/fr.json | 2 +- .../components/mailgun/translations/fr.json | 2 +- .../components/mill/translations/zh-Hant.json | 4 +- .../components/mjpeg/translations/fr.json | 30 ++++- .../moehlenhoff_alpha2/translations/fr.json | 2 +- .../components/moon/translations/fr.json | 3 +- .../components/moon/translations/pt-BR.json | 2 +- .../components/motioneye/translations/fr.json | 8 +- .../components/mqtt/translations/pt-BR.json | 2 +- .../mysensors/translations/zh-Hant.json | 2 +- .../components/netatmo/translations/fr.json | 8 +- .../components/netatmo/translations/it.json | 8 +- .../components/netgear/translations/fr.json | 2 +- .../components/octoprint/translations/fr.json | 2 +- .../components/octoprint/translations/it.json | 2 +- .../components/oncue/translations/fr.json | 6 +- .../components/onewire/translations/fr.json | 13 +- .../onewire/translations/pt-BR.json | 4 - .../onewire/translations/zh-Hant.json | 2 +- .../components/onvif/translations/fr.json | 2 +- .../components/overkiz/translations/fr.json | 4 +- .../components/owntracks/translations/fr.json | 2 +- .../components/ozw/translations/zh-Hant.json | 2 +- .../components/plaato/translations/fr.json | 2 +- .../plaato/translations/zh-Hant.json | 4 +- .../plugwise/translations/zh-Hant.json | 4 +- .../components/powerwall/translations/fr.json | 2 +- .../pure_energie/translations/fr.json | 8 ++ .../pure_energie/translations/it.json | 4 +- .../components/pvoutput/translations/fr.json | 10 +- .../radio_browser/translations/fr.json | 7 ++ .../recollect_waste/translations/zh-Hant.json | 2 +- .../rfxtrx/translations/zh-Hant.json | 4 +- .../rtsp_to_webrtc/translations/fr.json | 2 +- .../components/season/translations/fr.json | 7 ++ .../season/translations/zh-Hant.json | 2 +- .../components/sense/translations/fr.json | 6 +- .../components/senseme/translations/fr.json | 8 +- .../components/sensibo/translations/fr.json | 4 +- .../sensibo/translations/pt-BR.json | 2 +- .../components/sensor/translations/it.json | 4 +- .../components/sleepiq/translations/fr.json | 12 ++ .../components/solax/translations/fr.json | 2 +- .../components/sonarr/translations/fr.json | 2 +- .../components/steamist/translations/fr.json | 12 +- .../components/steamist/translations/it.json | 6 +- .../components/switch/translations/fr.json | 9 ++ .../switch_as_x/translations/fr.json | 14 +++ .../switch_as_x/translations/it.json | 14 +++ .../switch_as_x/translations/nl.json | 4 +- .../switch_as_x/translations/no.json | 14 +++ .../switch_as_x/translations/sv.json | 14 +++ .../switch_as_x/translations/zh-Hant.json | 14 +++ .../components/switchbot/translations/fr.json | 2 +- .../switchbot/translations/zh-Hant.json | 2 +- .../components/tplink/translations/fr.json | 2 +- .../components/traccar/translations/fr.json | 2 +- .../tuya/translations/select.fr.json | 16 +-- .../components/tuya/translations/zh-Hant.json | 6 +- .../components/twilio/translations/fr.json | 4 +- .../components/unifi/translations/fr.json | 4 +- .../unifiprotect/translations/fr.json | 4 +- .../unifiprotect/translations/it.json | 2 +- .../components/update/translations/fr.json | 3 + .../components/vallox/translations/fr.json | 14 +-- .../velbus/translations/zh-Hant.json | 2 +- .../version/translations/zh-Hant.json | 4 +- .../components/vicare/translations/fr.json | 8 +- .../vicare/translations/zh-Hant.json | 2 +- .../translations/zh-Hant.json | 2 +- .../components/webostv/translations/fr.json | 12 +- .../components/whois/translations/fr.json | 2 +- .../components/wiz/translations/fr.json | 3 +- .../wolflink/translations/sensor.it.json | 2 +- .../xiaomi_aqara/translations/fr.json | 2 +- .../xiaomi_aqara/translations/it.json | 2 +- .../yale_smart_alarm/translations/fr.json | 2 +- .../components/zha/translations/fr.json | 2 +- .../components/zha/translations/zh-Hant.json | 6 +- .../components/zwave_js/translations/fr.json | 2 +- .../zwave_js/translations/zh-Hant.json | 4 +- 138 files changed, 798 insertions(+), 248 deletions(-) create mode 100644 homeassistant/components/kaleidescape/translations/sv.json create mode 100644 homeassistant/components/radio_browser/translations/fr.json create mode 100644 homeassistant/components/switch_as_x/translations/fr.json create mode 100644 homeassistant/components/switch_as_x/translations/it.json create mode 100644 homeassistant/components/switch_as_x/translations/no.json create mode 100644 homeassistant/components/switch_as_x/translations/sv.json create mode 100644 homeassistant/components/switch_as_x/translations/zh-Hant.json create mode 100644 homeassistant/components/update/translations/fr.json diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json index 0ad4bcba854..92018be07da 100644 --- a/homeassistant/components/adax/translations/zh-Hant.json +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -27,11 +27,11 @@ "user": { "data": { "account_id": "\u5e33\u865f ID", - "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u578b", + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u9078\u64c7\u9023\u7dda\u985e\u578b\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" + "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index fed34b3346b..e8779af7f52 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -45,7 +45,7 @@ "title": "\u91cd\u65b0\u8a8d\u8b49 AirVisual" }, "user": { - "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684 AirVisual \u8cc7\u6599\u985e\u578b\u3002", + "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684 AirVisual \u8cc7\u6599\u985e\u5225\u3002", "title": "\u8a2d\u5b9a AirVisual" } } diff --git a/homeassistant/components/airzone/translations/fr.json b/homeassistant/components/airzone/translations/fr.json index 4cf889ff02d..1fdf1e4397b 100644 --- a/homeassistant/components/airzone/translations/fr.json +++ b/homeassistant/components/airzone/translations/fr.json @@ -11,7 +11,8 @@ "data": { "host": "H\u00f4te", "port": "Port" - } + }, + "description": "Configurer l'int\u00e9gration Airzone." } } } diff --git a/homeassistant/components/airzone/translations/pt-BR.json b/homeassistant/components/airzone/translations/pt-BR.json index f063b3461ae..1a8df1fef99 100644 --- a/homeassistant/components/airzone/translations/pt-BR.json +++ b/homeassistant/components/airzone/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "port": "Porta" }, "description": "Configure a integra\u00e7\u00e3o Airzone." diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index d1a96eedd15..19886544dfc 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -57,7 +57,7 @@ "zone_relayaddr": "\u4e2d\u7e7c\u4f4d\u5740", "zone_relaychan": "\u4e2d\u7e7c\u983b\u9053", "zone_rfid": "RF \u5e8f\u5217", - "zone_type": "\u5340\u57df\u985e\u578b" + "zone_type": "\u5340\u57df\u985e\u5225" }, "description": "\u8f38\u5165\u5340\u57df {zone_number} \u8a73\u7d30\u8cc7\u6599\u3002\u6b32\u522a\u9664\u5340\u57df {zone_number}\uff0c\u4fdd\u6301\u5340\u57df\u540d\u7a31\u7a7a\u767d\u3002", "title": "\u8a2d\u5b9a AlarmDecoder" diff --git a/homeassistant/components/androidtv/translations/zh-Hant.json b/homeassistant/components/androidtv/translations/zh-Hant.json index 6f6e6fd8180..9e9d78f0629 100644 --- a/homeassistant/components/androidtv/translations/zh-Hant.json +++ b/homeassistant/components/androidtv/translations/zh-Hant.json @@ -17,7 +17,7 @@ "adb_server_ip": "ADB \u4f3a\u670d\u5668 IP \u4f4d\u5740\uff08\u4fdd\u7559\u7a7a\u767d\u70ba\u4e0d\u4f7f\u7528\uff09", "adb_server_port": "ADB \u4f3a\u670d\u5668\u901a\u8a0a\u57e0", "adbkey": "ADB \u91d1\u9470\u6a94\u6848\u8def\u5f91\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "device_class": "\u88dd\u7f6e\u985e\u578b", + "device_class": "\u88dd\u7f6e\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, diff --git a/homeassistant/components/aurora_abb_powerone/translations/fr.json b/homeassistant/components/aurora_abb_powerone/translations/fr.json index d87822fb7c3..206167a5669 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/fr.json +++ b/homeassistant/components/aurora_abb_powerone/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "no_serial_ports": "Aucun port com trouv\u00e9. Besoin d'un p\u00e9riph\u00e9rique RS485 valide pour communiquer." + "no_serial_ports": "Aucun port com trouv\u00e9. Un p\u00e9riph\u00e9rique RS485 valide est requis pour communiquer." }, "error": { "cannot_connect": "Connexion impossible, veuillez v\u00e9rifier le port s\u00e9rie, l'adresse, la connexion \u00e9lectrique et que l'onduleur est allum\u00e9 (\u00e0 la lumi\u00e8re du jour)", diff --git a/homeassistant/components/aussie_broadband/translations/fr.json b/homeassistant/components/aussie_broadband/translations/fr.json index 518f05e8ac3..13178a5753e 100644 --- a/homeassistant/components/aussie_broadband/translations/fr.json +++ b/homeassistant/components/aussie_broadband/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, @@ -18,6 +18,13 @@ "description": "Mettre \u00e0 jour le mot de passe pour {username}", "title": "R\u00e9-authentifier l'int\u00e9gration" }, + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Mettre \u00e0 jour le mot de passe pour {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "service": { "data": { "services": "Services" @@ -34,7 +41,7 @@ }, "options": { "abort": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/azure_event_hub/translations/fr.json b/homeassistant/components/azure_event_hub/translations/fr.json index 2cfeb2f4d32..9e238c3836d 100644 --- a/homeassistant/components/azure_event_hub/translations/fr.json +++ b/homeassistant/components/azure_event_hub/translations/fr.json @@ -7,7 +7,7 @@ "unknown": "La connexion avec les informations d'identification du fichier configuration.yaml a \u00e9chou\u00e9 avec une erreur inconnue, veuillez supprimer de yaml et utiliser le flux de configuration." }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 5c81e8942e4..6054fd9d9fe 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -56,7 +56,7 @@ "bat_low": "{entity_name} batteria scarica", "co": "{entity_name} ha iniziato a rilevare il monossido di carbonio", "cold": "{entity_name} \u00e8 diventato freddo", - "connected": "{entity_name} connesso", + "connected": "{entity_name} \u00e8 connesso", "gas": "{entity_name} ha iniziato a rilevare il gas", "hot": "{entity_name} \u00e8 diventato caldo", "is_not_tampered": "{entity_name} ha smesso di rilevare manomissioni", @@ -139,7 +139,7 @@ "on": "Rilevato" }, "co": { - "off": "Non Rilevato", + "off": "Non rilevato", "on": "Rilevato" }, "cold": { @@ -228,7 +228,7 @@ }, "vibration": { "off": "Assente", - "on": "Rilevata" + "on": "Rilevato" }, "window": { "off": "Chiusa", diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 88bd481749f..016d83309f2 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -14,13 +14,13 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "type": "\u5370\u8868\u6a5f\u985e\u578b" + "type": "\u5370\u8868\u6a5f\u985e\u5225" }, "description": "\u8a2d\u5b9a Brother \u5370\u8868\u6a5f\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/brother" }, "zeroconf_confirm": { "data": { - "type": "\u5370\u8868\u6a5f\u985e\u578b" + "type": "\u5370\u8868\u6a5f\u985e\u5225" }, "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Brother \u5370\u8868\u6a5f {model} \u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Brother \u5370\u8868\u6a5f" diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index 6cc720e5630..87e7b1609fa 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -36,7 +36,7 @@ "data": { "known_hosts": "H\u00f4tes connus" }, - "description": "H\u00f4tes connus - Une liste de noms d'h\u00f4te ou d'adresses IP s\u00e9par\u00e9s par des virgules des p\u00e9riph\u00e9riques de diffusion, \u00e0 utiliser si la d\u00e9couverte mDNS ne fonctionne pas.", + "description": "H\u00f4tes connus \u2013\u00a0Une liste de noms d'h\u00f4te ou d'adresses IP s\u00e9par\u00e9s par des virgules des appareils de diffusion, \u00e0 utiliser si la d\u00e9couverte mDNS ne fonctionne pas.", "title": "Configuration de Google Cast" } } diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 5ef7396b0e5..68e06219ae7 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -15,7 +15,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u578b\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002" + "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u5225\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002" } } }, diff --git a/homeassistant/components/climate/translations/bg.json b/homeassistant/components/climate/translations/bg.json index 7c7389545eb..6c3eb3b612a 100644 --- a/homeassistant/components/climate/translations/bg.json +++ b/homeassistant/components/climate/translations/bg.json @@ -18,7 +18,7 @@ "_": { "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", "cool": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", - "dry": "\u0421\u0443\u0445", + "dry": "\u0418\u0437\u0441\u0443\u0448\u0430\u0432\u0430\u043d\u0435", "fan_only": "\u0421\u0430\u043c\u043e \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440", "heat": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "heat_cool": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435/\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 3b2dcd8ce27..7a25805caf5 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -5,7 +5,7 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "api_password": "Authentification invalide, utilisez la cl\u00e9 API ou le mot de passe.", + "api_password": "Authentification invalide, utilisez soit la cl\u00e9 d'API soit le mot de passe.", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index 64817192c64..d6d4dfeba45 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -101,7 +101,7 @@ "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44", "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u88dd\u7f6e" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b", + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u5225", "title": "deCONZ \u9078\u9805" } } diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 387edb9598d..3a93f50e8e4 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -13,14 +13,14 @@ "data": { "mydevolo_url": "URL mydevolo", "password": "Mot de passe", - "username": "Courriel / devolo ID" + "username": "Courriel / ID devolo" } }, "zeroconf_confirm": { "data": { - "mydevolo_url": "mydevolo URL", + "mydevolo_url": "URL mydevolo", "password": "Mot de passe", - "username": "Courriel / devolo ID" + "username": "Courriel / ID devolo" } } } diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json index d777ba890b1..311489bf927 100644 --- a/homeassistant/components/devolo_home_network/translations/fr.json +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -17,7 +17,7 @@ "description": "Voulez-vous commencer la configuration\u00a0?" }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter le p\u00e9riph\u00e9rique r\u00e9seau domestique devolo avec le nom d'h\u00f4te ` {host_name} ` \u00e0 Home Assistant\u00a0?", + "description": "Voulez-vous ajouter l'appareil de r\u00e9seau domestique devolo portant le nom d'h\u00f4te \u00ab\u00a0{host_name}\u00a0\u00bb \u00e0 Home Assistant\u00a0?", "title": "Appareil r\u00e9seau domestique devolo d\u00e9couvert" } } diff --git a/homeassistant/components/diagnostics/translations/fr.json b/homeassistant/components/diagnostics/translations/fr.json index f5936aced5b..cfa7ba1e755 100644 --- a/homeassistant/components/diagnostics/translations/fr.json +++ b/homeassistant/components/diagnostics/translations/fr.json @@ -1,3 +1,3 @@ { - "title": "Diagnostics" + "title": "Diagnostiques" } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/fr.json b/homeassistant/components/dialogflow/translations/fr.json index 302c0df7f05..661d86566b0 100644 --- a/homeassistant/components/dialogflow/translations/fr.json +++ b/homeassistant/components/dialogflow/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index 6d9b294d69b..7bcc563f679 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -4,15 +4,15 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "alternative_integration": "L'appareil est mieux pris en charge par une autre int\u00e9gration", "cannot_connect": "\u00c9chec de connexion", - "could_not_connect": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique DLNA", - "discovery_error": "\u00c9chec de la d\u00e9couverte d'un p\u00e9riph\u00e9rique DLNA correspondant", + "could_not_connect": "\u00c9chec de la connexion \u00e0 l'appareil DLNA", + "discovery_error": "\u00c9chec de la d\u00e9couverte d'un appareil DLNA correspondant", "incomplete_config": "Il manque une variable requise dans la configuration", "non_unique_id": "Plusieurs appareils trouv\u00e9s avec le m\u00eame identifiant unique", "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "could_not_connect": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique DLNA", + "could_not_connect": "\u00c9chec de la connexion \u00e0 l'appareil DLNA", "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, "flow_title": "{name}", @@ -36,7 +36,7 @@ "url": "URL" }, "description": "Choisissez un appareil \u00e0 configurer ou laissez vide pour saisir une URL", - "title": "P\u00e9riph\u00e9riques DLNA DMR d\u00e9couverts" + "title": "Appareils DLNA DMR d\u00e9couverts" } } }, diff --git a/homeassistant/components/dlna_dms/translations/fr.json b/homeassistant/components/dlna_dms/translations/fr.json index 3908dd082ea..3c4fe096ea9 100644 --- a/homeassistant/components/dlna_dms/translations/fr.json +++ b/homeassistant/components/dlna_dms/translations/fr.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "bad_ssdp": "Il manque une valeur requise dans les donn\u00e9es SSDP", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_dms": "L'appareil n'est pas un serveur multim\u00e9dia pris en charge" }, "flow_title": "{name}", "step": { @@ -13,7 +16,8 @@ "data": { "host": "H\u00f4te" }, - "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer", + "title": "Appareils DNLA DMA d\u00e9couverts" } } } diff --git a/homeassistant/components/dnsip/translations/fr.json b/homeassistant/components/dnsip/translations/fr.json index ae6da0296c2..cbecc060bd5 100644 --- a/homeassistant/components/dnsip/translations/fr.json +++ b/homeassistant/components/dnsip/translations/fr.json @@ -7,8 +7,8 @@ "user": { "data": { "hostname": "Le nom d'h\u00f4te pour lequel la requ\u00eate DNS doit \u00eatre effectu\u00e9e.", - "resolver": "R\u00e9solveur pour la recherche IPV4", - "resolver_ipv6": "R\u00e9solveur pour la recherche IPV6" + "resolver": "R\u00e9solveur pour la recherche IPv4", + "resolver_ipv6": "R\u00e9solveur pour la recherche IPv6" } } } @@ -20,8 +20,8 @@ "step": { "init": { "data": { - "resolver": "R\u00e9solveur pour la recherche IPV4", - "resolver_ipv6": "R\u00e9solveur pour la recherche IPV6" + "resolver": "R\u00e9solveur pour la recherche IPv4", + "resolver_ipv6": "R\u00e9solveur pour la recherche IPv6" } } } diff --git a/homeassistant/components/dnsip/translations/it.json b/homeassistant/components/dnsip/translations/it.json index 2ed18baa178..e0ddeb92a8b 100644 --- a/homeassistant/components/dnsip/translations/it.json +++ b/homeassistant/components/dnsip/translations/it.json @@ -7,8 +7,8 @@ "user": { "data": { "hostname": "Il nome host per il quale eseguire la query DNS", - "resolver": "Risolutore per la ricerca IPV4", - "resolver_ipv6": "Risolutore per la ricerca IPV6" + "resolver": "Risolutore per la ricerca IPv4", + "resolver_ipv6": "Risolutore per la ricerca IPv6" } } } @@ -20,8 +20,8 @@ "step": { "init": { "data": { - "resolver": "Risolutore per ricerca IPV4", - "resolver_ipv6": "Risolutore per ricerca IPV6" + "resolver": "Risolutore per ricerca IPv4", + "resolver_ipv6": "Risolutore per ricerca IPv6" } } } diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index 9d95685a87f..427011d5585 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -34,9 +34,9 @@ }, "user": { "data": { - "type": "\u9023\u7dda\u985e\u578b" + "type": "\u9023\u7dda\u985e\u5225" }, - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" } } }, diff --git a/homeassistant/components/elmax/translations/fr.json b/homeassistant/components/elmax/translations/fr.json index c6c68b9df3a..0f19428b5bf 100644 --- a/homeassistant/components/elmax/translations/fr.json +++ b/homeassistant/components/elmax/translations/fr.json @@ -9,7 +9,7 @@ "invalid_pin": "Le code PIN fourni n\u2019est pas valide", "network_error": "Une erreur r\u00e9seau s'est produite", "no_panel_online": "Aucun panneau de contr\u00f4le Elmax en ligne n'a \u00e9t\u00e9 trouv\u00e9.", - "unknown": "Erreur inconnue", + "unknown": "Erreur inattendue", "unknown_error": "une erreur inattendue est apparue" }, "step": { diff --git a/homeassistant/components/flux_led/translations/fr.json b/homeassistant/components/flux_led/translations/fr.json index c2177a0cb1f..baea6899999 100644 --- a/homeassistant/components/flux_led/translations/fr.json +++ b/homeassistant/components/flux_led/translations/fr.json @@ -17,7 +17,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } }, diff --git a/homeassistant/components/flux_led/translations/zh-Hant.json b/homeassistant/components/flux_led/translations/zh-Hant.json index 789a5aad6e8..e1c5ded8c75 100644 --- a/homeassistant/components/flux_led/translations/zh-Hant.json +++ b/homeassistant/components/flux_led/translations/zh-Hant.json @@ -27,7 +27,7 @@ "data": { "custom_effect_colors": "\u81ea\u8a02\u7279\u6548\uff1a1 \u5230 16 \u7a2e [R,G,B] \u984f\u8272\u3002\u4f8b\u5982\uff1a[255,0,255]\u3001[60,128,0]", "custom_effect_speed_pct": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u5207\u63db\u7684\u901f\u5ea6\u767e\u5206\u6bd4\u3002", - "custom_effect_transition": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u9593\u7684\u8f49\u63db\u985e\u578b\u3002", + "custom_effect_transition": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u9593\u7684\u8f49\u63db\u985e\u5225\u3002", "mode": "\u9078\u64c7\u4eae\u5ea6\u6a21\u5f0f\u3002" } } diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index 7c58c167ae6..44c3c2159b5 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -10,7 +10,8 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "upnp_not_configured": "Param\u00e8tres UPnP manquants sur l'appareil." }, "flow_title": "{name}", "step": { @@ -57,7 +58,7 @@ "init": { "data": { "consider_home": "Secondes pour consid\u00e9rer un appareil \u00e0 la 'maison'", - "old_discovery": "Autoriser l'ancienne m\u00e9thode de d\u00e9couverte" + "old_discovery": "Activer l'ancienne m\u00e9thode de d\u00e9couverte" } } } diff --git a/homeassistant/components/geofency/translations/fr.json b/homeassistant/components/geofency/translations/fr.json index db163efeac7..84da031d50d 100644 --- a/homeassistant/components/geofency/translations/fr.json +++ b/homeassistant/components/geofency/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/goodwe/translations/fr.json b/homeassistant/components/goodwe/translations/fr.json index 544d6cfda68..7f7238bd960 100644 --- a/homeassistant/components/goodwe/translations/fr.json +++ b/homeassistant/components/goodwe/translations/fr.json @@ -5,7 +5,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours" }, "error": { - "connection_error": "Impossible de se connecter" + "connection_error": "\u00c9chec de connexion" }, "step": { "user": { diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json index 929810a8564..f8ac86306a2 100644 --- a/homeassistant/components/google_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -26,7 +26,7 @@ "language": "\u8a9e\u8a00", "mode": "\u65c5\u884c\u6a21\u5f0f", "time": "\u6642\u9593", - "time_type": "\u6642\u9593\u985e\u578b", + "time_type": "\u6642\u9593\u985e\u5225", "transit_mode": "\u79fb\u52d5\u6a21\u5f0f", "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda", "units": "\u55ae\u4f4d" diff --git a/homeassistant/components/gpslogger/translations/fr.json b/homeassistant/components/gpslogger/translations/fr.json index a69191d05c6..4e207dddfcd 100644 --- a/homeassistant/components/gpslogger/translations/fr.json +++ b/homeassistant/components/gpslogger/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/group/translations/fr.json b/homeassistant/components/group/translations/fr.json index 97d2ed91941..8e8b5ee4233 100644 --- a/homeassistant/components/group/translations/fr.json +++ b/homeassistant/components/group/translations/fr.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres", + "name": "Nom" + }, + "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9.", + "title": "Nouveau groupe" + }, + "cover": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "cover_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "fan": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "fan_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "init": { + "data": { + "group_type": "Type de groupe" + }, + "description": "S\u00e9lectionnez le type de groupe" + }, + "light": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "light_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "media_player": { + "data": { + "entities": "Membres", + "name": "Nom", + "title": "Nouveau groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "media_player_options": { + "data": { + "entities": "Membres du groupe" + }, + "description": "S\u00e9lectionnez les options du groupe" + }, + "user": { + "data": { + "group_type": "Type de groupe" + }, + "title": "Nouveau groupe" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres" + } + }, + "cover_options": { + "data": { + "entities": "Membres" + } + }, + "fan_options": { + "data": { + "entities": "Membres" + } + }, + "light_options": { + "data": { + "entities": "Membres" + } + }, + "media_player_options": { + "data": { + "entities": "Membres" + } + } + } + }, "state": { "_": { "closed": "Ferm\u00e9", diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index 1f3fcb467dd..6dc9ff2cce8 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri", + "name": "Nome" + }, + "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo.", + "title": "Nuovo gruppo" + }, "cover": { "data": { - "entities": "Membri del gruppo", - "name": "Nome del gruppo" + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" }, "description": "Seleziona le opzioni di gruppo" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Membri del gruppo", - "name": "Nome del gruppo" + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" }, "description": "Seleziona le opzioni di gruppo" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Membri del gruppo", - "name": "Nome del gruppo" + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" }, "description": "Seleziona le opzioni di gruppo" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Membri del gruppo", - "name": "Nome del gruppo" + "entities": "Membri", + "name": "Nome", + "title": "Nuovo gruppo" }, "description": "Seleziona le opzioni di gruppo" }, @@ -58,6 +71,42 @@ "entities": "Membri del gruppo" }, "description": "Seleziona le opzioni di gruppo" + }, + "user": { + "data": { + "group_type": "Tipo di gruppo" + }, + "title": "Nuovo gruppo" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri" + } + }, + "cover_options": { + "data": { + "entities": "Membri" + } + }, + "fan_options": { + "data": { + "entities": "Membri" + } + }, + "light_options": { + "data": { + "entities": "Membri" + } + }, + "media_player_options": { + "data": { + "entities": "Membri" + } } } }, diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index f71c8607a7f..dac7583169d 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -7,6 +7,7 @@ "entities": "\u30e1\u30f3\u30d0\u30fc", "name": "\u540d\u524d" }, + "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "cover": { diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index 4b1c8ef4db0..e1eb8b0914e 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden", + "name": "Naam" + }, + "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld.", + "title": "Nieuwe groep" + }, "cover": { "data": { "entities": "Groepsleden", - "name": "Groepsnaam" + "name": "Groepsnaam", + "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" }, @@ -17,7 +27,8 @@ "fan": { "data": { "entities": "Groepsleden", - "name": "Groepsnaam" + "name": "Groepsnaam", + "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" }, @@ -36,7 +47,8 @@ "light": { "data": { "entities": "Groepsleden", - "name": "Groepsnaam" + "name": "Groepsnaam", + "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" }, @@ -49,7 +61,8 @@ "media_player": { "data": { "entities": "Groepsleden", - "name": "Groepsnaam" + "name": "Groepsnaam", + "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" }, @@ -58,6 +71,42 @@ "entities": "Groepsleden" }, "description": "Selecteer groepsopties" + }, + "user": { + "data": { + "group_type": "Groepstype" + }, + "title": "Nieuwe groep" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden" + } + }, + "cover_options": { + "data": { + "entities": "Leden" + } + }, + "fan_options": { + "data": { + "entities": "Leden" + } + }, + "light_options": { + "data": { + "entities": "Leden" + } + }, + "media_player_options": { + "data": { + "entities": "Leden" + } } } }, diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index 70a46ee09c5..0046479d686 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer", + "name": "Navn" + }, + "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5.", + "title": "Ny gruppe" + }, "cover": { "data": { - "entities": "Gruppemedlemmer", - "name": "Gruppenavn" + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" }, "description": "Velg gruppealternativer" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Gruppemedlemmer", - "name": "Gruppenavn" + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" }, "description": "Velg gruppealternativer" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Gruppemedlemmer", - "name": "Gruppenavn" + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" }, "description": "Velg gruppealternativer" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Gruppemedlemmer", - "name": "Gruppenavn" + "entities": "Medlemmer", + "name": "Navn", + "title": "Ny gruppe" }, "description": "Velg gruppealternativer" }, @@ -58,6 +71,42 @@ "entities": "Gruppemedlemmer" }, "description": "Velg gruppealternativer" + }, + "user": { + "data": { + "group_type": "Gruppetype" + }, + "title": "Ny gruppe" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer" + } + }, + "cover_options": { + "data": { + "entities": "Medlemmer" + } + }, + "fan_options": { + "data": { + "entities": "Medlemmer" + } + }, + "light_options": { + "data": { + "entities": "Medlemmer" + } + }, + "media_player_options": { + "data": { + "entities": "Medlemmer" + } } } }, diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 50b3f605682..9ee5390db8a 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -1,4 +1,72 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar", + "name": "Namn" + }, + "title": "Ny grupp" + }, + "cover": { + "data": { + "title": "Ny grupp" + } + }, + "fan": { + "data": { + "title": "Ny grupp" + } + }, + "light": { + "data": { + "title": "Ny grupp" + } + }, + "media_player": { + "data": { + "title": "Ny grupp" + } + }, + "user": { + "data": { + "group_type": "Grupptyp" + }, + "title": "Ny grupp" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar" + } + }, + "cover_options": { + "data": { + "entities": "Medlemmar" + } + }, + "fan_options": { + "data": { + "entities": "Medlemmar" + } + }, + "light_options": { + "data": { + "entities": "Medlemmar" + } + }, + "media_player_options": { + "data": { + "entities": "Medlemmar" + } + } + } + }, "state": { "_": { "closed": "St\u00e4ngd", diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index f7cffdad9de..9187455ad17 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31" + }, + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", + "title": "\u65b0\u589e\u7fa4\u7d44" + }, "cover": { "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1", - "name": "\u7fa4\u7d44\u540d\u7a31" + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1", - "name": "\u7fa4\u7d44\u540d\u7a31" + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" }, @@ -29,14 +40,15 @@ }, "init": { "data": { - "group_type": "\u7fa4\u7d44\u985e\u578b" + "group_type": "\u7fa4\u7d44\u985e\u5225" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u985e\u578b" + "description": "\u9078\u64c7\u7fa4\u7d44\u985e\u5225" }, "light": { "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1", - "name": "\u7fa4\u7d44\u540d\u7a31" + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1", - "name": "\u7fa4\u7d44\u540d\u7a31" + "entities": "\u6210\u54e1", + "name": "\u540d\u7a31", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" }, @@ -58,6 +71,42 @@ "entities": "\u7fa4\u7d44\u6210\u54e1" }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + }, + "user": { + "data": { + "group_type": "\u7fa4\u7d44\u985e\u5225" + }, + "title": "\u65b0\u589e\u7fa4\u7d44" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1" + } + }, + "cover_options": { + "data": { + "entities": "\u6210\u54e1" + } + }, + "fan_options": { + "data": { + "entities": "\u6210\u54e1" + } + }, + "light_options": { + "data": { + "entities": "\u6210\u54e1" + } + }, + "media_player_options": { + "data": { + "entities": "\u6210\u54e1" + } } } }, diff --git a/homeassistant/components/habitica/translations/fr.json b/homeassistant/components/habitica/translations/fr.json index 00fcd36a508..c17151ea29e 100644 --- a/homeassistant/components/habitica/translations/fr.json +++ b/homeassistant/components/habitica/translations/fr.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "Cl\u00e9 API", + "api_key": "Cl\u00e9 d'API", "api_user": "ID utilisateur de l'API d'Habitica", "name": "Remplacez le nom d\u2019utilisateur d\u2019Habitica. Sera utilis\u00e9 pour les appels de service", "url": "URL" diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index 21897b04560..03812830b26 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -5,7 +5,7 @@ "dev": "\u958b\u767c\u7248", "docker": "Docker", "hassio": "Supervisor", - "installation_type": "\u5b89\u88dd\u985e\u578b", + "installation_type": "\u5b89\u88dd\u985e\u5225", "os_name": "\u4f5c\u696d\u7cfb\u7d71\u5bb6\u65cf", "os_version": "\u4f5c\u696d\u7cfb\u7d71\u7248\u672c", "python_version": "Python \u7248\u672c", diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 7de4c531d6a..1977f7e6c18 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -28,7 +28,7 @@ "advanced": { "data": { "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", - "devices": "P\u00e9riph\u00e9riques (d\u00e9clencheurs)" + "devices": "Appareils (d\u00e9clencheurs)" }, "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", "title": "Configuration avanc\u00e9e" diff --git a/homeassistant/components/homekit_controller/translations/select.fr.json b/homeassistant/components/homekit_controller/translations/select.fr.json index 9da1f007992..91f99ef94ef 100644 --- a/homeassistant/components/homekit_controller/translations/select.fr.json +++ b/homeassistant/components/homekit_controller/translations/select.fr.json @@ -1,7 +1,7 @@ { "state": { "homekit_controller__ecobee_mode": { - "away": "Loin", + "away": "Absent", "home": "Domicile", "sleep": "Sommeil" } diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index 106ff6225d5..01e1d1ed8c2 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "ID du point d'acc\u00e8s (SGTIN)", - "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les p\u00e9riph\u00e9riques)", + "name": "Nom (facultatif, utilis\u00e9 comme pr\u00e9fixe de nom pour tous les appareils)", "pin": "Code PIN" }, "title": "Choisissez le point d'acc\u00e8s HomematicIP" diff --git a/homeassistant/components/homewizard/translations/fr.json b/homeassistant/components/homewizard/translations/fr.json index 6ddc51565fb..f935f830ac9 100644 --- a/homeassistant/components/homewizard/translations/fr.json +++ b/homeassistant/components/homewizard/translations/fr.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "api_not_enabled": "L'API n'est pas activ\u00e9e. Activer l'API dans l'application HomeWizard Energy dans les param\u00e8tres", + "api_not_enabled": "L'API n'est pas activ\u00e9e. Activez l'API dans les param\u00e8tres de l'application HomeWizard Energy", "device_not_supported": "Cet appareil n'est pas compatible", "invalid_discovery_parameters": "Version d'API non prise en charge d\u00e9tect\u00e9e", "unknown_error": "Erreur inattendue" }, "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {product_type} ( {serial} ) \u00e0 {ip_address}\u00a0?", + "description": "Voulez-vous configurer {product_type} ({serial}) sur {ip_address}\u00a0?", "title": "Confirmer" }, "user": { diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index 7bb5c02543d..e3286cabfb4 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -23,7 +23,7 @@ "description": "Configurer l'autorisation sur votre serveur Hyperion Ambilight" }, "confirm": { - "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant? \n\n ** H\u00f4te: ** {host}\n ** Port: ** {port}\n ** ID **: {id}", + "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant\u00a0?\n\n**H\u00f4te\u00a0:** {host}\n**Port\u00a0:** {port}\n**ID\u00a0:** {id}", "title": "Confirmer l'ajout du service Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index 0d699085057..0adf9547d62 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -23,7 +23,7 @@ "description": "Configura l'autorizzazione per il tuo server Hyperion Ambilight" }, "confirm": { - "description": "Vuoi aggiungere questo Hyperion Ambilight a Home Assistant? \n\n ** Host:** {host}\n ** Porta:** {port}\n ** ID:** {id}", + "description": "Vuoi aggiungere questo Hyperion Ambilight a Home Assistant? \n\n **Host:** {host}\n **Porta:** {port}\n **ID:** {id}", "title": "Conferma l'aggiunta del servizio Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/ifttt/translations/fr.json b/homeassistant/components/ifttt/translations/fr.json index 4628b7bea8b..371bbd65ee0 100644 --- a/homeassistant/components/ifttt/translations/fr.json +++ b/homeassistant/components/ifttt/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index cf090f974f7..2176ea67b94 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -36,9 +36,9 @@ }, "user": { "data": { - "modem_type": "\u6578\u64da\u6a5f\u985e\u578b\u3002" + "modem_type": "\u6578\u64da\u6a5f\u985e\u5225\u3002" }, - "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u578b\u3002", + "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u5225\u3002", "title": "Insteon" } } diff --git a/homeassistant/components/iss/translations/fr.json b/homeassistant/components/iss/translations/fr.json index c51a38d131b..2f9ad5d427c 100644 --- a/homeassistant/components/iss/translations/fr.json +++ b/homeassistant/components/iss/translations/fr.json @@ -9,7 +9,7 @@ "data": { "show_on_map": "Afficher sur la carte\u00a0?" }, - "description": "Voulez-vous configurer la Station spatiale internationale?" + "description": "Voulez-vous configurer la Station spatiale internationale (ISS)\u00a0?" } } }, diff --git a/homeassistant/components/kaleidescape/translations/fr.json b/homeassistant/components/kaleidescape/translations/fr.json index 83083352c56..48dfd027765 100644 --- a/homeassistant/components/kaleidescape/translations/fr.json +++ b/homeassistant/components/kaleidescape/translations/fr.json @@ -13,6 +13,7 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { + "description": "Voulez-vous configurer le lecteur {model} nomm\u00e9 {name}\u00a0?", "title": "Kaleidescape" }, "user": { diff --git a/homeassistant/components/kaleidescape/translations/pt-BR.json b/homeassistant/components/kaleidescape/translations/pt-BR.json index 1be9ae58e2f..f87534cb88a 100644 --- a/homeassistant/components/kaleidescape/translations/pt-BR.json +++ b/homeassistant/components/kaleidescape/translations/pt-BR.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "unknown": "Erro inesperado", "unsupported": "Dispositivo n\u00e3o compat\u00edvel" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unsupported": "Dispositivo n\u00e3o compat\u00edvel" }, - "flow_title": "{model} ( {name} )", + "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Deseja configurar o player {model} chamado {name} ?", + "description": "Deseja configurar o player {model} chamado {name}?", "title": "Kaleidescape" }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "title": "Configura\u00e7\u00e3o do Kaleidescape" } diff --git a/homeassistant/components/kaleidescape/translations/sv.json b/homeassistant/components/kaleidescape/translations/sv.json new file mode 100644 index 00000000000..c5cfbbb662a --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{model} ({name})" + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 08e53ad8d49..31221e900cd 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -17,7 +17,7 @@ "route_back": "Retour/Mode NAT", "tunneling_type": "Type de tunnel KNX" }, - "description": "Veuillez saisir les informations de connexion de votre p\u00e9riph\u00e9rique de tunneling." + "description": "Veuillez saisir les informations de connexion de votre appareil de cr\u00e9ation de tunnel." }, "routing": { "data": { diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index cc9d29714b6..b6c09456fb3 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -15,7 +15,7 @@ "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u4fdd\u7559\u7a7a\u767d\u4ee5\u81ea\u52d5\u5075\u6e2c\uff09", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", - "tunneling_type": "KNX \u901a\u9053\u985e\u578b" + "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" }, @@ -36,9 +36,9 @@ }, "type": { "data": { - "connection_type": "KNX \u9023\u7dda\u985e\u578b" + "connection_type": "KNX \u9023\u7dda\u985e\u5225" }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u578b\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" } } }, @@ -46,7 +46,7 @@ "step": { "init": { "data": { - "connection_type": "KNX \u9023\u7dda\u985e\u578b", + "connection_type": "KNX \u9023\u7dda\u985e\u5225", "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u586b\u5165 0.0.0.0 \u555f\u7528\u81ea\u52d5\u5075\u6e2c\uff09", "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u7fa4\u7d44", @@ -61,7 +61,7 @@ "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", - "tunneling_type": "KNX \u901a\u9053\u985e\u578b" + "tunneling_type": "KNX \u901a\u9053\u985e\u5225" } } } diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 523bb10b969..9d6d522b7e1 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -40,7 +40,7 @@ "data": { "inverse": "\u53cd\u8f49\u958b\u555f/\u95dc\u9589\u72c0\u614b", "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", - "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u578b" + "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u5225" }, "description": "{zone}\u9078\u9805", "title": "\u8a2d\u5b9a\u4e8c\u9032\u4f4d\u611f\u61c9\u5668" @@ -49,7 +49,7 @@ "data": { "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09", - "type": "\u611f\u61c9\u5668\u985e\u578b" + "type": "\u611f\u61c9\u5668\u985e\u5225" }, "description": "{zone}\u9078\u9805", "title": "\u8a2d\u5b9a\u6578\u4f4d\u611f\u61c9\u5668" diff --git a/homeassistant/components/locative/translations/fr.json b/homeassistant/components/locative/translations/fr.json index 3cb74185f97..75dd083d5a3 100644 --- a/homeassistant/components/locative/translations/fr.json +++ b/homeassistant/components/locative/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/mailgun/translations/fr.json b/homeassistant/components/mailgun/translations/fr.json index a8d4b08ed83..80c961babba 100644 --- a/homeassistant/components/mailgun/translations/fr.json +++ b/homeassistant/components/mailgun/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/mill/translations/zh-Hant.json b/homeassistant/components/mill/translations/zh-Hant.json index a77203a0495..5c4745b23ae 100644 --- a/homeassistant/components/mill/translations/zh-Hant.json +++ b/homeassistant/components/mill/translations/zh-Hant.json @@ -21,11 +21,11 @@ }, "user": { "data": { - "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u578b", + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u9078\u64c7\u9023\u7dda\u985e\u578b\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u7b2c\u4e09\u4ee3\u52a0\u71b1\u5668" + "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u7b2c\u4e09\u4ee3\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/mjpeg/translations/fr.json b/homeassistant/components/mjpeg/translations/fr.json index abc52237b92..dcfadb77328 100644 --- a/homeassistant/components/mjpeg/translations/fr.json +++ b/homeassistant/components/mjpeg/translations/fr.json @@ -1,10 +1,38 @@ { + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nom", + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, "options": { + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, "step": { "init": { "data": { + "mjpeg_url": "URL MJPEG", + "name": "Nom", "password": "Mot de passe", - "username": "Nom d'utilisateur" + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" } } } diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/fr.json b/homeassistant/components/moehlenhoff_alpha2/translations/fr.json index 205436aa03d..27fc4e30675 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/fr.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/moon/translations/fr.json b/homeassistant/components/moon/translations/fr.json index 1bd91ac8a21..0b67320b311 100644 --- a/homeassistant/components/moon/translations/fr.json +++ b/homeassistant/components/moon/translations/fr.json @@ -8,5 +8,6 @@ "description": "Voulez-vous commencer la configuration\u00a0?" } } - } + }, + "title": "Lune" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/pt-BR.json b/homeassistant/components/moon/translations/pt-BR.json index 83022094ad0..1570f4110ec 100644 --- a/homeassistant/components/moon/translations/pt-BR.json +++ b/homeassistant/components/moon/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index db844987d88..1d9daffcb4d 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -17,10 +17,10 @@ }, "user": { "data": { - "admin_password": "Admin Mot de passe", - "admin_username": "Admin Nom d'utilisateur", - "surveillance_password": "Surveillance Mot de passe", - "surveillance_username": "Surveillance Nom d'utilisateur", + "admin_password": "Mot de passe Admin", + "admin_username": "Nom d'utilisateur Admin", + "surveillance_password": "Mot de passe Surveillance", + "surveillance_username": "Nom d'utilisateur Surveillance", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 526fe072cf7..2e1bb5bebfd 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -10,7 +10,7 @@ "step": { "broker": { "data": { - "broker": "Broker", + "broker": "Endere\u00e7o do Broker", "discovery": "Ativar descoberta", "password": "Senha", "port": "Porta", diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index 4853b124856..5774df64b78 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -70,7 +70,7 @@ }, "user": { "data": { - "gateway_type": "\u9598\u9053\u5668\u985e\u578b" + "gateway_type": "\u9598\u9053\u5668\u985e\u5225" }, "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f" } diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 4cd25a6cace..0edeef3dcfc 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -47,10 +47,10 @@ "public_weather": { "data": { "area_name": "Nom de la zone", - "lat_ne": "Latitude Nord-Est", - "lat_sw": "Latitude Sud-Ouest", - "lon_ne": "Longitude Nord-Est", - "lon_sw": "Longitude Sud-Ouest", + "lat_ne": "Latitude du coin nord-est", + "lat_sw": "Latitude du coin sud-ouest", + "lon_ne": "Longitude du coin nord-est", + "lon_sw": "Longitude du coin sud-ouest", "mode": "Calcul", "show_on_map": "Montrer sur la carte" }, diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index c1bfbca3330..e26cd64705f 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -47,10 +47,10 @@ "public_weather": { "data": { "area_name": "Nome dell'area", - "lat_ne": "Latitudine angolo Nord-Est", - "lat_sw": "Latitudine angolo Sud-Ovest", - "lon_ne": "Logitudine angolo Nord-Est", - "lon_sw": "Logitudine angolo Sud-Ovest", + "lat_ne": "Latitudine angolo nord-est", + "lat_sw": "Latitudine angolo sud-ovest", + "lon_ne": "Logitudine angolo norde-st", + "lon_sw": "Logitudine angolo sud-ovest", "mode": "Calcolo", "show_on_map": "Mostra sulla mappa" }, diff --git a/homeassistant/components/netgear/translations/fr.json b/homeassistant/components/netgear/translations/fr.json index 3230b8df45f..fe507462e8d 100644 --- a/homeassistant/components/netgear/translations/fr.json +++ b/homeassistant/components/netgear/translations/fr.json @@ -13,7 +13,7 @@ "password": "Mot de passe", "port": "Port (facultatif)", "ssl": "Utilise un certificat SSL", - "username": "Nom d'utilisateur (Optional)" + "username": "Nom d'utilisateur (facultatif)" }, "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\nNom d'utilisateur par d\u00e9faut\u00a0: {username}", "title": "Netgear" diff --git a/homeassistant/components/octoprint/translations/fr.json b/homeassistant/components/octoprint/translations/fr.json index da1389f619a..be78ad8b877 100644 --- a/homeassistant/components/octoprint/translations/fr.json +++ b/homeassistant/components/octoprint/translations/fr.json @@ -22,7 +22,7 @@ "port": "Num\u00e9ro de port", "ssl": "Utiliser SSL", "username": "Nom d'utilisateur", - "verify_ssl": "V\u00e9rifier certificat SSL" + "verify_ssl": "V\u00e9rifier le certificat SSL" } } } diff --git a/homeassistant/components/octoprint/translations/it.json b/homeassistant/components/octoprint/translations/it.json index 3b5950adee3..639b304417d 100644 --- a/homeassistant/components/octoprint/translations/it.json +++ b/homeassistant/components/octoprint/translations/it.json @@ -22,7 +22,7 @@ "port": "Numero porta", "ssl": "Utilizza SSL", "username": "Nome utente", - "verify_ssl": "Verifica certificato SSL" + "verify_ssl": "Verifica il certificato SSL" } } } diff --git a/homeassistant/components/oncue/translations/fr.json b/homeassistant/components/oncue/translations/fr.json index 1114bc4069e..b6704aabae1 100644 --- a/homeassistant/components/oncue/translations/fr.json +++ b/homeassistant/components/oncue/translations/fr.json @@ -4,9 +4,9 @@ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "Erreur de connexion", - "invalid_auth": "Erreur d'authentification", - "unknown": "Erreur" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/onewire/translations/fr.json b/homeassistant/components/onewire/translations/fr.json index b9e373d2cd9..0c75fb23ad1 100644 --- a/homeassistant/components/onewire/translations/fr.json +++ b/homeassistant/components/onewire/translations/fr.json @@ -28,16 +28,23 @@ "device_not_selected": "S\u00e9lectionnez les appareils \u00e0 configurer" }, "step": { + "ack_no_options": { + "description": "Il n'y a pas d'option pour l'impl\u00e9mentation de SysBus" + }, "configure_device": { "data": { "precision": "Pr\u00e9cision du capteur" }, - "description": "Choisissez la pr\u00e9cision du capteur pour {sensor_id}" + "description": "S\u00e9lectionnez la pr\u00e9cision du capteur pour {sensor_id}", + "title": "Pr\u00e9cision du capteur OneWire" }, "device_selection": { "data": { - "device_selection": "S\u00e9lectionnez les appareils \u00e0 configurer." - } + "clear_device_options": "Effacer toutes les configurations de l'appareil", + "device_selection": "S\u00e9lectionnez les appareils \u00e0 configurer" + }, + "description": "S\u00e9lectionnez les \u00e9tapes de configuration \u00e0 traiter", + "title": "Options de l'appareil OneWire" } } } diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json index 1fa61c78a41..61405f5089a 100644 --- a/homeassistant/components/onewire/translations/pt-BR.json +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -29,10 +29,6 @@ }, "step": { "ack_no_options": { - "data": { - "one": "um", - "other": "outros" - }, "description": "N\u00e3o h\u00e1 op\u00e7\u00f5es para a implementa\u00e7\u00e3o do SysBus", "title": "Op\u00e7\u00f5es de OneWire SysBus" }, diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index 9b46a737939..8b11692a3f5 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -17,7 +17,7 @@ }, "user": { "data": { - "type": "\u9023\u7dda\u985e\u578b" + "type": "\u9023\u7dda\u985e\u5225" }, "title": "\u8a2d\u5b9a 1-Wire" } diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index 4ea0ae566cc..ab772fcec2d 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -26,7 +26,7 @@ "port": "Port", "username": "Nom d'utilisateur" }, - "title": "Configurer le p\u00e9riph\u00e9rique ONVIF" + "title": "Configurer l'appareil ONVIF" }, "configure_profile": { "data": { diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 790aafc0796..87a27a29a5e 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -12,12 +12,12 @@ "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue" }, - "flow_title": "Passerelle : {gateway_id}", + "flow_title": "Passerelle\u00a0: {gateway_id}", "step": { "user": { "data": { "host": "H\u00f4te", - "hub": "Moyeu", + "hub": "Hub", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index cecdab86436..0d9eb4d0860 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index 0e51da481d7..2208ce32c8f 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -27,7 +27,7 @@ "use_addon": "\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6" }, "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "start_addon": { "data": { diff --git a/homeassistant/components/plaato/translations/fr.json b/homeassistant/components/plaato/translations/fr.json index f750645dc91..39cd68d9362 100644 --- a/homeassistant/components/plaato/translations/fr.json +++ b/homeassistant/components/plaato/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index 8ffa238d816..09b729b3a36 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -10,7 +10,7 @@ "default": "\u540d\u7a31\u70ba **{device_name}** \u7684 Plaato {device_type} \u5df2\u6210\u529f\u8a2d\u5b9a\uff01" }, "error": { - "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u578b", + "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u5225", "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756\u6216\u9078\u64c7 Webhook", "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u6b0a\u6756" }, @@ -26,7 +26,7 @@ "user": { "data": { "device_name": "\u88dd\u7f6e\u540d\u7a31", - "device_type": "Plaato \u88dd\u7f6e\u985e\u578b" + "device_type": "Plaato \u88dd\u7f6e\u985e\u5225" }, "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", "title": "\u8a2d\u5b9a Plaato \u88dd\u7f6e" diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index edd67dd41cb..0a1e21a12ed 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -12,10 +12,10 @@ "step": { "user": { "data": { - "flow_type": "\u9023\u7dda\u985e\u578b" + "flow_type": "\u9023\u7dda\u985e\u5225" }, "description": "\u7522\u54c1\uff1a", - "title": "Plugwise \u985e\u578b" + "title": "Plugwise \u985e\u5225" }, "user_gateway": { "data": { diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index 1f5d5ac22cd..27af1e3e119 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -14,7 +14,7 @@ "flow_title": "{nom} ({ip_address})", "step": { "confirm_discovery": { - "description": "Voulez-vous configurer {name} ({ip_address})?", + "description": "Voulez-vous configurer {name} ({ip_address})\u00a0?", "title": "Connectez-vous au Powerwall" }, "reauth_confim": { diff --git a/homeassistant/components/pure_energie/translations/fr.json b/homeassistant/components/pure_energie/translations/fr.json index 9cb1d7dfd16..7ab74aae074 100644 --- a/homeassistant/components/pure_energie/translations/fr.json +++ b/homeassistant/components/pure_energie/translations/fr.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{model} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/pure_energie/translations/it.json b/homeassistant/components/pure_energie/translations/it.json index 457f7cfebc0..b7fb47b1ea4 100644 --- a/homeassistant/components/pure_energie/translations/it.json +++ b/homeassistant/components/pure_energie/translations/it.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "cannot_connect": "Connessione fallita" + "cannot_connect": "Impossibile connettersi" }, "error": { - "cannot_connect": "Connessione fallita" + "cannot_connect": "Impossibile connettersi" }, "flow_title": "{model} ({host})", "step": { diff --git a/homeassistant/components/pvoutput/translations/fr.json b/homeassistant/components/pvoutput/translations/fr.json index 3ca874733e4..1e10597e67a 100644 --- a/homeassistant/components/pvoutput/translations/fr.json +++ b/homeassistant/components/pvoutput/translations/fr.json @@ -1,22 +1,22 @@ { "config": { "abort": { - "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "Erreur de connexion", - "invalid_auth": "Erreur d'authentification" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" }, "step": { "reauth_confirm": { "data": { - "api_key": "Cl\u00e9 API" + "api_key": "Cl\u00e9 d'API" }, "description": "Pour vous r\u00e9-authentifier avec PVOutput, vous devrez obtenir la cl\u00e9 API sur {account_url} ." }, "user": { "data": { - "api_key": "Cl\u00e9 API", + "api_key": "Cl\u00e9 d'API", "system_id": "ID syst\u00e8me" }, "description": "Pour vous authentifier avec PVOutput, vous devrez obtenir la cl\u00e9 API sur {account_url} . \n\n Les ID syst\u00e8me des syst\u00e8mes enregistr\u00e9s sont r\u00e9pertori\u00e9s sur cette m\u00eame page." diff --git a/homeassistant/components/radio_browser/translations/fr.json b/homeassistant/components/radio_browser/translations/fr.json new file mode 100644 index 00000000000..807ba246694 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 2444a202720..62c064526ae 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -19,7 +19,7 @@ "step": { "init": { "data": { - "friendly_name": "\u91dd\u5c0d\u9078\u53d6\u985e\u578b\u4f7f\u7528\u53cb\u5584\u540d\u7a31\uff08\u5047\u5982\u9069\u7528\uff09" + "friendly_name": "\u91dd\u5c0d\u9078\u53d6\u985e\u5225\u4f7f\u7528\u53cb\u5584\u540d\u7a31\uff08\u5047\u5982\u9069\u7528\uff09" }, "title": "\u8a2d\u5b9a Recollect Waste" } diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index c86701185cf..3f84f0a5f98 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -29,9 +29,9 @@ }, "user": { "data": { - "type": "\u9023\u7dda\u985e\u578b" + "type": "\u9023\u7dda\u985e\u5225" }, - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" } } }, diff --git a/homeassistant/components/rtsp_to_webrtc/translations/fr.json b/homeassistant/components/rtsp_to_webrtc/translations/fr.json index 1235f36d26a..e51207a6254 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/fr.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "Le serveur RTSPtoWebRTC a renvoy\u00e9 une erreur. Consultez les journaux pour plus d'informations.", "server_unreachable": "Impossible de communiquer avec le serveur RTSPtoWebRTC. Consultez les journaux pour plus d'informations.", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible" + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "invalid_url": "Doit \u00eatre une URL de serveur RTSPtoWebRTC valide, par exemple https://example.com", diff --git a/homeassistant/components/season/translations/fr.json b/homeassistant/components/season/translations/fr.json index 9c74e1b5026..a3bf66a400e 100644 --- a/homeassistant/components/season/translations/fr.json +++ b/homeassistant/components/season/translations/fr.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "type": "D\u00e9finition du type de saison" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/zh-Hant.json b/homeassistant/components/season/translations/zh-Hant.json index 4e954253198..738ccab2c26 100644 --- a/homeassistant/components/season/translations/zh-Hant.json +++ b/homeassistant/components/season/translations/zh-Hant.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "type": "\u5b63\u7bc0\u985e\u578b\u5b9a\u7fa9" + "type": "\u5b63\u7bc0\u985e\u5225\u5b9a\u7fa9" } } } diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index 91fcbeb94bc..725073d5c66 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -12,7 +13,8 @@ "reauth_validate": { "data": { "password": "Mot de passe" - } + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { "data": { diff --git a/homeassistant/components/senseme/translations/fr.json b/homeassistant/components/senseme/translations/fr.json index efa82925191..fb185b7ee53 100644 --- a/homeassistant/components/senseme/translations/fr.json +++ b/homeassistant/components/senseme/translations/fr.json @@ -5,13 +5,13 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "cannot_connect": "Impossible de se connecter", - "invalid_host": "Adresse IP ou nom d'h\u00f4te invalide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" }, "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {name} - {model} ( {host} )\u00a0?" + "description": "Voulez-vous configurer {name} - {model} ({host})\u00a0?" }, "manual": { "data": { @@ -23,7 +23,7 @@ "data": { "device": "Appareil" }, - "description": "S\u00e9lectionnez un appareil ou choisissez \u00ab Adresse IP \u00bb pour entrer manuellement une adresse IP." + "description": "S\u00e9lectionnez un appareil ou choisissez \u00ab\u00a0Adresse IP\u00a0\u00bb pour saisir manuellement une adresse IP." } } } diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 60df120d77e..0509886e5ce 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -6,8 +6,10 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", + "incorrect_api_key": "Cl\u00e9 d'API non valide pour le compte s\u00e9lectionn\u00e9", "invalid_auth": "Authentification invalide", - "no_devices": "Aucun appareil d\u00e9couvert" + "no_devices": "Aucun appareil d\u00e9couvert", + "no_username": "Impossible d'obtenir le nom d'utilisateur" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index 40719894b2a..ff0ac4883ba 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -14,7 +14,7 @@ "step": { "reauth_confirm": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" } }, "user": { diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 1e13fb79fd2..321e3e3108b 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -30,7 +30,7 @@ "is_voltage": "Tensione attuale di {entity_name}" }, "trigger_type": { - "apparent_power": "variazioni di potenza apparente di {entity_name}", + "apparent_power": "Variazioni di potenza apparente di {entity_name}", "battery_level": "variazioni del livello di batteria di {entity_name} ", "carbon_dioxide": "Variazioni della concentrazione di anidride carbonica di {entity_name}", "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", @@ -50,7 +50,7 @@ "power": "Variazioni di alimentazione di {entity_name}", "power_factor": "variazioni del fattore di potenza di {entity_name}", "pressure": "Variazioni della pressione di {entity_name}", - "reactive_power": "variazioni di potenza reattiva di {entity_name}", + "reactive_power": "Variazioni di potenza reattiva di {entity_name}", "signal_strength": "Variazioni della potenza del segnale di {entity_name}", "sulphur_dioxide": "Variazioni della concentrazione di anidride solforosa di {entity_name}", "temperature": "Variazioni di temperatura di {entity_name}", diff --git a/homeassistant/components/sleepiq/translations/fr.json b/homeassistant/components/sleepiq/translations/fr.json index 199a0bd89f5..cab4affaac1 100644 --- a/homeassistant/components/sleepiq/translations/fr.json +++ b/homeassistant/components/sleepiq/translations/fr.json @@ -1,12 +1,24 @@ { "config": { "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, "step": { "reauth_confirm": { "data": { "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" } } } diff --git a/homeassistant/components/solax/translations/fr.json b/homeassistant/components/solax/translations/fr.json index a0d7a14dcdb..703bd8de288 100644 --- a/homeassistant/components/solax/translations/fr.json +++ b/homeassistant/components/solax/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json index de666f5e5a2..7e9cc805102 100644 --- a/homeassistant/components/sonarr/translations/fr.json +++ b/homeassistant/components/sonarr/translations/fr.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "L'int\u00e9gration Sonarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Sonarr h\u00e9berg\u00e9e sur: {host}", + "description": "L'int\u00e9gration Sonarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Sonarr h\u00e9berg\u00e9e sur\u00a0: {url}", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { diff --git a/homeassistant/components/steamist/translations/fr.json b/homeassistant/components/steamist/translations/fr.json index f3c122a2f2c..0427cb2e87e 100644 --- a/homeassistant/components/steamist/translations/fr.json +++ b/homeassistant/components/steamist/translations/fr.json @@ -3,18 +3,18 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "cannot_connect": "Impossible de se connecter", - "no_devices_found": "Pas d'appareils trouv\u00e9 sur le r\u00e9seau", + "cannot_connect": "\u00c9chec de connexion", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "not_steamist_device": "Pas un appareil \u00e0 vapeur" }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "unknown": "Erreur inattendue" }, - "flow_title": "{name} ( {ipaddress} )", + "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {name} ( {ipaddress} )\u00a0?" + "description": "Voulez-vous configurer {name} ({ipaddress})\u00a0?" }, "pick_device": { "data": { @@ -25,7 +25,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/steamist/translations/it.json b/homeassistant/components/steamist/translations/it.json index 284eb8e8401..8bcec86344c 100644 --- a/homeassistant/components/steamist/translations/it.json +++ b/homeassistant/components/steamist/translations/it.json @@ -3,15 +3,15 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "cannot_connect": "Connessione non riuscita", + "cannot_connect": "Impossibile connettersi", "no_devices_found": "Nessun dispositivo trovato sulla rete", "not_steamist_device": "Non \u00e8 un dispositivo a vapore" }, "error": { "cannot_connect": "Impossibile connettersi", - "unknown": "Errore inatteso" + "unknown": "Errore imprevisto" }, - "flow_title": "{name} ( {ipaddress} )", + "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { "description": "Vuoi configurare {name} ({ipaddress})?" diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index 08d14c21f13..7df7aa84f6a 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entit\u00e9 du commutateur" + } + } + } + }, "device_automation": { "action_type": { "toggle": "Basculer {entity_name}", diff --git a/homeassistant/components/switch_as_x/translations/fr.json b/homeassistant/components/switch_as_x/translations/fr.json new file mode 100644 index 00000000000..4b5bd6beebe --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entit\u00e9 du commutateur", + "target_domain": "Type" + }, + "title": "Transformer un commutateur en \u2026" + } + } + }, + "title": "Commutateur en tant que X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/it.json b/homeassistant/components/switch_as_x/translations/it.json new file mode 100644 index 00000000000..1ef154b0578 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Cambia entit\u00e0", + "target_domain": "Tipo" + }, + "title": "Rendi un interruttore un..." + } + } + }, + "title": "Interruttore come X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/nl.json b/homeassistant/components/switch_as_x/translations/nl.json index d434790fc6b..b1712904a76 100644 --- a/homeassistant/components/switch_as_x/translations/nl.json +++ b/homeassistant/components/switch_as_x/translations/nl.json @@ -3,10 +3,12 @@ "step": { "init": { "data": { + "entity_id": "Entiteit wijzigen", "target_domain": "Type" }, "title": "Schakel een..." } } - } + }, + "title": "Schakelen als X" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/no.json b/homeassistant/components/switch_as_x/translations/no.json new file mode 100644 index 00000000000..09d8d4e813e --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Bytt enhet", + "target_domain": "Type" + }, + "title": "Gj\u00f8r en bryter til en ..." + } + } + }, + "title": "Bryter som X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json new file mode 100644 index 00000000000..835de2f5a7f --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Kontakt-entitet", + "target_domain": "Typ" + }, + "title": "G\u00f6r en kontakt till ..." + } + } + }, + "title": "Kontakt som X" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/zh-Hant.json b/homeassistant/components/switch_as_x/translations/zh-Hant.json new file mode 100644 index 00000000000..231f5b58eff --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u958b\u95dc\u5be6\u9ad4", + "target_domain": "\u985e\u5225" + }, + "title": "\u5c07\u958b\u95dc\u8a2d\u5b9a\u70ba ..." + } + } + }, + "title": "\u958b\u95dc\u8a2d\u70ba X" +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index c554326c5da..d760e6eeb1a 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "no_unconfigured_devices": "Aucun p\u00e9riph\u00e9rique non configur\u00e9 trouv\u00e9.", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", "switchbot_unsupported_type": "Type Switchbot non pris en charge.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 44fe1fe5c54..8e7b4495328 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", - "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u578b\u3002", + "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index 0607c01c14d..d3a5fad5939 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -25,7 +25,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des p\u00e9riph\u00e9riques." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/traccar/translations/fr.json b/homeassistant/components/traccar/translations/fr.json index b3e9684424b..ab5d254dd98 100644 --- a/homeassistant/components/traccar/translations/fr.json +++ b/homeassistant/components/traccar/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, diff --git a/homeassistant/components/tuya/translations/select.fr.json b/homeassistant/components/tuya/translations/select.fr.json index 31199b930bf..c525238e4de 100644 --- a/homeassistant/components/tuya/translations/select.fr.json +++ b/homeassistant/components/tuya/translations/select.fr.json @@ -11,12 +11,12 @@ "2": "Activ\u00e9" }, "tuya__countdown": { - "1h": "1 heure", - "2h": "2 heures", - "3h": "3 heures", - "4h": "4 heures", - "5h": "5 heures", - "6h": "6 heures", + "1h": "1\u00a0heure", + "2h": "2\u00a0heures", + "3h": "3\u00a0heures", + "4h": "4\u00a0heures", + "5h": "5\u00a0heures", + "6h": "6\u00a0heures", "cancel": "Annuler" }, "tuya__curtain_mode": { @@ -24,8 +24,8 @@ "night": "Nuit" }, "tuya__curtain_motor_mode": { - "back": "Retour", - "forward": "Avance rapide" + "back": "En arri\u00e8re", + "forward": "En avant" }, "tuya__decibel_sensitivity": { "0": "Faible sensibilit\u00e9", diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index 10247767161..e9898f782af 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -32,7 +32,7 @@ "password": "\u5bc6\u78bc", "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", "region": "\u5340\u57df", - "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u578b", + "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u5225", "username": "\u5e33\u865f" }, "description": "\u8f38\u5165 Tuya \u6191\u8b49", @@ -45,8 +45,8 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { - "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", - "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", + "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u5225", + "dev_not_config": "\u88dd\u7f6e\u985e\u5225\u7121\u6cd5\u8a2d\u5b9a", "dev_not_found": "\u627e\u4e0d\u5230\u88dd\u7f6e" }, "step": { diff --git a/homeassistant/components/twilio/translations/fr.json b/homeassistant/components/twilio/translations/fr.json index f602994d8b2..a3ee815f4f9 100644 --- a/homeassistant/components/twilio/translations/fr.json +++ b/homeassistant/components/twilio/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "Pas connect\u00e9 \u00e0 Home Assistant Cloud", + "cloud_not_connected": "Non connect\u00e9 \u00e0 Home Assistant Cloud.", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "webhook_not_internet_accessible": "Votre installation de Home Assistant doit \u00eatre accessible depuis internet pour recevoir des messages webhook." }, @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer Twilio?", + "description": "Voulez-vous commencer la configuration\u00a0?", "title": "Configurer le Webhook Twilio" } } diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 36c9ee5bcc6..58b27b164d0 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -57,8 +57,8 @@ "simple_options": { "data": { "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau", - "track_clients": "Suivi de clients r\u00e9seaux", - "track_devices": "Suivi d'\u00e9quipement r\u00e9seau (Equipements Ubiquiti)" + "track_clients": "Suivre les clients du r\u00e9seau", + "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)" }, "description": "Configurer l'int\u00e9gration UniFi" }, diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index efd0cb529b1..4f14db0005d 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -10,7 +10,7 @@ "protect_version": "La version minimale requise est la v1.20.0. Veuillez mettre \u00e0 jour UniFi Protect, puis r\u00e9essayer.", "unknown": "Erreur inattendue" }, - "flow_title": "{name} ( {ip_address} )", + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { @@ -18,7 +18,7 @@ "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, - "description": "Voulez-vous configurer {name} ({ip_address})? Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", + "description": "Voulez-vous configurer {name} ({ip_address})\u00a0? Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", "title": "UniFi Protect d\u00e9couvert" }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index a7f82827ffb..747647b3d26 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -18,7 +18,7 @@ "username": "Nome utente", "verify_ssl": "Verifica il certificato SSL" }, - "description": "Vuoi configurare {name} ( {ip_address} )? Avrai bisogno di un utente locale creato nella tua console UniFi OS con cui accedere. Gli utenti Ubiquiti Cloud non funzioneranno. Per ulteriori informazioni: {local_user_documentation_url}", + "description": "Vuoi configurare {name} ({ip_address})? Avrai bisogno di un utente locale creato nella tua console UniFi OS con cui accedere. Gli utenti Ubiquiti Cloud non funzioneranno. Per ulteriori informazioni: {local_user_documentation_url}", "title": "Rilevato UniFi Protect" }, "reauth_confirm": { diff --git a/homeassistant/components/update/translations/fr.json b/homeassistant/components/update/translations/fr.json new file mode 100644 index 00000000000..1a49a0cab9f --- /dev/null +++ b/homeassistant/components/update/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Mettre \u00e0 jour" +} \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/fr.json b/homeassistant/components/vallox/translations/fr.json index 9bd74d4273c..4aa4f53e076 100644 --- a/homeassistant/components/vallox/translations/fr.json +++ b/homeassistant/components/vallox/translations/fr.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Service d\u00e9j\u00e0 configur\u00e9", - "cannot_connect": "Erreur de connexion", - "invalid_host": "Nom d'h\u00f4te or IP invalide", - "unknown": "Erreur inconnue" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "unknown": "Erreur inattendue" }, "error": { - "cannot_connect": "Erreur de connexion", - "invalid_host": "Nom d'h\u00f4te or IP invalide", - "unknown": "Erreur inconnue" + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "unknown": "Erreur inattendue" }, "step": { "user": { diff --git a/homeassistant/components/velbus/translations/zh-Hant.json b/homeassistant/components/velbus/translations/zh-Hant.json index ec0c1ca2c63..0f5e65c7cba 100644 --- a/homeassistant/components/velbus/translations/zh-Hant.json +++ b/homeassistant/components/velbus/translations/zh-Hant.json @@ -13,7 +13,7 @@ "name": "Velbus \u9023\u7dda\u540d\u7a31", "port": "\u9023\u7dda\u5b57\u4e32" }, - "title": "\u5b9a\u7fa9 Velbus \u9023\u7dda\u985e\u578b" + "title": "\u5b9a\u7fa9 Velbus \u9023\u7dda\u985e\u5225" } } } diff --git a/homeassistant/components/version/translations/zh-Hant.json b/homeassistant/components/version/translations/zh-Hant.json index ee24e6a4298..4e5ead19e3e 100644 --- a/homeassistant/components/version/translations/zh-Hant.json +++ b/homeassistant/components/version/translations/zh-Hant.json @@ -9,12 +9,12 @@ "version_source": "\u7248\u672c\u4f86\u6e90" }, "description": "\u8ffd\u8e64\u7248\u672c\u4f86\u6e90", - "title": "\u9078\u64c7\u5b89\u88dd\u985e\u578b" + "title": "\u9078\u64c7\u5b89\u88dd\u985e\u5225" }, "version_source": { "data": { "beta": "\u5305\u542b\u6e2c\u8a66\u7248\u672c", - "board": "\u6240\u8981\u8ffd\u8e64\u7684\u786c\u9ad4\u985e\u578b", + "board": "\u6240\u8981\u8ffd\u8e64\u7684\u786c\u9ad4\u985e\u5225", "channel": "\u6240\u8981\u8ffd\u8e64\u7684\u7248\u6b21", "image": "\u6240\u8981\u8ffd\u8e64\u7684\u6620\u50cf\u6a94\u7248\u672c" }, diff --git a/homeassistant/components/vicare/translations/fr.json b/homeassistant/components/vicare/translations/fr.json index 6f7d6df3624..69c8b127bb0 100644 --- a/homeassistant/components/vicare/translations/fr.json +++ b/homeassistant/components/vicare/translations/fr.json @@ -2,21 +2,21 @@ "config": { "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "unknown": "Erreur inatendue" + "unknown": "Erreur inattendue" }, "error": { "invalid_auth": "Authentification invalide" }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "client_id": "Cl\u00e9 API", + "client_id": "Cl\u00e9 d'API", "heating_type": "Type de chauffage", "name": "Nom", "password": "Mot de passe", "scan_interval": "Intervalle de balayage (secondes)", - "username": "Email" + "username": "Courriel" }, "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 API se rendre sur https://developer.viessmann.com", "title": "{name}" diff --git a/homeassistant/components/vicare/translations/zh-Hant.json b/homeassistant/components/vicare/translations/zh-Hant.json index c86ebf3cb34..c3b8fc02c4f 100644 --- a/homeassistant/components/vicare/translations/zh-Hant.json +++ b/homeassistant/components/vicare/translations/zh-Hant.json @@ -12,7 +12,7 @@ "user": { "data": { "client_id": "API \u91d1\u9470", - "heating_type": "\u6696\u6c23\u985e\u578b", + "heating_type": "\u6696\u6c23\u985e\u5225", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", diff --git a/homeassistant/components/waze_travel_time/translations/zh-Hant.json b/homeassistant/components/waze_travel_time/translations/zh-Hant.json index 5c30067de6c..d08d215b815 100644 --- a/homeassistant/components/waze_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/waze_travel_time/translations/zh-Hant.json @@ -29,7 +29,7 @@ "incl_filter": "\u6240\u9078\u64c7\u8def\u7dda\u63cf\u8ff0\u5305\u542b Substring", "realtime": "\u5373\u6642\u65c5\u7a0b\u6642\u9593\uff1f", "units": "\u55ae\u4f4d", - "vehicle_type": "\u8eca\u8f1b\u985e\u578b" + "vehicle_type": "\u8eca\u8f1b\u985e\u5225" }, "description": "`substring` \u8f38\u5165\u53ef\u4f9b\u5f37\u5236\u6574\u5408\u3001\u65bc\u8a08\u7b97\u65c5\u7a0b\u6642\u9593\u6642\uff0c\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u6216\u907f\u958b\u4f7f\u7528\u7279\u5b9a\u8def\u7dda\u3002" } diff --git a/homeassistant/components/webostv/translations/fr.json b/homeassistant/components/webostv/translations/fr.json index bccb1c3aa3c..7621d297a1d 100644 --- a/homeassistant/components/webostv/translations/fr.json +++ b/homeassistant/components/webostv/translations/fr.json @@ -3,16 +3,16 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "error_pairing": "Connect\u00e9 au t\u00e9l\u00e9viseur LG webOS mais non jumel\u00e9" + "error_pairing": "Connect\u00e9 \u00e0 la TV LG webOS mais non appair\u00e9" }, "error": { - "cannot_connect": "Impossible de vous connecter, veuillez allumer votre t\u00e9l\u00e9viseur ou v\u00e9rifier l\u2019adresse IP" + "cannot_connect": "\u00c9chec de la connexion, veuillez allumer votre TV ou v\u00e9rifier l'adresse IP" }, - "flow_title": "LG webOS Smart TV", + "flow_title": "TV connect\u00e9e LG webOS", "step": { "pairing": { "description": "Cliquez sur soumettre et acceptez la demande de jumelage sur votre t\u00e9l\u00e9viseur.\n\n![Image](/static/images/config_webos.png)", - "title": "Appairage webOS TV" + "title": "Appairage de la TV webOS" }, "user": { "data": { @@ -20,7 +20,7 @@ "name": "Nom" }, "description": "Allumez la t\u00e9l\u00e9vision, remplissez les champs suivants, cliquez sur Envoyer", - "title": "Se connecter \u00e0 webOS TV" + "title": "Se connecter \u00e0 la TV webOS" } } }, @@ -40,7 +40,7 @@ "sources": "Liste des sources" }, "description": "S\u00e9lectionnez les sources activ\u00e9es", - "title": "Options pour webOS Smart TV" + "title": "Options pour TV connect\u00e9e webOS" } } } diff --git a/homeassistant/components/whois/translations/fr.json b/homeassistant/components/whois/translations/fr.json index e45ecd615da..457c295bbca 100644 --- a/homeassistant/components/whois/translations/fr.json +++ b/homeassistant/components/whois/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { "unexpected_response": "R\u00e9ponse inattendue du serveur whois", diff --git a/homeassistant/components/wiz/translations/fr.json b/homeassistant/components/wiz/translations/fr.json index 82a29f54bfc..562a6a7f765 100644 --- a/homeassistant/components/wiz/translations/fr.json +++ b/homeassistant/components/wiz/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { @@ -17,7 +18,7 @@ "description": "Voulez-vous commencer la configuration\u00a0?" }, "discovery_confirm": { - "description": "Voulez-vous configurer {name} ({host}) ?" + "description": "Voulez-vous configurer {name} ({host})\u00a0?" }, "pick_device": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.it.json b/homeassistant/components/wolflink/translations/sensor.it.json index e5eb50e6586..7ca8a76ceac 100644 --- a/homeassistant/components/wolflink/translations/sensor.it.json +++ b/homeassistant/components/wolflink/translations/sensor.it.json @@ -49,7 +49,7 @@ "nur_heizgerat": "Solo caldaia", "parallelbetrieb": "Modalit\u00e0 parallela", "partymodus": "Modalit\u00e0 festa", - "perm_cooling": "Raffreddamento Permanente", + "perm_cooling": "Raffreddamento permanente", "permanent": "Permanente", "permanentbetrieb": "Modalit\u00e0 permanente", "reduzierter_betrieb": "Modalit\u00e0 limitata", diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index 31779ba80b5..f64f691f78d 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "Impossible de d\u00e9couvrir une passerelle Xiaomi Aqara, essayez d'utiliser l'IP du p\u00e9riph\u00e9rique ex\u00e9cutant HomeAssistant comme interface", - "invalid_host": "Adresse IP non valide, voir https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_host": "Adresse IP non valide, consultez https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interface r\u00e9seau non valide", "invalid_key": "Cl\u00e9 de passerelle non valide", "invalid_mac": "Adresse MAC non valide" diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 27330c11242..319a33f3964 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, prova a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia", - "invalid_host": "Nome host o indirizzo IP non valido, vedere https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_host": "Nome host o indirizzo IP non valido, vedi https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interfaccia di rete non valida", "invalid_key": "Chiave gateway non valida", "invalid_mac": "Indirizzo Mac non valido" diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json index 76065006684..40530caa974 100644 --- a/homeassistant/components/yale_smart_alarm/translations/fr.json +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "Impossible de se connecter", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, "step": { diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index fdb3a8476fe..a5e890bcaf4 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "not_zha_device": "Cet appareil n'est pas un appareil zha", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "usb_probe_failed": "\u00c9chec de la v\u00e9rification du p\u00e9riph\u00e9rique USB" + "usb_probe_failed": "\u00c9chec de l'analyse du p\u00e9riph\u00e9rique USB" }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index e0904cf0683..9bf7d3c9208 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -15,10 +15,10 @@ }, "pick_radio": { "data": { - "radio_type": "\u7121\u7dda\u96fb\u985e\u578b" + "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" }, - "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u578b", - "title": "\u7121\u7dda\u96fb\u985e\u578b" + "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u985e\u5225", + "title": "\u7121\u7dda\u96fb\u985e\u5225" }, "port_config": { "data": { diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 0933879e517..6d447b41a22 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -97,7 +97,7 @@ "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "different_device": "Le p\u00e9riph\u00e9rique USB connect\u00e9 n'est pas le m\u00eame que pr\u00e9c\u00e9demment configur\u00e9 pour cette entr\u00e9e de configuration. Veuillez plut\u00f4t cr\u00e9er une nouvelle entr\u00e9e de configuration pour le nouveau p\u00e9riph\u00e9rique." + "different_device": "Le p\u00e9riph\u00e9rique USB connect\u00e9 n'est pas le m\u00eame que celui qui \u00e9tait pr\u00e9c\u00e9demment configur\u00e9 pour cette entr\u00e9e de configuration. Veuillez \u00e0 la place cr\u00e9er une nouvelle entr\u00e9e de configuration pour le nouveau p\u00e9riph\u00e9rique." }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index b16f313fbd4..664cd6b48d9 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -52,7 +52,7 @@ "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6" }, "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "start_addon": { "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" @@ -136,7 +136,7 @@ "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6" }, "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "start_addon": { "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" From c4b3e2b9cd994adf6c889bb4e337df5514c64c61 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 12 Mar 2022 03:23:24 +0100 Subject: [PATCH 0366/1054] Add select entity to Sensibo (#67741) --- .coveragerc | 1 + homeassistant/components/sensibo/const.py | 2 +- .../components/sensibo/coordinator.py | 12 ++ homeassistant/components/sensibo/entity.py | 5 + homeassistant/components/sensibo/select.py | 117 ++++++++++++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensibo/select.py diff --git a/.coveragerc b/.coveragerc index 1f1efb96e7b..f48943388c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1017,6 +1017,7 @@ omit = homeassistant/components/sensibo/diagnostics.py homeassistant/components/sensibo/entity.py homeassistant/components/sensibo/number.py + homeassistant/components/sensibo/select.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 683a403cb08..9558376f079 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -12,7 +12,7 @@ LOGGER = logging.getLogger(__package__) DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" -PLATFORMS = [Platform.CLIMATE, Platform.NUMBER] +PLATFORMS = [Platform.CLIMATE, Platform.NUMBER, Platform.SELECT] ALL = ["all"] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 089cc0406bd..bceede8664e 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -91,6 +91,8 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): running = ac_states.get("on") fan_mode = ac_states.get("fanLevel") swing_mode = ac_states.get("swing") + horizontal_swing_mode = ac_states.get("horizontalSwing") + light_mode = ac_states.get("light") available = dev["connectionStatus"].get("isAlive", True) capabilities = dev["remoteCapabilities"] hvac_modes = list(capabilities["modes"]) @@ -99,6 +101,8 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): current_capabilities = capabilities["modes"][ac_states.get("mode")] fan_modes = current_capabilities.get("fanLevels") swing_modes = current_capabilities.get("swing") + horizontal_swing_modes = current_capabilities.get("horizontalSwing") + light_modes = current_capabilities.get("light") temperature_unit_key = dev.get("temperatureUnit") or ac_states.get( "temperatureUnit" ) @@ -123,6 +127,10 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): full_features.add("swing") if "fanLevels" in capabilities["modes"][mode]: full_features.add("fanLevel") + if "horizontalSwing" in capabilities["modes"][mode]: + full_features.add("horizontalSwing") + if "light" in capabilities["modes"][mode]: + full_features.add("light") state = hvac_mode if hvac_mode else "off" @@ -167,10 +175,14 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): "on": running, "fan_mode": fan_mode, "swing_mode": swing_mode, + "horizontal_swing_mode": horizontal_swing_mode, + "light_mode": light_mode, "available": available, "hvac_modes": hvac_modes, "fan_modes": fan_modes, "swing_modes": swing_modes, + "horizontal_swing_modes": horizontal_swing_modes, + "light_modes": light_modes, "temp_unit": temperature_unit_key, "temp_list": temperatures_list, "temp_step": temperature_step, diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index ff68b0ebfd1..026cca4ddff 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -41,6 +41,11 @@ class SensiboBaseEntity(CoordinatorEntity): suggested_area=device["name"], ) + @property + def device_data(self) -> dict[str, Any]: + """Return data for device.""" + return self.coordinator.data.parsed[self._device_id] + async def async_send_command( self, command: str, params: dict[str, Any] ) -> dict[str, Any]: diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py new file mode 100644 index 00000000000..3f615f06afe --- /dev/null +++ b/homeassistant/components/sensibo/select.py @@ -0,0 +1,117 @@ +"""Number platform for Sensibo integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboBaseEntity + + +@dataclass +class SensiboSelectDescriptionMixin: + """Mixin values for Sensibo entities.""" + + remote_key: str + remote_options: str + + +@dataclass +class SensiboSelectEntityDescription( + SelectEntityDescription, SensiboSelectDescriptionMixin +): + """Class describing Sensibo Number entities.""" + + +SELECT_TYPES = ( + SensiboSelectEntityDescription( + key="horizontalSwing", + remote_key="horizontal_swing_mode", + remote_options="horizontal_swing_modes", + name="Horizontal Swing", + icon="mdi:air-conditioner", + ), + SensiboSelectEntityDescription( + key="light", + remote_key="light_mode", + remote_options="light_modes", + name="Light", + icon="mdi:flashlight", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo number platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SensiboSelect(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in SELECT_TYPES + if device_data["hvac_modes"] and description.key in device_data["full_features"] + ) + + +class SensiboSelect(SensiboBaseEntity, SelectEntity): + """Representation of a Sensibo Select.""" + + entity_description: SensiboSelectEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboSelectEntityDescription, + ) -> None: + """Initiate Sensibo Select.""" + super().__init__(coordinator, device_id) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = ( + f"{coordinator.data.parsed[device_id]['name']} {entity_description.name}" + ) + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + return self.device_data[self.entity_description.remote_key] + + @property + def options(self) -> list[str]: + """Return possible options.""" + return self.device_data[self.entity_description.remote_options] or [] + + async def async_select_option(self, option: str) -> None: + """Set state to the selected option.""" + if self.entity_description.key not in self.device_data["active_features"]: + raise HomeAssistantError( + f"Current mode {self.device_data['hvac_mode']} doesn't support setting {self.entity_description.name}" + ) + + params = { + "name": self.entity_description.key, + "value": option, + "ac_states": self.device_data["ac_states"], + "assumed_state": False, + } + result = await self.async_send_command("set_ac_state", params) + + if result["result"]["status"] == "Success": + self.device_data[self.entity_description.remote_key] = option + self.async_write_ha_state() + return + + failure = result["result"]["failureReason"] + raise HomeAssistantError( + f"Could not set state for device {self.name} due to reason {failure}" + ) From 5d2a65bb2116b21b8d157956ea42954ff2586d97 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Fri, 11 Mar 2022 21:32:47 -0500 Subject: [PATCH 0367/1054] Add binary sensors to Mazda integration (#64604) --- homeassistant/components/mazda/__init__.py | 8 +- .../components/mazda/binary_sensor.py | 135 ++++++++++++++++++ homeassistant/components/mazda/sensor.py | 5 +- .../fixtures/diagnostics_config_entry.json | 4 +- .../mazda/fixtures/diagnostics_device.json | 4 +- .../mazda/fixtures/get_vehicle_status.json | 4 +- tests/components/mazda/test_binary_sensor.py | 98 +++++++++++++ tests/components/mazda/test_button.py | 2 +- tests/components/mazda/test_device_tracker.py | 2 +- tests/components/mazda/test_init.py | 3 +- tests/components/mazda/test_lock.py | 2 +- tests/components/mazda/test_sensor.py | 2 +- 12 files changed, 255 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/mazda/binary_sensor.py create mode 100644 tests/components/mazda/test_binary_sensor.py diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 38054bc653e..1fd0c3bd618 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -37,7 +37,13 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICE _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.LOCK, + Platform.SENSOR, +] async def with_timeout(task, timeout_seconds=10): diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py new file mode 100644 index 00000000000..cc60d6318c7 --- /dev/null +++ b/homeassistant/components/mazda/binary_sensor.py @@ -0,0 +1,135 @@ +"""Platform for Mazda binary sensor integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +@dataclass +class MazdaBinarySensorRequiredKeysMixin: + """Mixin for required keys.""" + + # Suffix to be appended to the vehicle name to obtain the binary sensor name + name_suffix: str + + # Function to determine the value for this binary sensor, given the coordinator data + value_fn: Callable[[dict[str, Any]], bool] + + +@dataclass +class MazdaBinarySensorEntityDescription( + BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin +): + """Describes a Mazda binary sensor entity.""" + + # Function to determine whether the vehicle supports this binary sensor, given the coordinator data + is_supported: Callable[[dict[str, Any]], bool] = lambda data: True + + +def _plugged_in_supported(data): + """Determine if 'plugged in' binary sensor is supported.""" + return ( + data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None + ) + + +BINARY_SENSOR_ENTITIES = [ + MazdaBinarySensorEntityDescription( + key="driver_door", + name_suffix="Driver Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="passenger_door", + name_suffix="Passenger Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="rear_left_door", + name_suffix="Rear Left Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="rear_right_door", + name_suffix="Rear Right Door", + icon="mdi:car-door", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], + ), + MazdaBinarySensorEntityDescription( + key="trunk", + name_suffix="Trunk", + icon="mdi:car-back", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["trunkOpen"], + ), + MazdaBinarySensorEntityDescription( + key="hood", + name_suffix="Hood", + icon="mdi:car", + device_class=BinarySensorDeviceClass.DOOR, + value_fn=lambda data: data["status"]["doors"]["hoodOpen"], + ), + MazdaBinarySensorEntityDescription( + key="ev_plugged_in", + name_suffix="Plugged In", + device_class=BinarySensorDeviceClass.PLUG, + is_supported=_plugged_in_supported, + value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + MazdaBinarySensorEntity(client, coordinator, index, description) + for index, data in enumerate(coordinator.data) + for description in BINARY_SENSOR_ENTITIES + if description.is_supported(data) + ) + + +class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity): + """Representation of a Mazda vehicle binary sensor.""" + + entity_description: MazdaBinarySensorEntityDescription + + def __init__(self, client, coordinator, index, description): + """Initialize Mazda binary sensor.""" + super().__init__(client, coordinator, index) + self.entity_description = description + + self._attr_name = f"{self.vehicle_name} {description.name_suffix}" + self._attr_unique_id = f"{self.vin}_{description.key}" + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self.entity_description.value_fn(self.data) diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index d94a4798630..99f8c74d64d 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -35,7 +36,7 @@ class MazdaSensorRequiredKeysMixin: name_suffix: str # Function to determine the value for this sensor, given the coordinator data and the configured unit system - value: Callable[[dict, UnitSystem], StateType] + value: Callable[[dict[str, Any], UnitSystem], StateType] @dataclass @@ -45,7 +46,7 @@ class MazdaSensorEntityDescription( """Describes a Mazda sensor entity.""" # Function to determine whether the vehicle supports this sensor, given the coordinator data - is_supported: Callable[[dict], bool] = lambda data: True + is_supported: Callable[[dict[str, Any]], bool] = lambda data: True # Function to determine the unit of measurement for this sensor, given the configured unit system # Falls back to description.native_unit_of_measurement if it is not provided diff --git a/tests/components/mazda/fixtures/diagnostics_config_entry.json b/tests/components/mazda/fixtures/diagnostics_config_entry.json index 445ae141cb2..e6319b3c43c 100644 --- a/tests/components/mazda/fixtures/diagnostics_config_entry.json +++ b/tests/components/mazda/fixtures/diagnostics_config_entry.json @@ -30,11 +30,11 @@ "odometerKm": 2795.8, "doors": { "driverDoorOpen": false, - "passengerDoorOpen": false, + "passengerDoorOpen": true, "rearLeftDoorOpen": false, "rearRightDoorOpen": false, "trunkOpen": false, - "hoodOpen": false, + "hoodOpen": true, "fuelLidOpen": false }, "doorLocks": { diff --git a/tests/components/mazda/fixtures/diagnostics_device.json b/tests/components/mazda/fixtures/diagnostics_device.json index 0b2fa8550ac..cbfa1ec8397 100644 --- a/tests/components/mazda/fixtures/diagnostics_device.json +++ b/tests/components/mazda/fixtures/diagnostics_device.json @@ -29,11 +29,11 @@ "odometerKm": 2795.8, "doors": { "driverDoorOpen": false, - "passengerDoorOpen": false, + "passengerDoorOpen": true, "rearLeftDoorOpen": false, "rearRightDoorOpen": false, "trunkOpen": false, - "hoodOpen": false, + "hoodOpen": true, "fuelLidOpen": false }, "doorLocks": { diff --git a/tests/components/mazda/fixtures/get_vehicle_status.json b/tests/components/mazda/fixtures/get_vehicle_status.json index 1e74d7202ca..70a8a4bf7cc 100644 --- a/tests/components/mazda/fixtures/get_vehicle_status.json +++ b/tests/components/mazda/fixtures/get_vehicle_status.json @@ -8,11 +8,11 @@ "odometerKm": 2795.8, "doors": { "driverDoorOpen": false, - "passengerDoorOpen": false, + "passengerDoorOpen": true, "rearLeftDoorOpen": false, "rearRightDoorOpen": false, "trunkOpen": false, - "hoodOpen": false, + "hoodOpen": true, "fuelLidOpen": false }, "doorLocks": { diff --git a/tests/components/mazda/test_binary_sensor.py b/tests/components/mazda/test_binary_sensor.py new file mode 100644 index 00000000000..f2b272109c9 --- /dev/null +++ b/tests/components/mazda/test_binary_sensor.py @@ -0,0 +1,98 @@ +"""The binary sensor tests for the Mazda Connected Services integration.""" + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.helpers import entity_registry as er + +from . import init_integration + + +async def test_binary_sensors(hass): + """Test creation of the binary sensors.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + + # Driver Door + state = hass.states.get("binary_sensor.my_mazda3_driver_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_driver_door") + assert entry + assert entry.unique_id == "JM000000000000000_driver_door" + + # Passenger Door + state = hass.states.get("binary_sensor.my_mazda3_passenger_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "on" + entry = entity_registry.async_get("binary_sensor.my_mazda3_passenger_door") + assert entry + assert entry.unique_id == "JM000000000000000_passenger_door" + + # Rear Left Door + state = hass.states.get("binary_sensor.my_mazda3_rear_left_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_left_door") + assert entry + assert entry.unique_id == "JM000000000000000_rear_left_door" + + # Rear Right Door + state = hass.states.get("binary_sensor.my_mazda3_rear_right_door") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Door" + assert state.attributes.get(ATTR_ICON) == "mdi:car-door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_rear_right_door") + assert entry + assert entry.unique_id == "JM000000000000000_rear_right_door" + + # Trunk + state = hass.states.get("binary_sensor.my_mazda3_trunk") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Trunk" + assert state.attributes.get(ATTR_ICON) == "mdi:car-back" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "off" + entry = entity_registry.async_get("binary_sensor.my_mazda3_trunk") + assert entry + assert entry.unique_id == "JM000000000000000_trunk" + + # Hood + state = hass.states.get("binary_sensor.my_mazda3_hood") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Hood" + assert state.attributes.get(ATTR_ICON) == "mdi:car" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR + assert state.state == "on" + entry = entity_registry.async_get("binary_sensor.my_mazda3_hood") + assert entry + assert entry.unique_id == "JM000000000000000_hood" + + +async def test_electric_vehicle_binary_sensors(hass): + """Test sensors which are specific to electric vehicles.""" + + await init_integration(hass, electric_vehicle=True) + + entity_registry = er.async_get(hass) + + # Plugged In + state = hass.states.get("binary_sensor.my_mazda3_plugged_in") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged In" + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG + assert state.state == "on" + entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in") + assert entry + assert entry.unique_id == "JM000000000000000_ev_plugged_in" diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py index cb9fdb40737..71b16434ee3 100644 --- a/tests/components/mazda/test_button.py +++ b/tests/components/mazda/test_button.py @@ -8,7 +8,7 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.components.mazda import init_integration +from . import init_integration async def test_button_setup_non_electric_vehicle(hass) -> None: diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index 5e09c23ecd8..4af367c1c04 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -9,7 +9,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import entity_registry as er -from tests.components.mazda import init_integration +from . import init_integration async def test_device_tracker(hass): diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index e2d4661d36f..9d221bbfe88 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -20,8 +20,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util +from . import init_integration + from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture -from tests.components.mazda import init_integration FIXTURE_USER_INPUT = { CONF_EMAIL: "example@example.com", diff --git a/tests/components/mazda/test_lock.py b/tests/components/mazda/test_lock.py index 1230e624cdd..9f0959dc88b 100644 --- a/tests/components/mazda/test_lock.py +++ b/tests/components/mazda/test_lock.py @@ -9,7 +9,7 @@ from homeassistant.components.lock import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME from homeassistant.helpers import entity_registry as er -from tests.components.mazda import init_integration +from . import init_integration async def test_lock_setup(hass): diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 8d4085930dd..f7fb379da51 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.components.mazda import init_integration +from . import init_integration async def test_sensors(hass): From 03ec77fb6263285c57313ce25ea6b4ec7fe59d5d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 12 Mar 2022 05:13:46 +0100 Subject: [PATCH 0368/1054] Motion blinds support direct wifi blinds (#67372) * bump motionblinds to 0.6.0 * fix unknown type * fix name for wifi direct blinds * push motionblinds to 0.6.1 * fix RSSI sensor * Do not add singnal sensor twice for direct WiFi blinds * fix device registry * fix typo * missing import * fix styling * fix spelling --- .../components/motion_blinds/__init__.py | 35 ++++----- .../components/motion_blinds/const.py | 1 + .../components/motion_blinds/cover.py | 71 +++++++++++++++---- .../components/motion_blinds/manifest.json | 2 +- .../components/motion_blinds/sensor.py | 41 +++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 109 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index b3712e6e832..b60de49044d 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -4,7 +4,7 @@ import logging from socket import timeout from typing import TYPE_CHECKING -from motionblinds import AsyncMotionMulticast, ParseException +from motionblinds import DEVICE_TYPES_WIFI, AsyncMotionMulticast, ParseException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP @@ -23,6 +23,7 @@ from .const import ( KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, + KEY_VERSION, MANUFACTURER, PLATFORMS, UPDATE_INTERVAL, @@ -147,29 +148,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = { - KEY_GATEWAY: motion_gateway, - KEY_COORDINATOR: coordinator, - } - if motion_gateway.firmware is not None: version = f"{motion_gateway.firmware}, protocol: {motion_gateway.protocol}" else: version = f"Protocol: {motion_gateway.protocol}" + hass.data[DOMAIN][entry.entry_id] = { + KEY_GATEWAY: motion_gateway, + KEY_COORDINATOR: coordinator, + KEY_VERSION: version, + } + if TYPE_CHECKING: assert entry.unique_id is not None - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)}, - identifiers={(DOMAIN, motion_gateway.mac)}, - manufacturer=MANUFACTURER, - name=entry.title, - model="Wi-Fi bridge", - sw_version=version, - ) + if motion_gateway.device_type not in DEVICE_TYPES_WIFI: + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)}, + identifiers={(DOMAIN, motion_gateway.mac)}, + manufacturer=MANUFACTURER, + name=entry.title, + model="Wi-Fi bridge", + sw_version=version, + ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index 01f74c4ef4d..696a00ff79e 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -15,6 +15,7 @@ DEFAULT_INTERFACE = "any" KEY_GATEWAY = "gateway" KEY_COORDINATOR = "coordinator" KEY_MULTICAST_LISTENER = "multicast_listener" +KEY_VERSION = "version" ATTR_WIDTH = "width" ATTR_ABSOLUTE_POSITION = "absolute_position" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 9bc952a21ea..ffb2a03ddc6 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,7 +1,7 @@ """Support for Motion Blinds using their WLAN API.""" import logging -from motionblinds import BlindType +from motionblinds import DEVICE_TYPES_WIFI, BlindType import voluptuous as vol from homeassistant.components.cover import ( @@ -12,7 +12,11 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_platform, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -24,6 +28,7 @@ from .const import ( DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, + KEY_VERSION, MANUFACTURER, SERVICE_SET_ABSOLUTE_POSITION, ) @@ -74,26 +79,40 @@ async def async_setup_entry( entities = [] motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY] coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + sw_version = hass.data[DOMAIN][config_entry.entry_id][KEY_VERSION] for blind in motion_gateway.device_list.values(): if blind.type in POSITION_DEVICE_MAP: entities.append( MotionPositionDevice( - coordinator, blind, POSITION_DEVICE_MAP[blind.type], config_entry + coordinator, + blind, + POSITION_DEVICE_MAP[blind.type], + config_entry, + sw_version, ) ) elif blind.type in TILT_DEVICE_MAP: entities.append( MotionTiltDevice( - coordinator, blind, TILT_DEVICE_MAP[blind.type], config_entry + coordinator, + blind, + TILT_DEVICE_MAP[blind.type], + config_entry, + sw_version, ) ) elif blind.type in TDBU_DEVICE_MAP: entities.append( MotionTDBUDevice( - coordinator, blind, TDBU_DEVICE_MAP[blind.type], config_entry, "Top" + coordinator, + blind, + TDBU_DEVICE_MAP[blind.type], + config_entry, + sw_version, + "Top", ) ) entities.append( @@ -102,6 +121,7 @@ async def async_setup_entry( blind, TDBU_DEVICE_MAP[blind.type], config_entry, + sw_version, "Bottom", ) ) @@ -111,12 +131,25 @@ async def async_setup_entry( blind, TDBU_DEVICE_MAP[blind.type], config_entry, + sw_version, "Combined", ) ) else: - _LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type) + _LOGGER.warning( + "Blind type '%s' not yet supported, " "assuming RollerBlind", + blind.blind_type, + ) + entities.append( + MotionPositionDevice( + coordinator, + blind, + POSITION_DEVICE_MAP[BlindType.RollerBlind], + config_entry, + sw_version, + ) + ) async_add_entities(entities) @@ -131,22 +164,34 @@ async def async_setup_entry( class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Representation of a Motion Blind Device.""" - def __init__(self, coordinator, blind, device_class, config_entry): + def __init__(self, coordinator, blind, device_class, config_entry, sw_version): """Initialize the blind.""" super().__init__(coordinator) self._blind = blind self._config_entry = config_entry + if blind.device_type in DEVICE_TYPES_WIFI: + via_device = () + connections = {(dr.CONNECTION_NETWORK_MAC, blind.mac)} + name = blind.blind_type + else: + via_device = (DOMAIN, blind._gateway.mac) + connections = {} + name = f"{blind.blind_type}-{blind.mac[12:]}" + sw_version = None + self._attr_device_class = device_class - self._attr_name = f"{blind.blind_type}-{blind.mac[12:]}" + self._attr_name = name self._attr_unique_id = blind.mac self._attr_device_info = DeviceInfo( + connections=connections, identifiers={(DOMAIN, blind.mac)}, manufacturer=MANUFACTURER, model=blind.blind_type, - name=f"{blind.blind_type}-{blind.mac[12:]}", - via_device=(DOMAIN, blind._gateway.mac), + name=name, + via_device=via_device, + sw_version=sw_version, hw_version=blind.wireless_name, ) @@ -247,9 +292,11 @@ class MotionTiltDevice(MotionPositionDevice): class MotionTDBUDevice(MotionPositionDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" - def __init__(self, coordinator, blind, device_class, config_entry, motor): + def __init__( + self, coordinator, blind, device_class, config_entry, sw_version, motor + ): """Initialize the blind.""" - super().__init__(coordinator, blind, device_class, config_entry) + super().__init__(coordinator, blind, device_class, config_entry, sw_version) self._motor = motor self._motor_key = motor[0] self._attr_name = f"{blind.blind_type}-{motor}-{blind.mac[12:]}" diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index c904320d9af..40fd9cafad4 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.5.13"], + "requirements": ["motionblinds==0.6.1"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push", diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 4c0b4251bf2..03e3a6e7618 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -1,5 +1,5 @@ """Support for Motion Blinds sensors.""" -from motionblinds import BlindType +from motionblinds import DEVICE_TYPES_WIFI, BlindType from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -31,13 +31,15 @@ async def async_setup_entry( if blind.type == BlindType.TopDownBottomUp: entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom")) entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top")) - elif blind.battery_voltage > 0: + elif blind.battery_voltage is not None and blind.battery_voltage > 0: # Only add battery powered blinds entities.append(MotionBatterySensor(coordinator, blind)) - entities.append( - MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY) - ) + # Do not add signal sensor twice for direct WiFi blinds + if motion_gateway.device_type not in DEVICE_TYPES_WIFI: + entities.append( + MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY) + ) async_add_entities(entities) @@ -52,9 +54,14 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator) + if blind.device_type in DEVICE_TYPES_WIFI: + name = f"{blind.blind_type}-battery" + else: + name = f"{blind.blind_type}-battery-{blind.mac[12:]}" + self._blind = blind self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, blind.mac)}) - self._attr_name = f"{blind.blind_type}-battery-{blind.mac[12:]}" + self._attr_name = name self._attr_unique_id = f"{blind.mac}-battery" @property @@ -96,9 +103,14 @@ class MotionTDBUBatterySensor(MotionBatterySensor): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator, blind) + if blind.device_type in DEVICE_TYPES_WIFI: + name = f"{blind.blind_type}-{motor}-battery" + else: + name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" + self._motor = motor self._attr_unique_id = f"{blind.mac}-{motor}-battery" - self._attr_name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" + self._attr_name = name @property def native_value(self): @@ -130,17 +142,18 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Initialize the Motion Signal Strength Sensor.""" super().__init__(coordinator) + if device_type == TYPE_GATEWAY: + name = "Motion gateway signal strength" + elif device.device_type in DEVICE_TYPES_WIFI: + name = f"{device.blind_type} signal strength" + else: + name = f"{device.blind_type} signal strength - {device.mac[12:]}" + self._device = device self._device_type = device_type self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device.mac)}) self._attr_unique_id = f"{device.mac}-RSSI" - - @property - def name(self): - """Return the name of the blind signal strength sensor.""" - if self._device_type == TYPE_GATEWAY: - return "Motion gateway signal strength" - return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}" + self._attr_name = name @property def available(self): diff --git a/requirements_all.txt b/requirements_all.txt index 744a1973215..fa3e8e745c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1011,7 +1011,7 @@ mitemp_bt==0.0.5 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.5.13 +motionblinds==0.6.1 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 893530eab42..f67bac50270 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -671,7 +671,7 @@ minio==5.0.10 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.5.13 +motionblinds==0.6.1 # homeassistant.components.motioneye motioneye-client==0.3.12 From 09a85d2a5df5a452223ece6f86434f8b5e3fcce8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 12 Mar 2022 05:27:08 +0100 Subject: [PATCH 0369/1054] Add basic rfxtrx diagnostics (#67671) * Add basic rfxtrx diagnostics * Skip diagnostics for coverage --- .coveragerc | 1 + homeassistant/components/rfxtrx/__init__.py | 3 +-- homeassistant/components/rfxtrx/const.py | 2 ++ .../components/rfxtrx/diagnostics.py | 19 +++++++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/rfxtrx/diagnostics.py diff --git a/.coveragerc b/.coveragerc index f48943388c3..95856947fb6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -960,6 +960,7 @@ omit = homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py + homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py homeassistant/components/ridwell/sensor.py homeassistant/components/ridwell/switch.py diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index d2afda81f9f..ba4cb9eb226 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -42,12 +42,11 @@ from .const import ( CONF_PROTOCOLS, DATA_RFXOBJECT, DEVICE_PACKET_TYPE_LIGHTING4, + DOMAIN, EVENT_RFXTRX_EVENT, SERVICE_SEND, ) -DOMAIN = "rfxtrx" - DEFAULT_OFF_DELAY = 2.0 SIGNAL_EVENT = f"{DOMAIN}_event" diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 529289090a6..7a6e333d3db 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -44,3 +44,5 @@ DEVICE_PACKET_TYPE_LIGHTING4 = 0x13 EVENT_RFXTRX_EVENT = "rfxtrx_event" DATA_RFXOBJECT = "rfxobject" + +DOMAIN = "rfxtrx" diff --git a/homeassistant/components/rfxtrx/diagnostics.py b/homeassistant/components/rfxtrx/diagnostics.py new file mode 100644 index 00000000000..bc2fae2452d --- /dev/null +++ b/homeassistant/components/rfxtrx/diagnostics.py @@ -0,0 +1,19 @@ +"""Diagnostics support for RFXCOM RFXtrx.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +TO_REDACT = {"host"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + return { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + } From 09944d936de81a94378f9577841056b6329c7564 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 11 Mar 2022 23:29:18 -0500 Subject: [PATCH 0370/1054] Clean up Efergy (#67755) * Clean up Efergy * tweak --- homeassistant/components/efergy/__init__.py | 27 ++++++++----------- .../components/efergy/config_flow.py | 7 ++--- homeassistant/components/efergy/const.py | 9 ++++--- homeassistant/components/efergy/sensor.py | 11 +++----- homeassistant/components/efergy/strings.json | 1 - .../components/efergy/translations/en.json | 3 +-- 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 372dbe77e75..915eb0daf46 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -4,14 +4,14 @@ from __future__ import annotations from pyefergy import Efergy, exceptions from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, Platform +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ATTRIBUTION, DATA_KEY_API, DEFAULT_NAME, DOMAIN +from .const import DEFAULT_NAME, DOMAIN PLATFORMS = [Platform.SENSOR] @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "API Key is no longer valid. Please reauthenticate" ) from ex - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -42,8 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -51,21 +50,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class EfergyEntity(Entity): """Representation of a Efergy entity.""" - def __init__( - self, - api: Efergy, - server_unique_id: str, - ) -> None: + _attr_attribution = "Data provided by Efergy" + + def __init__(self, api: Efergy, server_unique_id: str) -> None: """Initialize an Efergy entity.""" self.api = api - self._server_unique_id = server_unique_id - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_device_info = DeviceInfo( configuration_url="https://engage.efergy.com/user/login", - connections={(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])}, - identifiers={(DOMAIN, self._server_unique_id)}, + connections={(dr.CONNECTION_NETWORK_MAC, api.info["mac"])}, + identifiers={(DOMAIN, server_unique_id)}, manufacturer=DEFAULT_NAME, name=DEFAULT_NAME, - model=self.api.info["type"], - sw_version=self.api.info["version"], + model=api.info["type"], + sw_version=api.info["version"], ) diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index 8283bfce62d..5ff6e9ba9f2 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Efergy integration.""" from __future__ import annotations -import logging from typing import Any from pyefergy import Efergy, exceptions @@ -12,9 +11,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEFAULT_NAME, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_NAME, DOMAIN, LOGGER class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -69,6 +66,6 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except exceptions.InvalidAuth: return None, "invalid_auth" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") return None, "unknown" return api.info["hid"], None diff --git a/homeassistant/components/efergy/const.py b/homeassistant/components/efergy/const.py index f5e9af6a4c8..5a0ca11693b 100644 --- a/homeassistant/components/efergy/const.py +++ b/homeassistant/components/efergy/const.py @@ -1,12 +1,13 @@ """Constants for the Efergy integration.""" from datetime import timedelta - -ATTRIBUTION = "Data provided by Efergy" +import logging +from typing import Final CONF_CURRENT_VALUES = "current_values" -DATA_KEY_API = "api" DEFAULT_NAME = "Efergy" -DOMAIN = "efergy" +DOMAIN: Final = "efergy" + +LOGGER = logging.getLogger(__package__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 00a10b713d2..abe34a21bcc 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -1,7 +1,6 @@ """Support for Efergy sensors.""" from __future__ import annotations -import logging from re import sub from typing import cast @@ -21,9 +20,7 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.typing import StateType from . import EfergyEntity -from .const import CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import CONF_CURRENT_VALUES, DOMAIN, LOGGER SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -112,7 +109,7 @@ async def async_setup_entry( async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up Efergy sensors.""" - api: Efergy = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API] + api: Efergy = hass.data[DOMAIN][entry.entry_id] sensors = [] for description in SENSOR_TYPES: if description.key != CONF_CURRENT_VALUES: @@ -174,8 +171,8 @@ class EfergySensor(EfergyEntity, SensorEntity): except (ConnectError, DataError, ServiceError) as ex: if self._attr_available: self._attr_available = False - _LOGGER.error("Error getting data: %s", ex) + LOGGER.error("Error getting data: %s", ex) return if not self._attr_available: self._attr_available = True - _LOGGER.info("Connection has resumed") + LOGGER.info("Connection has resumed") diff --git a/homeassistant/components/efergy/strings.json b/homeassistant/components/efergy/strings.json index dc625c92840..924d5a56bcf 100644 --- a/homeassistant/components/efergy/strings.json +++ b/homeassistant/components/efergy/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Efergy", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/homeassistant/components/efergy/translations/en.json b/homeassistant/components/efergy/translations/en.json index aa76f9c0636..0909241c423 100644 --- a/homeassistant/components/efergy/translations/en.json +++ b/homeassistant/components/efergy/translations/en.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API Key" - }, - "title": "Efergy" + } } } } From d3deae6288a2d2703f0824b184be1d1d07a227ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Mar 2022 18:31:03 -1000 Subject: [PATCH 0371/1054] Fix typing on Context.user_id (#68019) --- homeassistant/components/google_assistant/smart_home.py | 1 + homeassistant/core.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index f2aea247ecd..805c9100d9f 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -248,6 +248,7 @@ async def async_devices_disconnect(hass, data: RequestData, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ + assert data.context.user_id is not None await data.config.async_disconnect_agent_user(data.context.user_id) return None diff --git a/homeassistant/core.py b/homeassistant/core.py index 27dba3cbc52..8c662afce48 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -701,7 +701,7 @@ class HomeAssistant: class Context: """The context that triggered something.""" - user_id: str = attr.ib(default=None) + user_id: str | None = attr.ib(default=None) parent_id: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) From c5800d6103573d1245d9cc187c3a4a36c4c0ecdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 12 Mar 2022 05:32:05 +0100 Subject: [PATCH 0372/1054] Split out sync functions in backup manager (#67428) --- homeassistant/components/backup/manager.py | 96 ++++++++++++---------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index bc037287c09..0344ffa4543 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -8,6 +8,7 @@ from pathlib import Path import tarfile from tarfile import TarError from tempfile import TemporaryDirectory +from typing import Any from securetar import SecureTarFile, atomic_contents_add @@ -47,30 +48,31 @@ class BackupManager: async def load_backups(self) -> None: """Load data of stored backup files.""" - backups = {} - - def _read_backups() -> None: - for backup_path in self.backup_dir.glob("*.tar"): - try: - with tarfile.open(backup_path, "r:") as backup_file: - if data_file := backup_file.extractfile("./backup.json"): - data = json.loads(data_file.read()) - backup = Backup( - slug=data["slug"], - name=data["name"], - date=data["date"], - path=backup_path, - size=round(backup_path.stat().st_size / 1_048_576, 2), - ) - backups[backup.slug] = backup - except (OSError, TarError, json.JSONDecodeError) as err: - LOGGER.warning("Unable to read backup %s: %s", backup_path, err) - - await self.hass.async_add_executor_job(_read_backups) + backups = await self.hass.async_add_executor_job(self._read_backups) LOGGER.debug("Loaded %s backups", len(backups)) self.backups = backups self.loaded = True + def _read_backups(self) -> dict[str, Backup]: + """Read backups from disk.""" + backups: dict[str, Backup] = {} + for backup_path in self.backup_dir.glob("*.tar"): + try: + with tarfile.open(backup_path, "r:") as backup_file: + if data_file := backup_file.extractfile("./backup.json"): + data = json.loads(data_file.read()) + backup = Backup( + slug=data["slug"], + name=data["name"], + date=data["date"], + path=backup_path, + size=round(backup_path.stat().st_size / 1_048_576, 2), + ) + backups[backup.slug] = backup + except (OSError, TarError, json.JSONDecodeError) as err: + LOGGER.warning("Unable to read backup %s: %s", backup_path, err) + return backups + async def get_backups(self) -> dict[str, Backup]: """Return backups.""" if not self.loaded: @@ -126,33 +128,17 @@ class BackupManager: "homeassistant": {"version": HAVERSION}, "compressed": True, } - tar_file_path = Path(self.backup_dir, f"{slug}.tar") + tar_file_path = Path(self.backup_dir, f"{backup_data['slug']}.tar") if not self.backup_dir.exists(): LOGGER.debug("Creating backup directory") self.hass.async_add_executor_job(self.backup_dir.mkdir) - def _create_backup() -> None: - with TemporaryDirectory() as tmp_dir: - tmp_dir_path = Path(tmp_dir) - json_util.save_json( - tmp_dir_path.joinpath("./backup.json").as_posix(), - backup_data, - ) - with SecureTarFile(tar_file_path, "w", gzip=False) as tar_file: - with SecureTarFile( - tmp_dir_path.joinpath("./homeassistant.tar.gz").as_posix(), - "w", - ) as core_tar: - atomic_contents_add( - tar_file=core_tar, - origin_path=Path(self.hass.config.path()), - excludes=EXCLUDE_FROM_BACKUP, - arcname="data", - ) - tar_file.add(tmp_dir_path, arcname=".") - - await self.hass.async_add_executor_job(_create_backup) + await self.hass.async_add_executor_job( + self._generate_backup_contents, + tar_file_path, + backup_data, + ) backup = Backup( slug=slug, name=backup_name, @@ -167,6 +153,32 @@ class BackupManager: finally: self.backing_up = False + def _generate_backup_contents( + self, + tar_file_path: Path, + backup_data: dict[str, Any], + ) -> None: + """Generate backup contents.""" + with TemporaryDirectory() as tmp_dir, SecureTarFile( + tar_file_path, "w", gzip=False + ) as tar_file: + tmp_dir_path = Path(tmp_dir) + json_util.save_json( + tmp_dir_path.joinpath("./backup.json").as_posix(), + backup_data, + ) + with SecureTarFile( + tmp_dir_path.joinpath("./homeassistant.tar.gz").as_posix(), + "w", + ) as core_tar: + atomic_contents_add( + tar_file=core_tar, + origin_path=Path(self.hass.config.path()), + excludes=EXCLUDE_FROM_BACKUP, + arcname="data", + ) + tar_file.add(tmp_dir_path, arcname=".") + def _generate_slug(date: str, name: str) -> str: """Generate a backup slug.""" From c8351387d739f5bd8890e3fb0e4c39e8791ed34e Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Sat, 12 Mar 2022 05:34:06 +0100 Subject: [PATCH 0373/1054] Print client error in rest_command (#67900) --- homeassistant/components/rest_command/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index f9174743f3c..70f887cf896 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -144,8 +144,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: except asyncio.TimeoutError: _LOGGER.warning("Timeout call %s", request_url) - except aiohttp.ClientError: - _LOGGER.error("Client error %s", request_url) + except aiohttp.ClientError as err: + _LOGGER.error( + "Client error. Url: %s. Error: %s", + request_url, + err, + ) # register services hass.services.async_register(DOMAIN, name, async_service_handler) From 6526b4eae5519639568f584eb4dcdcb8ee5b7ac7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 12 Mar 2022 05:35:25 +0100 Subject: [PATCH 0374/1054] Add typing to deconz_device (#67501) --- .../components/deconz/deconz_device.py | 34 +++++++++++++------ homeassistant/components/deconz/gateway.py | 6 ++-- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 45f57729a6f..f429faf54ad 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -1,7 +1,10 @@ """Base class for deCONZ devices.""" + from __future__ import annotations -from pydeconz.group import Scene as PydeconzScene +from pydeconz.group import Group as DeconzGroup, Scene as PydeconzScene +from pydeconz.light import DeconzLight +from pydeconz.sensor import DeconzSensor from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -9,25 +12,30 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN +from .gateway import DeconzGateway class DeconzBase: """Common base for deconz entities and events.""" - def __init__(self, device, gateway): + def __init__( + self, + device: DeconzGroup | DeconzLight | DeconzSensor, + gateway: DeconzGateway, + ) -> None: """Set up device and add update callback to get data from websocket.""" self._device = device self.gateway = gateway @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique identifier for this device.""" return self._device.unique_id @property - def serial(self): + def serial(self) -> str | None: """Return a serial number for this device.""" - if self._device.unique_id is None or self._device.unique_id.count(":") != 7: + if not self._device.unique_id or self._device.unique_id.count(":") != 7: return None return self._device.unique_id.split("-", 1)[0] @@ -56,14 +64,18 @@ class DeconzDevice(DeconzBase, Entity): TYPE = "" - def __init__(self, device, gateway): + def __init__( + self, + device: DeconzGroup | DeconzLight | DeconzSensor, + gateway: DeconzGateway, + ) -> None: """Set up device and add update callback to get data from websocket.""" super().__init__(device, gateway) self.gateway.entities[self.TYPE].add(self.unique_id) self._attr_name = self._device.name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to device events.""" self._device.register_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id @@ -82,12 +94,12 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.entities[self.TYPE].remove(self.unique_id) @callback - def async_update_connection_state(self): + def async_update_connection_state(self) -> None: """Update the device's available state.""" self.async_write_ha_state() @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Update the device's state.""" if self.gateway.ignore_state_updates: return @@ -95,7 +107,7 @@ class DeconzDevice(DeconzBase, Entity): self.async_write_ha_state() @property - def available(self): + def available(self) -> bool: """Return True if device is available.""" return self.gateway.available and self._device.reachable @@ -105,7 +117,7 @@ class DeconzSceneMixin(DeconzDevice): _device: PydeconzScene - def __init__(self, device, gateway) -> None: + def __init__(self, device: PydeconzScene, gateway: DeconzGateway) -> None: """Set up a scene.""" super().__init__(device, gateway) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 3bd0278a27a..821f3f477ab 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from types import MappingProxyType -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors, group, light, sensor @@ -37,9 +37,11 @@ from .const import ( LOGGER, PLATFORMS, ) -from .deconz_event import DeconzAlarmEvent, DeconzEvent from .errors import AuthenticationRequired, CannotConnect +if TYPE_CHECKING: + from .deconz_event import DeconzAlarmEvent, DeconzEvent + class DeconzGateway: """Manages a single deCONZ gateway.""" From 0d8f649bd65c8c54cd3503dd75485d3ec35d6076 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Mar 2022 18:54:49 -1000 Subject: [PATCH 0375/1054] Websocket api to subscribe to entities (payloads reduced by ~80%+ vs state_changed events) (#67891) --- .../components/websocket_api/commands.py | 105 ++++- .../components/websocket_api/messages.py | 119 +++++- .../components/websocket_api/test_commands.py | 378 +++++++++++++++++- 3 files changed, 590 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 6044e55978d..e64ba46beb7 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -16,7 +16,7 @@ from homeassistant.const import ( MATCH_ALL, SIGNAL_BOOTSTRAP_INTEGRATONS, ) -from homeassistant.core import Context, Event, HomeAssistant, callback +from homeassistant.core import Context, Event, HomeAssistant, State, callback from homeassistant.exceptions import ( HomeAssistantError, ServiceNotFound, @@ -68,6 +68,7 @@ def async_register_commands( async_reg(hass, handle_test_condition) async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_validate_config) + async_reg(hass, handle_subscribe_entities) def pong_message(iden: int) -> dict[str, Any]: @@ -213,21 +214,27 @@ async def handle_call_service( connection.send_error(msg["id"], const.ERR_UNKNOWN_ERROR, str(err)) +@callback +def _async_get_allowed_states( + hass: HomeAssistant, connection: ActiveConnection +) -> list[State]: + if connection.user.permissions.access_all_entities("read"): + return hass.states.async_all() + entity_perm = connection.user.permissions.check_entity + return [ + state + for state in hass.states.async_all() + if entity_perm(state.entity_id, "read") + ] + + @callback @decorators.websocket_command({vol.Required("type"): "get_states"}) def handle_get_states( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle get states command.""" - if connection.user.permissions.access_all_entities("read"): - states = hass.states.async_all() - else: - entity_perm = connection.user.permissions.check_entity - states = [ - state - for state in hass.states.async_all() - if entity_perm(state.entity_id, "read") - ] + states = _async_get_allowed_states(hass, connection) # JSON serialize here so we can recover if it blows up due to the # state machine containing unserializable data. This command is required @@ -260,6 +267,84 @@ def handle_get_states( connection.send_message(response2) +@callback +@decorators.websocket_command( + { + vol.Required("type"): "subscribe_entities", + vol.Optional("entity_ids"): cv.entity_ids, + } +) +def handle_subscribe_entities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle subscribe entities command.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from .permissions import SUBSCRIBE_ALLOWLIST + + if "state_changed" not in SUBSCRIBE_ALLOWLIST and not connection.user.is_admin: + raise Unauthorized + + entity_ids = set(msg.get("entity_ids", [])) + + @callback + def forward_entity_changes(event: Event) -> None: + """Forward entity state changed events to websocket.""" + if not connection.user.permissions.check_entity( + event.data["entity_id"], POLICY_READ + ): + return + if entity_ids and event.data["entity_id"] not in entity_ids: + return + + connection.send_message(messages.cached_state_diff_message(msg["id"], event)) + + # We must never await between sending the states and listening for + # state changed events or we will introduce a race condition + # where some states are missed + states = _async_get_allowed_states(hass, connection) + connection.subscriptions[msg["id"]] = hass.bus.async_listen( + "state_changed", forward_entity_changes + ) + connection.send_result(msg["id"]) + data: dict[str, dict[str, dict]] = { + messages.ENTITY_EVENT_ADD: { + state.entity_id: messages.compressed_state_dict_add(state) + for state in states + if not entity_ids or state.entity_id in entity_ids + } + } + + # JSON serialize here so we can recover if it blows up due to the + # state machine containing unserializable data. This command is required + # to succeed for the UI to show. + response = messages.event_message(msg["id"], data) + try: + connection.send_message(const.JSON_DUMP(response)) + return + except (ValueError, TypeError): + connection.logger.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(response, dump=const.JSON_DUMP) + ), + ) + del response + + add_entities = data[messages.ENTITY_EVENT_ADD] + cannot_serialize: list[str] = [] + for entity_id, state_dict in add_entities.items(): + try: + const.JSON_DUMP(state_dict) + except (ValueError, TypeError): + cannot_serialize.append(entity_id) + + for entity_id in cannot_serialize: + del add_entities[entity_id] + + connection.send_message(const.JSON_DUMP(messages.event_message(msg["id"], data))) + + @decorators.websocket_command({vol.Required("type"): "get_services"}) @decorators.async_response async def handle_get_services( diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 8cdda3f8fa3..0695b279361 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,7 +7,7 @@ from typing import Any, Final import voluptuous as vol -from homeassistant.core import Event +from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv from homeassistant.util.json import ( find_paths_unserializable_data, @@ -31,6 +31,19 @@ BASE_COMMAND_MESSAGE_SCHEMA: Final = vol.Schema({vol.Required("id"): cv.positive IDEN_TEMPLATE: Final = "__IDEN__" IDEN_JSON_TEMPLATE: Final = '"__IDEN__"' +COMPRESSED_STATE_STATE = "s" +COMPRESSED_STATE_ATTRIBUTES = "a" +COMPRESSED_STATE_CONTEXT = "c" +COMPRESSED_STATE_LAST_CHANGED = "lc" +COMPRESSED_STATE_LAST_UPDATED = "lu" + +STATE_DIFF_ADDITIONS = "+" +STATE_DIFF_REMOVALS = "-" + +ENTITY_EVENT_ADD = "a" +ENTITY_EVENT_REMOVE = "r" +ENTITY_EVENT_CHANGE = "c" + def result_message(iden: int, result: Any = None) -> dict[str, Any]: """Return a success result message.""" @@ -74,6 +87,110 @@ def _cached_event_message(event: Event) -> str: return message_to_json(event_message(IDEN_TEMPLATE, event)) +def cached_state_diff_message(iden: int, event: Event) -> str: + """Return an event message. + + Serialize to json once per message. + + Since we can have many clients connected that are + all getting many of the same events (mostly state changed) + we can avoid serializing the same data for each connection. + """ + return _cached_state_diff_message(event).replace(IDEN_JSON_TEMPLATE, str(iden), 1) + + +@lru_cache(maxsize=128) +def _cached_state_diff_message(event: Event) -> str: + """Cache and serialize the event to json. + + The IDEN_TEMPLATE is used which will be replaced + with the actual iden in cached_event_message + """ + return message_to_json(event_message(IDEN_TEMPLATE, _state_diff_event(event))) + + +def _state_diff_event(event: Event) -> dict: + """Convert a state_changed event to the minimal version. + + State update example + + { + "a": {entity_id: compressed_state,…} + "c": {entity_id: diff,…} + "r": [entity_id,…] + } + """ + if (event_new_state := event.data["new_state"]) is None: + return {ENTITY_EVENT_REMOVE: [event.data["entity_id"]]} + assert isinstance(event_new_state, State) + if (event_old_state := event.data["old_state"]) is None: + return { + ENTITY_EVENT_ADD: { + event_new_state.entity_id: compressed_state_dict_add(event_new_state) + } + } + assert isinstance(event_old_state, State) + return _state_diff(event_old_state, event_new_state) + + +def _state_diff( + old_state: State, new_state: State +) -> dict[str, dict[str, dict[str, dict[str, str | list[str]]]]]: + """Create a diff dict that can be used to overlay changes.""" + diff: dict = {STATE_DIFF_ADDITIONS: {}} + additions = diff[STATE_DIFF_ADDITIONS] + if old_state.state != new_state.state: + additions[COMPRESSED_STATE_STATE] = new_state.state + if old_state.last_changed != new_state.last_changed: + additions[COMPRESSED_STATE_LAST_CHANGED] = new_state.last_changed.timestamp() + elif old_state.last_updated != new_state.last_updated: + additions[COMPRESSED_STATE_LAST_UPDATED] = new_state.last_updated.timestamp() + if old_state.context.parent_id != new_state.context.parent_id: + additions.setdefault(COMPRESSED_STATE_CONTEXT, {})[ + "parent_id" + ] = new_state.context.parent_id + if old_state.context.user_id != new_state.context.user_id: + additions.setdefault(COMPRESSED_STATE_CONTEXT, {})[ + "user_id" + ] = new_state.context.user_id + if old_state.context.id != new_state.context.id: + if COMPRESSED_STATE_CONTEXT in additions: + additions[COMPRESSED_STATE_CONTEXT]["id"] = new_state.context.id + else: + additions[COMPRESSED_STATE_CONTEXT] = new_state.context.id + old_attributes = old_state.attributes + for key, value in new_state.attributes.items(): + if old_attributes.get(key) != value: + additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value + if removed := set(old_attributes).difference(new_state.attributes): + diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} + return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} + + +def compressed_state_dict_add(state: State) -> dict[str, Any]: + """Build a compressed dict of a state for adds. + + Omits the lu (last_updated) if it matches (lc) last_changed. + + Sends c (context) as a string if it only contains an id. + """ + if state.context.parent_id is None and state.context.user_id is None: + context: dict[str, Any] | str = state.context.id # type: ignore[unreachable] + else: + context = state.context.as_dict() + compressed_state: dict[str, Any] = { + COMPRESSED_STATE_STATE: state.state, + COMPRESSED_STATE_ATTRIBUTES: state.attributes, + COMPRESSED_STATE_CONTEXT: context, + } + if state.last_changed == state.last_updated: + compressed_state[COMPRESSED_STATE_LAST_CHANGED] = state.last_changed.timestamp() + else: + compressed_state[COMPRESSED_STATE_LAST_CHANGED] = state.last_changed.timestamp() + compressed_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated.timestamp() + return compressed_state + + def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 742d9bddd38..007e130ff6f 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,4 +1,5 @@ """Tests for WebSocket API commands.""" +from copy import deepcopy import datetime from unittest.mock import ANY, patch @@ -14,7 +15,7 @@ from homeassistant.components.websocket_api.auth import ( ) from homeassistant.components.websocket_api.const import URL from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATONS -from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -23,6 +24,38 @@ from homeassistant.setup import DATA_SETUP_TIME, async_setup_component from tests.common import MockEntity, MockEntityPlatform, async_mock_service +STATE_KEY_SHORT_NAMES = { + "entity_id": "e", + "state": "s", + "last_changed": "lc", + "last_updated": "lu", + "context": "c", + "attributes": "a", +} +STATE_KEY_LONG_NAMES = {v: k for k, v in STATE_KEY_SHORT_NAMES.items()} + + +def _apply_entities_changes(state_dict: dict, change_dict: dict) -> None: + """Apply a diff set to a dict. + + Port of the client side merging + """ + additions = change_dict.get("+", {}) + if "lc" in additions: + additions["lu"] = additions["lc"] + if attributes := additions.pop("a", None): + state_dict["attributes"].update(attributes) + if context := additions.pop("c", None): + if isinstance(context, str): + state_dict["context"]["id"] = context + else: + state_dict["context"].update(context) + for k, v in additions.items(): + state_dict[STATE_KEY_LONG_NAMES[k]] = v + for key, items in change_dict.get("-", {}).items(): + for item in items: + del state_dict[STATE_KEY_LONG_NAMES[key]][item] + async def test_fire_event(hass, websocket_client): """Test fire event command.""" @@ -666,6 +699,349 @@ async def test_subscribe_unsubscribe_events_state_changed( assert msg["event"]["data"]["entity_id"] == "light.permitted" +async def test_subscribe_entities_with_unserializable_state( + hass, websocket_client, hass_admin_user +): + """Test subscribe entities with an unserializeable state.""" + + class CannotSerializeMe: + """Cannot serialize this.""" + + def __init__(self): + """Init cannot serialize this.""" + + hass.states.async_set("light.permitted", "off", {"color": "red"}) + hass.states.async_set( + "light.cannot_serialize", + "off", + {"color": "red", "cannot_serialize": CannotSerializeMe()}, + ) + original_state = hass.states.get("light.cannot_serialize") + assert isinstance(original_state, State) + state_dict = { + "attributes": dict(original_state.attributes), + "context": dict(original_state.context.as_dict()), + "entity_id": original_state.entity_id, + "last_changed": original_state.last_changed.isoformat(), + "last_updated": original_state.last_updated.isoformat(), + "state": original_state.state, + } + hass_admin_user.groups = [] + hass_admin_user.mock_policy( + { + "entities": { + "entity_ids": {"light.permitted": True, "light.cannot_serialize": True} + } + } + ) + + await websocket_client.send_json({"id": 7, "type": "subscribe_entities"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "a": { + "light.permitted": { + "a": {"color": "red"}, + "c": ANY, + "lc": ANY, + "s": "off", + } + } + } + hass.states.async_set("light.permitted", "on", {"effect": "help"}) + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": { + "light.permitted": { + "+": { + "a": {"effect": "help"}, + "c": ANY, + "lc": ANY, + "s": "on", + }, + "-": {"a": ["color"]}, + } + } + } + hass.states.async_set("light.cannot_serialize", "on", {"effect": "help"}) + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + # Order does not matter + msg["event"]["c"]["light.cannot_serialize"]["-"]["a"] = set( + msg["event"]["c"]["light.cannot_serialize"]["-"]["a"] + ) + assert msg["event"] == { + "c": { + "light.cannot_serialize": { + "+": {"a": {"effect": "help"}, "c": ANY, "lc": ANY, "s": "on"}, + "-": {"a": {"color", "cannot_serialize"}}, + } + } + } + change_set = msg["event"]["c"]["light.cannot_serialize"] + _apply_entities_changes(state_dict, change_set) + assert state_dict == { + "attributes": {"effect": "help"}, + "context": { + "id": ANY, + "parent_id": None, + "user_id": None, + }, + "entity_id": "light.cannot_serialize", + "last_changed": ANY, + "last_updated": ANY, + "state": "on", + } + hass.states.async_set( + "light.cannot_serialize", + "off", + {"color": "red", "cannot_serialize": CannotSerializeMe()}, + ) + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "result" + assert msg["error"] == { + "code": "unknown_error", + "message": "Invalid JSON in response", + } + + +async def test_subscribe_unsubscribe_entities(hass, websocket_client, hass_admin_user): + """Test subscribe/unsubscribe entities.""" + + hass.states.async_set("light.permitted", "off", {"color": "red"}) + original_state = hass.states.get("light.permitted") + assert isinstance(original_state, State) + state_dict = { + "attributes": dict(original_state.attributes), + "context": dict(original_state.context.as_dict()), + "entity_id": original_state.entity_id, + "last_changed": original_state.last_changed.isoformat(), + "last_updated": original_state.last_updated.isoformat(), + "state": original_state.state, + } + hass_admin_user.groups = [] + hass_admin_user.mock_policy({"entities": {"entity_ids": {"light.permitted": True}}}) + + await websocket_client.send_json({"id": 7, "type": "subscribe_entities"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert isinstance(msg["event"]["a"]["light.permitted"]["c"], str) + assert msg["event"] == { + "a": { + "light.permitted": { + "a": {"color": "red"}, + "c": ANY, + "lc": ANY, + "s": "off", + } + } + } + hass.states.async_set("light.not_permitted", "on") + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + hass.states.async_set("light.permitted", "on", {"effect": "help"}) + hass.states.async_set( + "light.permitted", "on", {"effect": "help", "color": ["blue", "green"]} + ) + hass.states.async_remove("light.permitted") + hass.states.async_set("light.permitted", "on", {"effect": "help", "color": "blue"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": { + "light.permitted": { + "+": { + "a": {"color": "blue"}, + "c": ANY, + "lc": ANY, + "s": "on", + } + } + } + } + + change_set = msg["event"]["c"]["light.permitted"] + additions = deepcopy(change_set["+"]) + _apply_entities_changes(state_dict, change_set) + assert state_dict == { + "attributes": {"color": "blue"}, + "context": { + "id": additions["c"], + "parent_id": None, + "user_id": None, + }, + "entity_id": "light.permitted", + "last_changed": additions["lc"], + "last_updated": additions["lc"], + "state": "on", + } + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": { + "light.permitted": { + "+": { + "a": {"effect": "help"}, + "c": ANY, + "lu": ANY, + }, + "-": {"a": ["color"]}, + } + } + } + + change_set = msg["event"]["c"]["light.permitted"] + additions = deepcopy(change_set["+"]) + _apply_entities_changes(state_dict, change_set) + + assert state_dict == { + "attributes": {"effect": "help"}, + "context": { + "id": additions["c"], + "parent_id": None, + "user_id": None, + }, + "entity_id": "light.permitted", + "last_changed": ANY, + "last_updated": additions["lu"], + "state": "on", + } + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": { + "light.permitted": { + "+": { + "a": {"color": ["blue", "green"]}, + "c": ANY, + "lu": ANY, + } + } + } + } + + change_set = msg["event"]["c"]["light.permitted"] + additions = deepcopy(change_set["+"]) + _apply_entities_changes(state_dict, change_set) + + assert state_dict == { + "attributes": {"effect": "help", "color": ["blue", "green"]}, + "context": { + "id": additions["c"], + "parent_id": None, + "user_id": None, + }, + "entity_id": "light.permitted", + "last_changed": ANY, + "last_updated": additions["lu"], + "state": "on", + } + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == {"r": ["light.permitted"]} + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "a": { + "light.permitted": { + "a": {"color": "blue", "effect": "help"}, + "c": ANY, + "lc": ANY, + "s": "on", + } + } + } + + +async def test_subscribe_unsubscribe_entities_specific_entities( + hass, websocket_client, hass_admin_user +): + """Test subscribe/unsubscribe entities with a list of entity ids.""" + + hass.states.async_set("light.permitted", "off", {"color": "red"}) + hass.states.async_set("light.not_intrested", "off", {"color": "blue"}) + original_state = hass.states.get("light.permitted") + assert isinstance(original_state, State) + hass_admin_user.groups = [] + hass_admin_user.mock_policy( + { + "entities": { + "entity_ids": {"light.permitted": True, "light.not_intrested": True} + } + } + ) + + await websocket_client.send_json( + {"id": 7, "type": "subscribe_entities", "entity_ids": ["light.permitted"]} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert isinstance(msg["event"]["a"]["light.permitted"]["c"], str) + assert msg["event"] == { + "a": { + "light.permitted": { + "a": {"color": "red"}, + "c": ANY, + "lc": ANY, + "s": "off", + } + } + } + hass.states.async_set("light.not_intrested", "on", {"effect": "help"}) + hass.states.async_set("light.not_permitted", "on") + hass.states.async_set("light.permitted", "on", {"color": "blue"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == { + "c": { + "light.permitted": { + "+": { + "a": {"color": "blue"}, + "c": ANY, + "lc": ANY, + "s": "on", + } + } + } + } + + async def test_render_template_renders_template(hass, websocket_client): """Test simple template is rendered and updated.""" hass.states.async_set("light.test", "on") From 23b8229143883b91360fc738391a906f45b4f5b5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 12 Mar 2022 12:20:04 +0100 Subject: [PATCH 0376/1054] Adjust ms.remote.control error logging in SamsungTV (#67988) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 18 ++++++++- tests/components/samsungtv/conftest.py | 27 +++++++++++--- .../components/samsungtv/test_media_player.py | 37 +++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 52776242911..acc6d5cd766 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -12,6 +12,7 @@ from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledRespo from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.command import SamsungTVCommand +from samsungtvws.event import MS_ERROR_EVENT from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException @@ -460,7 +461,7 @@ class SamsungTVWSBridge(SamsungTVBridge): name=VALUE_CONF_NAME, ) try: - await self._remote.start_listening() + await self._remote.start_listening(self._remote_event) except ConnectionClosedError as err: # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting @@ -498,6 +499,21 @@ class SamsungTVWSBridge(SamsungTVBridge): self._notify_new_token_callback() return self._remote + @staticmethod + def _remote_event(event: str, response: Any) -> None: + """Received event from remote websocket.""" + if event == MS_ERROR_EVENT: + # { 'event': 'ms.error', + # 'data': {'message': 'unrecognized method value : ms.remote.control'}} + if (data := response.get("data")) and ( + message := data.get("message") + ) == "unrecognized method value : ms.remote.control": + LOGGER.error( + "Your TV seems to be unsupported by " + "SamsungTVWSBridge and may need a PIN: '%s'", + message, + ) + async def async_power_off(self) -> None: """Send power off command to remote.""" if self._get_device_spec("FrameTVSupport") == "true": diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index d8bbfbf4627..c7733a3652a 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -1,5 +1,9 @@ """Fixtures for Samsung TV.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable from datetime import datetime +from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest @@ -48,14 +52,27 @@ def rest_api_fixture() -> Mock: @pytest.fixture(name="remotews") def remotews_fixture() -> Mock: """Patch the samsungtvws SamsungTVWS.""" + remotews = Mock(SamsungTVWSAsyncRemote) + remotews.__aenter__ = AsyncMock(return_value=remotews) + remotews.__aexit__ = AsyncMock() + remotews.app_list.return_value = SAMPLE_APP_LIST + remotews.token = "FAKE_TOKEN" + + def _start_listening( + ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None + ): + remotews.ws_event_callback = ws_event_callback + + def _mock_ws_event_callback(event: str, response: Any): + if remotews.ws_event_callback: + remotews.ws_event_callback(event, response) + + remotews.start_listening.side_effect = _start_listening + remotews.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback) + with patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", ) as remotews_class: - remotews = Mock(SamsungTVWSAsyncRemote) - remotews.__aenter__ = AsyncMock(return_value=remotews) - remotews.__aexit__ = AsyncMock() - remotews.app_list.return_value = SAMPLE_APP_LIST - remotews.token = "FAKE_TOKEN" remotews_class.return_value = remotews yield remotews diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c38dd2639b1..c76b9e9efb9 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1066,3 +1066,40 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: assert len(commands) == 1 assert isinstance(commands[0], ChannelEmitCommand) assert commands[0].params["data"]["appId"] == "3201608010191" + + +async def test_websocket_unsupported_remote_control( + hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test for turn_off.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=[OSError("Boom"), DEFAULT_MOCK], + ): + await setup_samsungtv(hass, MOCK_CONFIGWS) + + remotews.send_command.reset_mock() + + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + remotews.raise_mock_ws_event_callback( + "ms.error", + { + "event": "ms.error", + "data": {"message": "unrecognized method value : ms.remote.control"}, + }, + ) + + # key called + assert remotews.send_command.call_count == 1 + commands = remotews.send_command.call_args_list[0].args[0] + assert len(commands) == 1 + assert isinstance(commands[0], SendRemoteKey) + assert commands[0].params["DataOfCmd"] == "KEY_POWER" + + # error logged + assert ( + "Your TV seems to be unsupported by SamsungTVWSBridge and may need a PIN: " + "'unrecognized method value : ms.remote.control'" in caplog.text + ) From 0467dc55feff5ca7ee71ae2c04e67ba9b3e1b10e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Mar 2022 12:36:08 +0100 Subject: [PATCH 0377/1054] Add config flow to Uptime (#67408) --- CODEOWNERS | 2 + homeassistant/components/uptime/__init__.py | 17 ++++- .../components/uptime/config_flow.py | 37 ++++++++++ homeassistant/components/uptime/const.py | 9 +++ homeassistant/components/uptime/manifest.json | 5 +- homeassistant/components/uptime/sensor.py | 33 ++++++--- homeassistant/components/uptime/strings.json | 13 ++++ .../components/uptime/translations/en.json | 13 ++++ homeassistant/generated/config_flows.py | 1 + tests/components/uptime/conftest.py | 41 +++++++++++ tests/components/uptime/test_config_flow.py | 72 +++++++++++++++++++ tests/components/uptime/test_init.py | 55 ++++++++++++++ tests/components/uptime/test_sensor.py | 30 ++++++-- 13 files changed, 308 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/uptime/config_flow.py create mode 100644 homeassistant/components/uptime/const.py create mode 100644 homeassistant/components/uptime/strings.json create mode 100644 homeassistant/components/uptime/translations/en.json create mode 100644 tests/components/uptime/conftest.py create mode 100644 tests/components/uptime/test_config_flow.py create mode 100644 tests/components/uptime/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index c2be1901344..7e7f25ad153 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1066,6 +1066,8 @@ homeassistant/components/updater/* @home-assistant/core tests/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman @ehendrix23 tests/components/upnp/* @StevenLooman @ehendrix23 +homeassistant/components/uptime/* @frenck +tests/components/uptime/* @frenck homeassistant/components/uptimerobot/* @ludeeus @chemelli74 tests/components/uptimerobot/* @ludeeus @chemelli74 homeassistant/components/usb/* @bdraco diff --git a/homeassistant/components/uptime/__init__.py b/homeassistant/components/uptime/__init__.py index 99abc91cdf1..1c36fea3b32 100644 --- a/homeassistant/components/uptime/__init__.py +++ b/homeassistant/components/uptime/__init__.py @@ -1 +1,16 @@ -"""The uptime component.""" +"""The Uptime integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py new file mode 100644 index 00000000000..6ff36ee34b1 --- /dev/null +++ b/homeassistant/components/uptime/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Uptime integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Uptime.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry( + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={}, + ) + + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/uptime/const.py b/homeassistant/components/uptime/const.py new file mode 100644 index 00000000000..bbce8021474 --- /dev/null +++ b/homeassistant/components/uptime/const.py @@ -0,0 +1,9 @@ +"""Constants for the Uptime integration.""" +from typing import Final + +from homeassistant.const import Platform + +DOMAIN: Final = "uptime" +PLATFORMS: Final = [Platform.SENSOR] + +DEFAULT_NAME: Final = "Uptime" diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index cf2dd1a6ea1..3bcc47815f8 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -2,7 +2,8 @@ "domain": "uptime", "name": "Uptime", "documentation": "https://www.home-assistant.io/integrations/uptime", - "codeowners": [], + "codeowners": ["@frenck"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index a622835a0da..944f9b77de8 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -15,17 +16,15 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -DEFAULT_NAME = "Uptime" +from .const import DEFAULT_NAME, DOMAIN PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_UNIT_OF_MEASUREMENT), + cv.removed(CONF_UNIT_OF_MEASUREMENT, raise_if_present=False), PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="days"): vol.All( - cv.string, vol.In(["minutes", "hours", "days", "seconds"]) - ), - } + vol.Remove(CONF_UNIT_OF_MEASUREMENT): cv.string, + }, ), ) @@ -37,9 +36,22 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the uptime sensor platform.""" - name = config[CONF_NAME] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - async_add_entities([UptimeSensor(name)], True) + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config_entry.""" + async_add_entities([UptimeSensor(entry)]) class UptimeSensor(SensorEntity): @@ -48,7 +60,8 @@ class UptimeSensor(SensorEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP _attr_should_poll = False - def __init__(self, name: str) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize the uptime sensor.""" - self._attr_name = name + self._attr_name = entry.title self._attr_native_value = dt_util.utcnow() + self._attr_unique_id = entry.entry_id diff --git a/homeassistant/components/uptime/strings.json b/homeassistant/components/uptime/strings.json new file mode 100644 index 00000000000..9ceb91de9ba --- /dev/null +++ b/homeassistant/components/uptime/strings.json @@ -0,0 +1,13 @@ +{ + "title": "Uptime", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/uptime/translations/en.json b/homeassistant/components/uptime/translations/en.json new file mode 100644 index 00000000000..5d38ae74e21 --- /dev/null +++ b/homeassistant/components/uptime/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 93592a2e1a9..b48df75c189 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -355,6 +355,7 @@ FLOWS = [ "upb", "upcloud", "upnp", + "uptime", "uptimerobot", "vallox", "velbus", diff --git a/tests/components/uptime/conftest.py b/tests/components/uptime/conftest.py new file mode 100644 index 00000000000..7ee34856e63 --- /dev/null +++ b/tests/components/uptime/conftest.py @@ -0,0 +1,41 @@ +"""Fixtures for Uptime integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.uptime.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Uptime", + domain=DOMAIN, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.uptime.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Uptime integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py new file mode 100644 index 00000000000..69ba00f6ac8 --- /dev/null +++ b/tests/components/uptime/test_config_flow.py @@ -0,0 +1,72 @@ +"""Tests for the Uptime config flow.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.uptime.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Uptime" + assert result2.get("data") == {} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_NAME: "My Uptime"}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "My Uptime" + assert result.get("data") == {} diff --git a/tests/components/uptime/test_init.py b/tests/components/uptime/test_init.py new file mode 100644 index 00000000000..0f966734550 --- /dev/null +++ b/tests/components/uptime/test_init.py @@ -0,0 +1,55 @@ +"""Tests for the Uptime integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.uptime.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the Uptime configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_import_config( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test Uptime being set up from config via import.""" + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_NAME: "My Uptime", + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 1 + + entry = config_entries[0] + assert entry.title == "My Uptime" + assert entry.unique_id is None + assert entry.data == {} diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index fe3ae30a843..e8d0306246f 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,11 +1,27 @@ """The tests for the uptime sensor platform.""" +import pytest -from homeassistant.setup import async_setup_component +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry -async def test_uptime_sensor_name_change(hass): - """Test uptime sensor with different name.""" - config = {"sensor": {"platform": "uptime", "name": "foobar"}} - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - assert hass.states.get("sensor.foobar") +@pytest.mark.freeze_time("2022-03-01 00:00:00+00:00") +async def test_uptime_sensor( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test Uptime sensor.""" + state = hass.states.get("sensor.uptime") + assert state + assert state.state == "2022-03-01T00:00:00+00:00" + assert state.attributes["friendly_name"] == "Uptime" + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TIMESTAMP + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("sensor.uptime") + assert entry + assert entry.unique_id == init_integration.entry_id From d41eeaa88b0dd2589e527680a46c3f3aa1e5da60 Mon Sep 17 00:00:00 2001 From: Davor Val Vega Date: Sat, 12 Mar 2022 12:38:43 +0100 Subject: [PATCH 0378/1054] Add Home Connect pre-rinse capability for a dishwasher (#63536) --- homeassistant/components/home_connect/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index f3c98e618b8..bce3f5ece61 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -312,6 +312,7 @@ class Dishwasher( """Dishwasher class.""" PROGRAMS = [ + {"name": "Dishcare.Dishwasher.Program.PreRinse"}, {"name": "Dishcare.Dishwasher.Program.Auto1"}, {"name": "Dishcare.Dishwasher.Program.Auto2"}, {"name": "Dishcare.Dishwasher.Program.Auto3"}, From 2bb5573ddc296eb4e5ab679e5a63e462f827e5db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Mar 2022 13:04:46 +0100 Subject: [PATCH 0379/1054] Remove deprecated PiFace Digital I/O (PFIO) integration (#67282) --- .coveragerc | 1 - homeassistant/components/rpi_pfio/__init__.py | 62 ------------- .../components/rpi_pfio/binary_sensor.py | 90 ------------------- .../components/rpi_pfio/manifest.json | 9 -- homeassistant/components/rpi_pfio/switch.py | 85 ------------------ requirements_all.txt | 6 -- 6 files changed, 253 deletions(-) delete mode 100644 homeassistant/components/rpi_pfio/__init__.py delete mode 100644 homeassistant/components/rpi_pfio/binary_sensor.py delete mode 100644 homeassistant/components/rpi_pfio/manifest.json delete mode 100644 homeassistant/components/rpi_pfio/switch.py diff --git a/.coveragerc b/.coveragerc index 95856947fb6..aa2eef16079 100644 --- a/.coveragerc +++ b/.coveragerc @@ -983,7 +983,6 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* - homeassistant/components/rpi_pfio/* homeassistant/components/rtorrent/sensor.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py deleted file mode 100644 index c4687c59114..00000000000 --- a/homeassistant/components/rpi_pfio/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Support for controlling the PiFace Digital I/O module on a RPi.""" -import logging - -import pifacedigitalio as PFIO - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "rpi_pfio" - -DATA_PFIO_LISTENER = "pfio_listener" - -_LOGGER = logging.getLogger(__name__) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Raspberry PI PFIO component.""" - _LOGGER.warning( - "The PiFace Digital I/O (PFIO) integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pifacedigital = PFIO.PiFaceDigital() - hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) - - def cleanup_pfio(event): - """Stuff to do before stopping.""" - PFIO.deinit() - - def prepare_pfio(event): - """Stuff to do when Home Assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_pfio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_pfio) - PFIO.init() - - return True - - -def write_output(port, value): - """Write a value to a PFIO.""" - PFIO.digital_write(port, value) - - -def read_input(port): - """Read a value from a PFIO.""" - return PFIO.digital_read(port) - - -def edge_detect(hass, port, event_callback, settle): - """Add detection for RISING and FALLING events.""" - hass.data[DATA_PFIO_LISTENER].register( - port, PFIO.IODIR_BOTH, event_callback, settle_time=settle - ) - - -def activate_listener(hass): - """Activate the registered listener events.""" - hass.data[DATA_PFIO_LISTENER].activate() diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py deleted file mode 100644 index ddce88949fd..00000000000 --- a/homeassistant/components/rpi_pfio/binary_sensor.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Support for binary sensor using the PiFace Digital I/O module on a RPi.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import rpi_pfio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_PORTS = "ports" -CONF_SETTLE_TIME = "settle_time" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_SETTLE_TIME = 20 - -PORT_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_PORTS, default={}): vol.Schema({cv.positive_int: PORT_SCHEMA})} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PiFace Digital Input devices.""" - binary_sensors = [] - ports = config[CONF_PORTS] - for port, port_entity in ports.items(): - name = port_entity.get(CONF_NAME) - settle_time = port_entity[CONF_SETTLE_TIME] / 1000 - invert_logic = port_entity[CONF_INVERT_LOGIC] - - binary_sensors.append( - RPiPFIOBinarySensor(hass, port, name, settle_time, invert_logic) - ) - add_entities(binary_sensors, True) - - rpi_pfio.activate_listener(hass) - - -class RPiPFIOBinarySensor(BinarySensorEntity): - """Represent a binary sensor that a PiFace Digital Input.""" - - def __init__(self, hass, port, name, settle_time, invert_logic): - """Initialize the RPi binary sensor.""" - self._port = port - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._state = None - - def read_pfio(port): - """Read state from PFIO.""" - self._state = rpi_pfio.read_input(self._port) - self.schedule_update_ha_state() - - rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update the PFIO state.""" - self._state = rpi_pfio.read_input(self._port) diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json deleted file mode 100644 index 7f72a7ba77d..00000000000 --- a/homeassistant/components/rpi_pfio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "rpi_pfio", - "name": "PiFace Digital I/O (PFIO)", - "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", - "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["pifacedigitalio"] -} diff --git a/homeassistant/components/rpi_pfio/switch.py b/homeassistant/components/rpi_pfio/switch.py deleted file mode 100644 index ca3d176eecb..00000000000 --- a/homeassistant/components/rpi_pfio/switch.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Support for switches using the PiFace Digital I/O module on a RPi.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import rpi_pfio -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -ATTR_INVERT_LOGIC = "invert_logic" - -CONF_PORTS = "ports" - -DEFAULT_INVERT_LOGIC = False - -PORT_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_PORTS, default={}): vol.Schema({cv.positive_int: PORT_SCHEMA})} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PiFace Digital Output devices.""" - switches = [] - ports = config[CONF_PORTS] - for port, port_entity in ports.items(): - name = port_entity.get(ATTR_NAME) - invert_logic = port_entity[ATTR_INVERT_LOGIC] - - switches.append(RPiPFIOSwitch(port, name, invert_logic)) - add_entities(switches) - - -class RPiPFIOSwitch(SwitchEntity): - """Representation of a PiFace Digital Output.""" - - def __init__(self, port, name, invert_logic): - """Initialize the pin.""" - self._port = port - self._name = name or DEVICE_DEFAULT_NAME - self._invert_logic = invert_logic - self._state = False - rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn the device on.""" - rpi_pfio.write_output(self._port, 0 if self._invert_logic else 1) - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) - self._state = False - self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index fa3e8e745c9..2b7667e524f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1192,12 +1192,6 @@ phone_modem==0.1.1 # homeassistant.components.onewire pi1wire==0.1.0 -# homeassistant.components.rpi_pfio -pifacecommon==4.2.2 - -# homeassistant.components.rpi_pfio -pifacedigitalio==3.0.5 - # homeassistant.components.pilight pilight==0.1.1 From 0070e27c041c04029ea0dc5bc393ad366e505688 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 12 Mar 2022 13:05:22 +0100 Subject: [PATCH 0380/1054] Remove deprecated PCAL9535A I/O Expander integration (#67274) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/pcal9535a/__init__.py | 3 - .../components/pcal9535a/binary_sensor.py | 110 ---------------- .../components/pcal9535a/manifest.json | 9 -- homeassistant/components/pcal9535a/switch.py | 119 ------------------ requirements_all.txt | 3 - 7 files changed, 246 deletions(-) delete mode 100644 homeassistant/components/pcal9535a/__init__.py delete mode 100644 homeassistant/components/pcal9535a/binary_sensor.py delete mode 100644 homeassistant/components/pcal9535a/manifest.json delete mode 100644 homeassistant/components/pcal9535a/switch.py diff --git a/.coveragerc b/.coveragerc index aa2eef16079..26b24bfbe00 100644 --- a/.coveragerc +++ b/.coveragerc @@ -878,7 +878,6 @@ omit = homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py - homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py homeassistant/components/philips_js/diagnostics.py diff --git a/CODEOWNERS b/CODEOWNERS index 7e7f25ad153..668a6bae19e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -743,7 +743,6 @@ homeassistant/components/panel_custom/* @home-assistant/frontend tests/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend tests/components/panel_iframe/* @home-assistant/frontend -homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core tests/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus diff --git a/homeassistant/components/pcal9535a/__init__.py b/homeassistant/components/pcal9535a/__init__.py deleted file mode 100644 index fa1295939be..00000000000 --- a/homeassistant/components/pcal9535a/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Support for I2C PCAL9535A chip.""" - -DOMAIN = "pcal9535a" diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py deleted file mode 100644 index 729bc534402..00000000000 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Support for binary sensor using I2C PCAL9535A chip.""" -from __future__ import annotations - -import logging - -from pcal9535a import PCAL9535A -import voluptuous as vol - -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_PINS = "pins" -CONF_PULL_MODE = "pull_mode" - -MODE_UP = "UP" -MODE_DOWN = "DOWN" -MODE_DISABLED = "DISABLED" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 -DEFAULT_I2C_BUS = 1 -DEFAULT_PULL_MODE = MODE_DISABLED - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SENSORS_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( - vol.Upper, vol.In([MODE_UP, MODE_DOWN, MODE_DISABLED]) - ), - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PCAL9535A binary sensors.""" - _LOGGER.warning( - "The PCAL9535A I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - pull_mode = config[CONF_PULL_MODE] - invert_logic = config[CONF_INVERT_LOGIC] - i2c_address = config[CONF_I2C_ADDRESS] - bus = config[CONF_I2C_BUS] - - pcal = PCAL9535A(bus, i2c_address) - - binary_sensors = [] - pins = config[CONF_PINS] - - for pin_num, pin_name in pins.items(): - pin = pcal.get_pin(pin_num // 8, pin_num % 8) - binary_sensors.append( - PCAL9535ABinarySensor(pin_name, pin, pull_mode, invert_logic) - ) - - add_entities(binary_sensors, True) - - -class PCAL9535ABinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses PCAL9535A.""" - - def __init__(self, name, pin, pull_mode, invert_logic): - """Initialize the PCAL9535A binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._pin.input = True - self._pin.inverted = invert_logic - if pull_mode == "DISABLED": - self._pin.pullup = 0 - elif pull_mode == "DOWN": - self._pin.pullup = -1 - else: - self._pin.pullup = 1 - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the cached state of the entity.""" - return self._state - - def update(self): - """Update the GPIO state.""" - self._state = self._pin.level diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json deleted file mode 100644 index fc821426542..00000000000 --- a/homeassistant/components/pcal9535a/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "pcal9535a", - "name": "PCAL9535A I/O Expander", - "documentation": "https://www.home-assistant.io/integrations/pcal9535a", - "requirements": ["pcal9535a==0.7"], - "codeowners": ["@Shulyaka"], - "iot_class": "local_polling", - "loggers": ["pcal9535a", "smbus_cffi"] -} diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py deleted file mode 100644 index 70da61a597a..00000000000 --- a/homeassistant/components/pcal9535a/switch.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Support for switch sensor using I2C PCAL9535A chip.""" -from __future__ import annotations - -import logging - -from pcal9535a import PCAL9535A -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -CONF_INVERT_LOGIC = "invert_logic" -CONF_I2C_ADDRESS = "i2c_address" -CONF_I2C_BUS = "i2c_bus" -CONF_PINS = "pins" -CONF_STRENGTH = "strength" - -STRENGTH_025 = "0.25" -STRENGTH_050 = "0.5" -STRENGTH_075 = "0.75" -STRENGTH_100 = "1.0" - -DEFAULT_INVERT_LOGIC = False -DEFAULT_I2C_ADDRESS = 0x20 -DEFAULT_I2C_BUS = 1 -DEFAULT_STRENGTH = STRENGTH_100 - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PINS): _SWITCHES_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_STRENGTH, default=DEFAULT_STRENGTH): vol.In( - [STRENGTH_025, STRENGTH_050, STRENGTH_075, STRENGTH_100] - ), - vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), - vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, - } -) - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the PCAL9535A devices.""" - _LOGGER.warning( - "The PCAL9535A I/O Expander integration is deprecated and will be removed " - "in Home Assistant Core 2022.4; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - invert_logic = config[CONF_INVERT_LOGIC] - i2c_address = config[CONF_I2C_ADDRESS] - bus = config[CONF_I2C_BUS] - - pcal = PCAL9535A(bus, i2c_address) - - switches = [] - pins = config[CONF_PINS] - for pin_num, pin_name in pins.items(): - pin = pcal.get_pin(pin_num // 8, pin_num % 8) - switches.append(PCAL9535ASwitch(pin_name, pin, invert_logic)) - - add_entities(switches) - - -class PCAL9535ASwitch(SwitchEntity): - """Representation of a PCAL9535A output pin.""" - - def __init__(self, name, pin, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._pin = pin - self._pin.inverted = invert_logic - self._pin.input = False - self._state = self._pin.level - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def assumed_state(self): - """Return true if optimistic updates are used.""" - return True - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._pin.level = True - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._pin.level = False - self._state = False - self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 2b7667e524f..76824c14796 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1171,9 +1171,6 @@ panacotta==0.1 # homeassistant.components.panasonic_viera panasonic_viera==0.3.6 -# homeassistant.components.pcal9535a -pcal9535a==0.7 - # homeassistant.components.dunehd pdunehd==1.3.2 From 68310a426b3a5d1043f8d02010d85e0cfcc5e649 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Mar 2022 02:07:01 -1000 Subject: [PATCH 0381/1054] Small code quality improvements for subscribe_entities (#68026) --- homeassistant/components/websocket_api/messages.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 0695b279361..eac40c9510b 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -175,18 +175,16 @@ def compressed_state_dict_add(state: State) -> dict[str, Any]: Sends c (context) as a string if it only contains an id. """ if state.context.parent_id is None and state.context.user_id is None: - context: dict[str, Any] | str = state.context.id # type: ignore[unreachable] + context: dict[str, Any] | str = state.context.id else: context = state.context.as_dict() compressed_state: dict[str, Any] = { COMPRESSED_STATE_STATE: state.state, COMPRESSED_STATE_ATTRIBUTES: state.attributes, COMPRESSED_STATE_CONTEXT: context, + COMPRESSED_STATE_LAST_CHANGED: state.last_changed.timestamp(), } - if state.last_changed == state.last_updated: - compressed_state[COMPRESSED_STATE_LAST_CHANGED] = state.last_changed.timestamp() - else: - compressed_state[COMPRESSED_STATE_LAST_CHANGED] = state.last_changed.timestamp() + if state.last_changed != state.last_updated: compressed_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated.timestamp() return compressed_state From a851921fe6257510a474aba99e5ba7eeaea3d613 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Mar 2022 02:09:02 -1000 Subject: [PATCH 0382/1054] Filter IPv6 addresses from doorbird discovery (#68031) --- .../components/doorbird/config_flow.py | 4 +- .../components/doorbird/translations/en.json | 3 +- tests/components/doorbird/test_config_flow.py | 48 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 31ddd1f6193..cc882b0ed50 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.util.network import is_link_local +from homeassistant.util.network import is_ipv4_address, is_link_local from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI from .util import get_mac_address_from_doorstation_info @@ -103,6 +103,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") if is_link_local(ip_address(host)): return self.async_abort(reason="link_local_address") + if not is_ipv4_address(host): + return self.async_abort(reason="not_ipv4_address") await self.async_set_unique_id(macaddress) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index db1cea2d73f..c67658196c4 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Device is already configured", "link_local_address": "Link local addresses are not supported", - "not_doorbird_device": "This device is not a DoorBird" + "not_doorbird_device": "This device is not a DoorBird", + "not_ipv4_address": "Only IPv4 addresess are supported" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 10cc4a4fb77..cbf455653ff 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -116,6 +116,54 @@ async def test_form_zeroconf_link_local_ignored(hass): assert result["reason"] == "link_local_address" +async def test_form_zeroconf_ipv4_address(hass): + """Test we abort and update the ip address from zeroconf with an ipv4 address.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="1CCAE3AAAAAA", + data=VALID_CONFIG, + options={CONF_EVENTS: ["event1", "event2", "event3"]}, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="4.4.4.4", + addresses=["4.4.4.4"], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3AAAAAA"}, + type="mock_type", + ), + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == "4.4.4.4" + + +async def test_form_zeroconf_non_ipv4_ignored(hass): + """Test we abort when we get a non ipv4 address via zeroconf.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3DOORBIRD"}, + type="mock_type", + ), + ) + assert result["type"] == "abort" + assert result["reason"] == "not_ipv4_address" + + async def test_form_zeroconf_correct_oui(hass): """Test we can setup from zeroconf with the correct OUI source.""" doorbirdapi = _get_mock_doorbirdapi_return_values( From 1a791186006a59c3bd330be86dc4d207e99a5e7e Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 12 Mar 2022 13:12:38 +0100 Subject: [PATCH 0383/1054] Fix modbus reload service (#68040) * Fix modbus reload service. * Please coverage. * Resolve difference between local pytest and github. --- homeassistant/components/modbus/__init__.py | 13 ++++++ homeassistant/components/modbus/modbus.py | 15 +------ .../modbus/fixtures/configuration.yaml | 6 +++ tests/components/modbus/test_init.py | 44 ++++++++++++++++++- 4 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 tests/components/modbus/fixtures/configuration.yaml diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 41a1e37425e..17a4acc1742 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,6 +1,7 @@ """Support for Modbus.""" from __future__ import annotations +import logging from typing import cast import voluptuous as vol @@ -110,6 +111,9 @@ from .validators import ( struct_validator, ) +_LOGGER = logging.getLogger(__name__) + + BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) @@ -342,3 +346,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, config, ) + + +async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: + """Release modbus resources.""" + _LOGGER.info("Modbus reloading") + hubs = hass.data[DOMAIN] + for name in hubs: + await hubs[name].async_close() + del hass.data[DOMAIN] diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 3fa4a74a932..5e15f65035a 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -130,10 +130,7 @@ async def async_modbus_setup( ) -> bool: """Set up Modbus component.""" - platform_names = [] - for entry in PLATFORMS: - platform_names.append(entry[1]) - await async_setup_reload_service(hass, DOMAIN, platform_names) + await async_setup_reload_service(hass, DOMAIN, [DOMAIN]) hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: @@ -245,14 +242,6 @@ async def async_modbus_setup( return True -async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: - """Release modbus resources.""" - _LOGGER.info("Modbus reloading") - for hub in hass.data[DOMAIN]: - await hub.async_close() - del hass.data[DOMAIN] - - class ModbusHub: """Thread safe wrapper class for pymodbus.""" @@ -410,7 +399,7 @@ class ModbusHub: return None async with self._lock: if not self._client: - return None # pragma: no cover + return None result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) diff --git a/tests/components/modbus/fixtures/configuration.yaml b/tests/components/modbus/fixtures/configuration.yaml new file mode 100644 index 00000000000..0a2f46b3151 --- /dev/null +++ b/tests/components/modbus/fixtures/configuration.yaml @@ -0,0 +1,6 @@ +modbus: + type: "tcp" + host: "testHost" + port: 5001 + name: "testModbus" + diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index ac9aa964316..11ddf4bc426 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -21,6 +21,7 @@ from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest import pytest import voluptuous as vol +from homeassistant import config as hass_config from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.modbus.const import ( ATTR_ADDRESS, @@ -83,6 +84,7 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -99,7 +101,7 @@ from .conftest import ( ReadResult, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, get_fixture_path @pytest.fixture(name="mock_modbus_with_pymodbus") @@ -824,3 +826,43 @@ async def test_stop_restart(hass, caplog, mock_modbus): assert mock_modbus.connect.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_write_no_client(hass, mock_modbus): + """Run test for service stop and write without client.""" + + mock_modbus.reset() + data = { + ATTR_HUB: TEST_MODBUS_NAME, + } + await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) + await hass.async_block_till_done() + assert mock_modbus.close.called + + data = { + ATTR_HUB: TEST_MODBUS_NAME, + ATTR_UNIT: 17, + ATTR_ADDRESS: 16, + ATTR_STATE: True, + } + await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_integration_reload(hass, caplog, mock_modbus): + """Run test for integration reload.""" + + caplog.set_level(logging.INFO) + caplog.clear() + + yaml_path = get_fixture_path("configuration.yaml", "modbus") + now = dt_util.utcnow() + with mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.async_block_till_done() + for i in range(4): + now = now + timedelta(seconds=1) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert "Modbus reloading" in caplog.text From 083d51a727b398fc31b46a280f562ec87133e2da Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 12 Mar 2022 14:43:57 +0200 Subject: [PATCH 0384/1054] Cleanup - move Shelly logger to const (#68046) --- homeassistant/components/shelly/__init__.py | 76 +++++++++---------- homeassistant/components/shelly/climate.py | 20 +++-- .../components/shelly/config_flow.py | 11 +-- homeassistant/components/shelly/const.py | 6 +- homeassistant/components/shelly/entity.py | 16 ++-- homeassistant/components/shelly/light.py | 8 +- homeassistant/components/shelly/number.py | 9 +-- homeassistant/components/shelly/utils.py | 12 ++- 8 files changed, 74 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4747d044bfa..d29584c4e83 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from datetime import timedelta -import logging from typing import Any, Final, cast import aioshelly @@ -47,6 +46,7 @@ from .const import ( ENTRY_RELOAD_COOLDOWN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, + LOGGER, MODELS_SUPPORTING_LIGHT_EFFECTS, POLLING_TIMEOUT_SEC, REST, @@ -91,7 +91,7 @@ RPC_PLATFORMS: Final = [ Platform.SENSOR, Platform.SWITCH, ] -_LOGGER: Final = logging.getLogger(__name__) + COAP_SCHEMA: Final = vol.Schema( { @@ -119,7 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # error. The config entry data for this custom component doesn't contain host # value, so if host isn't present, config entry will not be configured. if not entry.data.get(CONF_HOST): - _LOGGER.warning( + LOGGER.warning( "The config entry %s probably comes from a custom integration, please remove it if you want to use core Shelly integration", entry.title, ) @@ -173,7 +173,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo @callback def _async_device_online(_: Any) -> None: - _LOGGER.debug("Device %s is online, resuming setup", entry.title) + LOGGER.debug("Device %s is online, resuming setup", entry.title) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = None if sleep_period is None: @@ -186,7 +186,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo if sleep_period == 0: # Not a sleeping device, finish setup - _LOGGER.debug("Setting up online block device %s", entry.title) + LOGGER.debug("Setting up online block device %s", entry.title) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await device.initialize() @@ -201,13 +201,13 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device - _LOGGER.debug( + LOGGER.debug( "Setup for device %s will resume when device is online", entry.title ) device.subscribe_updates(_async_device_online) else: # Restore sensors for sleeping device - _LOGGER.debug("Setting up offline block device %s", entry.title) + LOGGER.debug("Setting up offline block device %s", entry.title) await async_block_device_setup(hass, entry, device) return True @@ -241,7 +241,7 @@ async def async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool entry.data.get(CONF_PASSWORD), ) - _LOGGER.debug("Setting up online RPC device %s", entry.title) + LOGGER.debug("Setting up online RPC device %s", entry.title) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): device = await RpcDevice.create( @@ -287,7 +287,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): ) super().__init__( hass, - _LOGGER, + LOGGER, name=device_name, update_interval=timedelta(seconds=update_interval), ) @@ -297,7 +297,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._debounced_reload = Debouncer( hass, - _LOGGER, + LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, immediate=False, function=self._async_reload_entry, @@ -318,7 +318,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_reload_entry(self) -> None: """Reload entry.""" - _LOGGER.debug("Reloading entry %s", self.name) + LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) @callback @@ -389,14 +389,14 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): }, ) else: - _LOGGER.warning( + LOGGER.warning( "Shelly input event %s for device %s is not supported, please open issue", event_type, self.name, ) if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: - _LOGGER.info( + LOGGER.info( "Config for %s changed, reloading entry in %s seconds", self.name, ENTRY_RELOAD_COOLDOWN, @@ -412,7 +412,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): f"Sleeping device did not update within {sleep_period} seconds interval" ) - _LOGGER.debug("Polling Shelly Block Device - %s", self.name) + LOGGER.debug("Polling Shelly Block Device - %s", self.name) try: async with async_timeout.timeout(POLLING_TIMEOUT_SEC): await self.device.update() @@ -454,26 +454,26 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def async_trigger_ota_update(self, beta: bool = False) -> None: """Trigger or schedule an ota update.""" update_data = self.device.status["update"] - _LOGGER.debug("OTA update service - update_data: %s", update_data) + LOGGER.debug("OTA update service - update_data: %s", update_data) if not update_data["has_update"] and not beta: - _LOGGER.warning("No OTA update available for device %s", self.name) + LOGGER.warning("No OTA update available for device %s", self.name) return if beta and not update_data.get("beta_version"): - _LOGGER.warning( + LOGGER.warning( "No OTA update on beta channel available for device %s", self.name ) return if update_data["status"] == "updating": - _LOGGER.warning("OTA update already in progress for %s", self.name) + LOGGER.warning("OTA update already in progress for %s", self.name) return new_version = update_data["new_version"] if beta: new_version = update_data["beta_version"] - _LOGGER.info( + LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, self.device.firmware_version, @@ -483,8 +483,8 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): result = await self.device.trigger_ota_update(beta=beta) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.exception("Error while perform ota update: %s", err) - _LOGGER.debug("Result of OTA update call: %s", result) + LOGGER.exception("Error while perform ota update: %s", err) + LOGGER.debug("Result of OTA update call: %s", result) def shutdown(self) -> None: """Shutdown the wrapper.""" @@ -493,7 +493,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): @callback def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" - _LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) + LOGGER.debug("Stopping BlockDeviceWrapper for %s", self.name) self.shutdown() @@ -516,7 +516,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): super().__init__( hass, - _LOGGER, + LOGGER, name=get_block_device_name(device), update_interval=timedelta(seconds=update_interval), ) @@ -527,7 +527,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Fetch data.""" try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - _LOGGER.debug("REST update for %s", self.name) + LOGGER.debug("REST update for %s", self.name) await self.device.update_status() if self.device.status["uptime"] > 2 * REST_SENSORS_UPDATE_INTERVAL: @@ -628,7 +628,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): device_name = get_rpc_device_name(device) if device.initialized else entry.title super().__init__( hass, - _LOGGER, + LOGGER, name=device_name, update_interval=timedelta(seconds=RPC_RECONNECT_INTERVAL), ) @@ -637,7 +637,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._debounced_reload = Debouncer( hass, - _LOGGER, + LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, immediate=False, function=self._async_reload_entry, @@ -655,7 +655,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_reload_entry(self) -> None: """Reload entry.""" - _LOGGER.debug("Reloading entry %s", self.name) + LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) @callback @@ -676,7 +676,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): continue if event_type == "config_changed": - _LOGGER.info( + LOGGER.info( "Config for %s changed, reloading entry in %s seconds", self.name, ENTRY_RELOAD_COOLDOWN, @@ -700,7 +700,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): return try: - _LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) + LOGGER.debug("Reconnecting to Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.initialize() device_update_info(self.hass, self.device, self.entry) @@ -742,14 +742,14 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): """Trigger an ota update.""" update_data = self.device.status["sys"]["available_updates"] - _LOGGER.debug("OTA update service - update_data: %s", update_data) + LOGGER.debug("OTA update service - update_data: %s", update_data) if not bool(update_data) or (not update_data.get("stable") and not beta): - _LOGGER.warning("No OTA update available for device %s", self.name) + LOGGER.warning("No OTA update available for device %s", self.name) return if beta and not update_data.get(ATTR_BETA): - _LOGGER.warning( + LOGGER.warning( "No OTA update on beta channel available for device %s", self.name ) return @@ -759,7 +759,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): new_version = update_data.get(ATTR_BETA, {"version": ""})["version"] assert self.device.shelly - _LOGGER.info( + LOGGER.info( "Start OTA update of device %s from '%s' to '%s'", self.name, self.device.firmware_version, @@ -770,9 +770,9 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): result = await self.device.trigger_ota_update(beta=beta) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.exception("Error while perform ota update: %s", err) + LOGGER.exception("Error while perform ota update: %s", err) - _LOGGER.debug("Result of OTA update call: %s", result) + LOGGER.debug("Result of OTA update call: %s", result) async def shutdown(self) -> None: """Shutdown the wrapper.""" @@ -780,7 +780,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _handle_ha_stop(self, _event: Event) -> None: """Handle Home Assistant stopping.""" - _LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) + LOGGER.debug("Stopping RpcDeviceWrapper for %s", self.name) await self.shutdown() @@ -796,7 +796,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): device_name = get_rpc_device_name(device) if device.initialized else entry.title super().__init__( hass, - _LOGGER, + LOGGER, name=device_name, update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), ) @@ -809,7 +809,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): raise update_coordinator.UpdateFailed("Device disconnected") try: - _LOGGER.debug("Polling Shelly RPC Device - %s", self.name) + LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() except OSError as err: diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index 1e7ba2dd183..4e6767e1d61 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -3,8 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import Block import async_timeout @@ -34,12 +33,11 @@ from .const import ( BLOCK, DATA_CONFIG_ENTRY, DOMAIN, + LOGGER, SHTRV_01_TEMPERATURE_SETTINGS, ) from .utils import get_device_entry_gen -_LOGGER: Final = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -81,7 +79,7 @@ async def async_setup_climate_entities( sensor_block = block if sensor_block and device_block: - _LOGGER.debug("Setup online climate device %s", wrapper.name) + LOGGER.debug("Setup online climate device %s", wrapper.name) async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) @@ -103,8 +101,8 @@ async def async_restore_climate_entities( if entry.domain != CLIMATE_DOMAIN: continue - _LOGGER.debug("Setup sleeping climate device %s", wrapper.name) - _LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) + LOGGER.debug("Setup sleeping climate device %s", wrapper.name) + LOGGER.debug("Found entry %s [%s]", entry.original_name, entry.domain) async_add_entities([BlockSleepingClimate(wrapper, None, None, entry)]) break @@ -242,14 +240,14 @@ class BlockSleepingClimate( async def set_state_full_path(self, **kwargs: Any) -> Any: """Set block state (HTTP request).""" - _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) + LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.http_request( "get", f"thermostat/{self._channel}", kwargs ) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( + LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", self.name, kwargs, @@ -287,7 +285,7 @@ class BlockSleepingClimate( async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" - _LOGGER.info("Restoring entity %s", self.name) + LOGGER.info("Restoring entity %s", self.name) last_state = await self.async_get_last_state() @@ -316,7 +314,7 @@ class BlockSleepingClimate( self.block = block if self.device_block and self.block: - _LOGGER.debug("Entity %s attached to blocks", self.name) + LOGGER.debug("Entity %s attached to blocks", self.name) assert self.block.channel diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 521fca79dc9..d0e962b5e5f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from http import HTTPStatus -import logging from typing import Any, Final import aiohttp @@ -20,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, DOMAIN, LOGGER from .utils import ( get_block_device_name, get_block_device_sleep_period, @@ -31,8 +30,6 @@ from .utils import ( get_rpc_device_name, ) -_LOGGER: Final = logging.getLogger(__name__) - HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) HTTP_CONNECT_ERRORS: Final = (asyncio.TimeoutError, aiohttp.ClientError) @@ -107,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except aioshelly.exceptions.FirmwareUnsupported: return self.async_abort(reason="unsupported_firmware") except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: await self.async_set_unique_id(self.info["mac"]) @@ -123,7 +120,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry( @@ -158,7 +155,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return self.async_create_entry( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cd5735b2888..3dacf2bfd6a 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -1,13 +1,17 @@ """Constants for the Shelly integration.""" from __future__ import annotations +from logging import Logger, getLogger import re from typing import Final +DOMAIN: Final = "shelly" + +LOGGER: Logger = getLogger(__package__) + BLOCK: Final = "block" DATA_CONFIG_ENTRY: Final = "config_entry" DEVICE: Final = "device" -DOMAIN: Final = "shelly" REST: Final = "rest" RPC: Final = "rpc" RPC_POLL: Final = "rpc_poll" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 85abd67c069..f544770722f 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -4,8 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Mapping from dataclasses import dataclass -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import Block import async_timeout @@ -34,6 +33,7 @@ from .const import ( BLOCK, DATA_CONFIG_ENTRY, DOMAIN, + LOGGER, REST, RPC, RPC_POLL, @@ -45,8 +45,6 @@ from .utils import ( get_rpc_key_instances, ) -_LOGGER: Final = logging.getLogger(__name__) - async def async_setup_entry_attribute_entities( hass: HomeAssistant, @@ -313,12 +311,12 @@ class ShellyBlockEntity(entity.Entity): async def set_state(self, **kwargs: Any) -> Any: """Set block state (HTTP request).""" - _LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) + LOGGER.debug("Setting state for entity %s, state: %s", self.name, kwargs) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.block.set_state(**kwargs) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( + LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", self.name, kwargs, @@ -371,7 +369,7 @@ class ShellyRpcEntity(entity.Entity): async def call_rpc(self, method: str, params: Any) -> Any: """Call RPC method.""" - _LOGGER.debug( + LOGGER.debug( "Call RPC for entity %s, method: %s, params: %s", self.name, method, @@ -381,7 +379,7 @@ class ShellyRpcEntity(entity.Entity): async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.call_rpc(method, params) except asyncio.TimeoutError as err: - _LOGGER.error( + LOGGER.error( "Call RPC for entity %s failed, method: %s, params: %s, error: %s", self.name, method, @@ -625,6 +623,6 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self.block = block self.entity_description = description - _LOGGER.debug("Entity %s attached to block", self.name) + LOGGER.debug("Entity %s attached to block", self.name) super()._update_callback() return diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 13a7720e7ed..e86c53c1914 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,8 +1,7 @@ """Light for Shelly.""" from __future__ import annotations -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import Block @@ -42,6 +41,7 @@ from .const import ( KELVIN_MIN_VALUE_COLOR, KELVIN_MIN_VALUE_WHITE, LIGHT_TRANSITION_MIN_FIRMWARE_DATE, + LOGGER, MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, RGBW_MODELS, @@ -58,8 +58,6 @@ from .utils import ( is_rpc_channel_type_light, ) -_LOGGER: Final = logging.getLogger(__name__) - MIRED_MAX_VALUE_WHITE = color_temperature_kelvin_to_mired(KELVIN_MIN_VALUE_WHITE) MIRED_MIN_VALUE = color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE) MIRED_MAX_VALUE_COLOR = color_temperature_kelvin_to_mired(KELVIN_MIN_VALUE_COLOR) @@ -348,7 +346,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): k for k, v in effect_dict.items() if v == kwargs[ATTR_EFFECT] ][0] else: - _LOGGER.error( + LOGGER.error( "Effect '%s' not supported by device %s", kwargs[ATTR_EFFECT], self.wrapper.model, diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 27773c629c0..bfac4cd4033 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -import logging from typing import Any, Final, cast import async_timeout @@ -20,7 +19,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry -from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC, CONF_SLEEP_PERIOD, LOGGER from .entity import ( BlockEntityDescription, ShellySleepingBlockAttributeEntity, @@ -28,8 +27,6 @@ from .entity import ( ) from .utils import get_device_entry_gen -_LOGGER: Final = logging.getLogger(__name__) - @dataclass class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription): @@ -119,12 +116,12 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity): async def _set_state_full_path(self, path: str, params: Any) -> Any: """Set block state (HTTP request).""" - _LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) + LOGGER.debug("Setting state for entity %s, state: %s", self.name, params) try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await self.wrapper.device.http_request("get", path, params) except (asyncio.TimeoutError, OSError) as err: - _LOGGER.error( + LOGGER.error( "Setting state for entity %s failed, state: %s, error: %s", self.name, params, diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 7a41c914e8a..dd07d17e419 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -2,8 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta -import logging -from typing import Any, Final, cast +from typing import Any, cast from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice from aioshelly.const import MODEL_NAMES @@ -21,6 +20,7 @@ from .const import ( CONF_COAP_PORT, DEFAULT_COAP_PORT, DOMAIN, + LOGGER, MAX_RPC_KEY_INSTANCES, RPC_INPUTS_EVENTS_TYPES, SHBTN_INPUTS_EVENTS_TYPES, @@ -29,8 +29,6 @@ from .const import ( UPTIME_DEVIATION, ) -_LOGGER: Final = logging.getLogger(__name__) - async def async_remove_shelly_entity( hass: HomeAssistant, domain: str, unique_id: str @@ -39,7 +37,7 @@ async def async_remove_shelly_entity( entity_reg = await hass.helpers.entity_registry.async_get_registry() entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) if entity_id: - _LOGGER.debug("Removing entity: %s", entity_id) + LOGGER.debug("Removing entity: %s", entity_id) entity_reg.async_remove(entity_id) @@ -220,7 +218,7 @@ async def get_coap_context(hass: HomeAssistant) -> COAP: port = hass.data[DOMAIN].get(CONF_COAP_PORT, DEFAULT_COAP_PORT) else: port = DEFAULT_COAP_PORT - _LOGGER.info("Starting CoAP context with UDP port %s", port) + LOGGER.info("Starting CoAP context with UDP port %s", port) await context.initialize(port) @callback @@ -362,7 +360,7 @@ def device_update_info( ) -> None: """Update device registry info.""" - _LOGGER.debug("Updating device registry info for %s", entry.title) + LOGGER.debug("Updating device registry info for %s", entry.title) assert entry.unique_id From de31e576b71889d895d3832cf2abc18513e41556 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 13 Mar 2022 06:52:41 +1300 Subject: [PATCH 0385/1054] Bump python-juicenet to 1.1.0 (#67992) --- homeassistant/components/juicenet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 35e9414a1e6..a080ba77c4d 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -2,7 +2,7 @@ "domain": "juicenet", "name": "JuiceNet", "documentation": "https://www.home-assistant.io/integrations/juicenet", - "requirements": ["python-juicenet==1.0.2"], + "requirements": ["python-juicenet==1.1.0"], "codeowners": ["@jesserockz"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 76824c14796..d796451c105 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1885,7 +1885,7 @@ python-izone==1.2.3 python-join-api==0.0.6 # homeassistant.components.juicenet -python-juicenet==1.0.2 +python-juicenet==1.1.0 # homeassistant.components.tplink python-kasa==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f67bac50270..254b306c727 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1215,7 +1215,7 @@ python-forecastio==1.4.0 python-izone==1.2.3 # homeassistant.components.juicenet -python-juicenet==1.0.2 +python-juicenet==1.1.0 # homeassistant.components.tplink python-kasa==0.4.1 From 6124081ddcd116140136acdf0e5f8c27952d0483 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Sat, 12 Mar 2022 13:42:28 -0500 Subject: [PATCH 0386/1054] Fix turning amcrest camera on and off (#68050) --- homeassistant/components/amcrest/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 3846f4945a9..49d5cfd7afd 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -548,7 +548,7 @@ class AmcrestCam(Camera): # recording on if video stream is being turned off. if self.is_recording and not enable: await self._async_enable_recording(False) - await self._async_change_setting(enable, "video", "is_streaming") + await self._async_change_setting(enable, "video", "_attr_is_streaming") if self._control_light: await self._async_change_light() From 8e3454e46a1f74f6650d4a2030bcefb3c7c37cdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Mar 2022 08:47:14 -1000 Subject: [PATCH 0387/1054] Remove legacy compatiblity for camera platforms that do not support width/height (#68039) - async_camera_image or camera_image must accept the width and height arguments --- homeassistant/components/camera/__init__.py | 38 +++------------------ tests/components/camera/test_init.py | 22 ------------ 2 files changed, 5 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b955f1a0249..f3258681b52 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial import hashlib -import inspect import logging import os from random import SystemRandom @@ -161,18 +160,9 @@ async def _async_get_image( """ with suppress(asyncio.CancelledError, asyncio.TimeoutError): async with async_timeout.timeout(timeout): - # Calling inspect will be removed in 2022.1 after all - # custom components have had a chance to change their signature - sig = inspect.signature(camera.async_camera_image) - if "height" in sig.parameters and "width" in sig.parameters: - image_bytes = await camera.async_camera_image( - width=width, height=height - ) - else: - camera.async_warn_old_async_camera_image_signature() - image_bytes = await camera.async_camera_image() - - if image_bytes: + if image_bytes := await camera.async_camera_image( + width=width, height=height + ): content_type = camera.content_type image = Image(content_type, image_bytes) if ( @@ -576,27 +566,9 @@ class Camera(Entity): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - sig = inspect.signature(self.camera_image) - # Calling inspect will be removed in 2022.1 after all - # custom components have had a chance to change their signature - if "height" in sig.parameters and "width" in sig.parameters: - return await self.hass.async_add_executor_job( - partial(self.camera_image, width=width, height=height) - ) - self.async_warn_old_async_camera_image_signature() - return await self.hass.async_add_executor_job(self.camera_image) - - # Remove in 2022.1 after all custom components have had a chance to change their signature - @callback - def async_warn_old_async_camera_image_signature(self) -> None: - """Warn once when calling async_camera_image with the function old signature.""" - if self._warned_old_signature: - return - _LOGGER.warning( - "The camera entity %s does not support requesting width and height, please open an issue with the integration author", - self.entity_id, + return await self.hass.async_add_executor_job( + partial(self.camera_image, width=width, height=height) ) - self._warned_old_signature = True async def handle_async_still_stream( self, request: web.Request, interval: float diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 0e53e163404..e72941ef488 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -108,28 +108,6 @@ async def test_get_image_from_camera(hass, image_mock_url): assert image.content == b"Test" -async def test_legacy_async_get_image_signature_warns_only_once( - hass, image_mock_url, caplog -): - """Test that we only warn once when we encounter a legacy async_get_image function signature.""" - - async def _legacy_async_camera_image(self): - return b"Image" - - with patch( - "homeassistant.components.demo.camera.DemoCamera.async_camera_image", - new=_legacy_async_camera_image, - ): - image = await camera.async_get_image(hass, "camera.demo_camera") - assert image.content == b"Image" - assert "does not support requesting width and height" in caplog.text - caplog.clear() - - image = await camera.async_get_image(hass, "camera.demo_camera") - assert image.content == b"Image" - assert "does not support requesting width and height" not in caplog.text - - async def test_get_image_from_camera_with_width_height(hass, image_mock_url): """Grab an image from camera entity with width and height.""" From 6831be67f4eeabe24b7ca1414c7c813581c50a5b Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Sat, 12 Mar 2022 14:58:03 -0500 Subject: [PATCH 0388/1054] Add number entities to control SleepIQ actuator positions (#67770) --- homeassistant/components/sleepiq/const.py | 4 +- .../components/sleepiq/coordinator.py | 8 +- homeassistant/components/sleepiq/entity.py | 4 +- homeassistant/components/sleepiq/number.py | 147 +++++++++++++++--- tests/components/sleepiq/conftest.py | 35 ++++- tests/components/sleepiq/test_number.py | 72 ++++++++- 6 files changed, 239 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/sleepiq/const.py b/homeassistant/components/sleepiq/const.py index 2aabf29ef54..4eb6148f9b8 100644 --- a/homeassistant/components/sleepiq/const.py +++ b/homeassistant/components/sleepiq/const.py @@ -3,6 +3,7 @@ DATA_SLEEPIQ = "data_sleepiq" DOMAIN = "sleepiq" +ACTUATOR = "actuator" BED = "bed" FIRMNESS = "firmness" ICON_EMPTY = "mdi:bed-empty" @@ -10,7 +11,8 @@ ICON_OCCUPIED = "mdi:bed" IS_IN_BED = "is_in_bed" PRESSURE = "pressure" SLEEP_NUMBER = "sleep_number" -SENSOR_TYPES = { +ENTITY_TYPES = { + ACTUATOR: "Position", FIRMNESS: "Firmness", PRESSURE: "Pressure", IS_IN_BED: "Is In Bed", diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index ef84b17ba9b..47512d23ce9 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -34,9 +34,11 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): self.client = client async def _async_update_data(self) -> None: - tasks = [self.client.fetch_bed_statuses()] + [ - bed.foundation.update_lights() for bed in self.client.beds.values() - ] + tasks = ( + [self.client.fetch_bed_statuses()] + + [bed.foundation.update_lights() for bed in self.client.beds.values()] + + [bed.foundation.update_actuators() for bed in self.client.beds.values()] + ) await asyncio.gather(*tasks) diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index 7fa14f2cbe7..e610119e2a0 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ICON_OCCUPIED, SENSOR_TYPES +from .const import ENTITY_TYPES, ICON_OCCUPIED def device_from_bed(bed: SleepIQBed) -> DeviceInfo: @@ -77,5 +77,5 @@ class SleepIQSleeperEntity(SleepIQBedEntity): self.sleeper = sleeper super().__init__(coordinator, bed) - self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {SENSOR_TYPES[name]}" + self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[name]}" self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 39140b51a5b..fb17336ccb3 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -1,15 +1,98 @@ """Support for SleepIQ SleepNumber firmness number entities.""" -from asyncsleepiq import SleepIQBed, SleepIQSleeper +from __future__ import annotations -from homeassistant.components.number import NumberEntity +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any, cast + +from asyncsleepiq import SleepIQActuator, SleepIQBed, SleepIQSleeper + +from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, FIRMNESS +from .const import ACTUATOR, DOMAIN, ENTITY_TYPES, FIRMNESS, ICON_OCCUPIED from .coordinator import SleepIQData -from .entity import SleepIQSleeperEntity +from .entity import SleepIQBedEntity + + +@dataclass +class SleepIQNumberEntityDescriptionMixin: + """Mixin to describe a SleepIQ number entity.""" + + value_fn: Callable[[Any], float] + set_value_fn: Callable[[Any, int], Coroutine[None, None, None]] + get_name_fn: Callable[[SleepIQBed, Any], str] + get_unique_id_fn: Callable[[SleepIQBed, Any], str] + + +@dataclass +class SleepIQNumberEntityDescription( + NumberEntityDescription, SleepIQNumberEntityDescriptionMixin +): + """Class to describe a SleepIQ number entity.""" + + +async def _async_set_firmness(sleeper: SleepIQSleeper, firmness: int) -> None: + await sleeper.set_sleepnumber(firmness) + + +async def _async_set_actuator_position( + actuator: SleepIQActuator, position: int +) -> None: + await actuator.set_position(position) + + +def _get_actuator_name(bed: SleepIQBed, actuator: SleepIQActuator) -> str: + if actuator.side: + return f"SleepNumber {bed.name} {actuator.side_full} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}" + + return f"SleepNumber {bed.name} {actuator.actuator_full} {ENTITY_TYPES[ACTUATOR]}" + + +def _get_actuator_unique_id(bed: SleepIQBed, actuator: SleepIQActuator) -> str: + if actuator.side: + return f"{bed.id}_{actuator.side}_{actuator.actuator}" + + return f"{bed.id}_{actuator.actuator}" + + +def _get_sleeper_name(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str: + return f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[FIRMNESS]}" + + +def _get_sleeper_unique_id(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str: + return f"{sleeper.sleeper_id}_{FIRMNESS}" + + +NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = { + FIRMNESS: SleepIQNumberEntityDescription( + key=FIRMNESS, + min_value=5, + max_value=100, + step=5, + name=ENTITY_TYPES[FIRMNESS], + icon=ICON_OCCUPIED, + value_fn=lambda sleeper: cast(float, sleeper.sleep_number), + set_value_fn=_async_set_firmness, + get_name_fn=_get_sleeper_name, + get_unique_id_fn=_get_sleeper_unique_id, + ), + ACTUATOR: SleepIQNumberEntityDescription( + key=ACTUATOR, + min_value=0, + max_value=100, + step=1, + name=ENTITY_TYPES[ACTUATOR], + icon=ICON_OCCUPIED, + value_fn=lambda actuator: cast(float, actuator.position), + set_value_fn=_async_set_actuator_position, + get_name_fn=_get_actuator_name, + get_unique_id_fn=_get_actuator_unique_id, + ), +} async def async_setup_entry( @@ -19,37 +102,59 @@ async def async_setup_entry( ) -> None: """Set up the SleepIQ bed sensors.""" data: SleepIQData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - SleepNumberFirmnessEntity(data.data_coordinator, bed, sleeper) - for bed in data.client.beds.values() - for sleeper in bed.sleepers - ) + + entities = [] + for bed in data.client.beds.values(): + for sleeper in bed.sleepers: + entities.append( + SleepIQNumberEntity( + data.data_coordinator, + bed, + sleeper, + NUMBER_DESCRIPTIONS[FIRMNESS], + ) + ) + for actuator in bed.foundation.actuators: + entities.append( + SleepIQNumberEntity( + data.data_coordinator, + bed, + actuator, + NUMBER_DESCRIPTIONS[ACTUATOR], + ) + ) + + async_add_entities(entities) -class SleepNumberFirmnessEntity(SleepIQSleeperEntity, NumberEntity): - """Representation of an SleepIQ Entity with CoordinatorEntity.""" +class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): + """Representation of a SleepIQ number entity.""" _attr_icon = "mdi:bed" - _attr_max_value: float = 100 - _attr_min_value: float = 5 - _attr_step: float = 5 def __init__( self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, - sleeper: SleepIQSleeper, + device: Any, + description: SleepIQNumberEntityDescription, ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator, bed, sleeper, FIRMNESS) + """Initialize the number.""" + self.description = description + self.device = device + + self._attr_name = description.get_name_fn(bed, device) + self._attr_unique_id = description.get_unique_id_fn(bed, device) + + super().__init__(coordinator, bed) @callback def _async_update_attrs(self) -> None: - """Update sensor attributes.""" - self._attr_value = float(self.sleeper.sleep_number) + """Update number attributes.""" + self._attr_value = float(self.description.value_fn(self.device)) async def async_set_value(self, value: float) -> None: - """Set the firmness value.""" - await self.sleeper.set_sleepnumber(int(value)) + """Set the number value.""" + await self.description.set_value_fn(self.device, int(value)) self._attr_value = value self.async_write_ha_state() diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 5b51b0f4670..a6f6ba78aba 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -3,7 +3,13 @@ from __future__ import annotations from unittest.mock import create_autospec, patch -from asyncsleepiq import SleepIQBed, SleepIQFoundation, SleepIQLight, SleepIQSleeper +from asyncsleepiq import ( + SleepIQActuator, + SleepIQBed, + SleepIQFoundation, + SleepIQLight, + SleepIQSleeper, +) import pytest from homeassistant.components.sleepiq import DOMAIN @@ -16,12 +22,13 @@ from tests.common import MockConfigEntry BED_ID = "123456" BED_NAME = "Test Bed" BED_NAME_LOWER = BED_NAME.lower().replace(" ", "_") +SLEEPER_L_ID = "98765" +SLEEPER_R_ID = "43219" SLEEPER_L_NAME = "SleeperL" SLEEPER_R_NAME = "Sleeper R" SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") - SLEEPIQ_CONFIG = { CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", @@ -49,12 +56,14 @@ def mock_asyncsleepiq(): sleeper_l.in_bed = True sleeper_l.sleep_number = 40 sleeper_l.pressure = 1000 + sleeper_l.sleeper_id = SLEEPER_L_ID sleeper_r.side = "R" sleeper_r.name = SLEEPER_R_NAME sleeper_r.in_bed = False sleeper_r.sleep_number = 80 sleeper_r.pressure = 1400 + sleeper_r.sleeper_id = SLEEPER_R_ID bed.foundation = create_autospec(SleepIQFoundation) light_1 = create_autospec(SleepIQLight) @@ -65,6 +74,28 @@ def mock_asyncsleepiq(): light_2.is_on = False bed.foundation.lights = [light_1, light_2] + actuator_h_r = create_autospec(SleepIQActuator) + actuator_h_l = create_autospec(SleepIQActuator) + actuator_f = create_autospec(SleepIQActuator) + bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f] + + actuator_h_r.side = "R" + actuator_h_r.side_full = "Right" + actuator_h_r.actuator = "H" + actuator_h_r.actuator_full = "Head" + actuator_h_r.position = 60 + + actuator_h_l.side = "L" + actuator_h_l.side_full = "Left" + actuator_h_l.actuator = "H" + actuator_h_l.actuator_full = "Head" + actuator_h_l.position = 50 + + actuator_f.side = None + actuator_f.actuator = "F" + actuator_f.actuator_full = "Foot" + actuator_f.position = 10 + yield client diff --git a/tests/components/sleepiq/test_number.py b/tests/components/sleepiq/test_number.py index f00bef2b4cb..be9221f3b12 100644 --- a/tests/components/sleepiq/test_number.py +++ b/tests/components/sleepiq/test_number.py @@ -8,8 +8,10 @@ from tests.components.sleepiq.conftest import ( BED_ID, BED_NAME, BED_NAME_LOWER, + SLEEPER_L_ID, SLEEPER_L_NAME, SLEEPER_L_NAME_LOWER, + SLEEPER_R_ID, SLEEPER_R_NAME, SLEEPER_R_NAME_LOWER, setup_platform, @@ -35,7 +37,7 @@ async def test_firmness(hass, mock_asyncsleepiq): f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_firmness" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_firmness" + assert entry.unique_id == f"{SLEEPER_L_ID}_firmness" state = hass.states.get( f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness" @@ -51,7 +53,7 @@ async def test_firmness(hass, mock_asyncsleepiq): f"number.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_firmness" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_firmness" + assert entry.unique_id == f"{SLEEPER_R_ID}_firmness" await hass.services.async_call( DOMAIN, @@ -66,3 +68,69 @@ async def test_firmness(hass, mock_asyncsleepiq): mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_once() mock_asyncsleepiq.beds[BED_ID].sleepers[0].set_sleepnumber.assert_called_with(42) + + +async def test_actuators(hass, mock_asyncsleepiq): + """Test the SleepIQ actuator position values for a bed with adjustable head and foot.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position") + assert state.state == "60.0" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Right Head Position" + ) + + entry = entity_registry.async_get( + f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_R_H" + + state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_left_head_position") + assert state.state == "50.0" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Left Head Position" + ) + + entry = entity_registry.async_get( + f"number.sleepnumber_{BED_NAME_LOWER}_left_head_position" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_L_H" + + state = hass.states.get(f"number.sleepnumber_{BED_NAME_LOWER}_foot_position") + assert state.state == "10.0" + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foot Position" + ) + + entry = entity_registry.async_get( + f"number.sleepnumber_{BED_NAME_LOWER}_foot_position" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_F" + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: f"number.sleepnumber_{BED_NAME_LOWER}_right_head_position", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].foundation.actuators[ + 0 + ].set_position.assert_called_once() + mock_asyncsleepiq.beds[BED_ID].foundation.actuators[ + 0 + ].set_position.assert_called_with(42) From eccf8c76fd9219d6d9321c4b0e8522dcee4e9d23 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 12 Mar 2022 22:21:45 +0200 Subject: [PATCH 0389/1054] Fix Shelly EM/3EM invalid energy value after reboot (#68052) --- homeassistant/components/shelly/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 21a7447e2b2..7e19b9724d7 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -174,6 +174,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + available=lambda block: cast(int, block.energy) != -1, ), ("emeter", "energyReturned"): BlockSensorDescription( key="emeter|energyReturned", @@ -182,6 +183,7 @@ SENSORS: Final = { value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + available=lambda block: cast(int, block.energyReturned) != -1, ), ("light", "energy"): BlockSensorDescription( key="light|energy", From 03a155af8358276d301591d1d19bddae8827e0c1 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 12 Mar 2022 21:45:24 +0100 Subject: [PATCH 0390/1054] Add sensors to Sensibo for motion sensor (#67748) --- .coveragerc | 1 + homeassistant/components/sensibo/climate.py | 4 +- homeassistant/components/sensibo/const.py | 2 +- .../components/sensibo/coordinator.py | 34 +++-- homeassistant/components/sensibo/entity.py | 71 ++++++++-- homeassistant/components/sensibo/number.py | 4 +- homeassistant/components/sensibo/select.py | 4 +- homeassistant/components/sensibo/sensor.py | 133 ++++++++++++++++++ 8 files changed, 217 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/sensibo/sensor.py diff --git a/.coveragerc b/.coveragerc index 26b24bfbe00..a10f3ca997f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1017,6 +1017,7 @@ omit = homeassistant/components/sensibo/entity.py homeassistant/components/sensibo/number.py homeassistant/components/sensibo/select.py + homeassistant/components/sensibo/sensor.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 669fa7a9543..9299952024c 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -37,7 +37,7 @@ from homeassistant.util.temperature import convert as convert_temperature from .const import ALL, DOMAIN, LOGGER from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboBaseEntity +from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" @@ -119,7 +119,7 @@ async def async_setup_entry( ) -class SensiboClimate(SensiboBaseEntity, ClimateEntity): +class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Representation of a Sensibo device.""" def __init__( diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 9558376f079..d39f941d63e 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -12,7 +12,7 @@ LOGGER = logging.getLogger(__package__) DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" -PLATFORMS = [Platform.CLIMATE, Platform.NUMBER, Platform.SELECT] +PLATFORMS = [Platform.CLIMATE, Platform.NUMBER, Platform.SELECT, Platform.SENSOR] ALL = ["all"] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index bceede8664e..7d37ae7f235 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -26,6 +26,7 @@ class MotionSensor: id: str alive: bool | None = None + motion: bool | None = None fw_ver: str | None = None fw_type: str | None = None is_main_sensor: bool | None = None @@ -33,6 +34,7 @@ class MotionSensor: humidity: int | None = None temperature: float | None = None model: str | None = None + rssi: int | None = None @dataclass @@ -147,21 +149,23 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): temperature = measurements.get("temperature") humidity = measurements.get("humidity") - motion_sensors = [ - MotionSensor( - id=motionsensor["id"], - alive=motionsensor["connectionStatus"].get("isAlive"), - fw_ver=motionsensor.get("firmwareVersion"), - fw_type=motionsensor.get("firmwareType"), - is_main_sensor=motionsensor.get("isMainSensor"), - battery_voltage=motionsensor["measurements"].get("batteryVoltage"), - humidity=motionsensor["measurements"].get("humidity"), - temperature=motionsensor["measurements"].get("temperature"), - model=motionsensor.get("productModel"), - ) - for motionsensor in dev["motionSensors"] - if dev["motionSensors"] - ] + motion_sensors: dict[str, Any] = {} + if dev["motionSensors"]: + for sensor in dev["motionSensors"]: + measurement = sensor["measurements"] + motion_sensors[sensor["id"]] = MotionSensor( + id=sensor["id"], + alive=sensor["connectionStatus"].get("isAlive"), + motion=measurement.get("motion"), + fw_ver=sensor.get("firmwareVersion"), + fw_type=sensor.get("firmwareType"), + is_main_sensor=sensor.get("isMainSensor"), + battery_voltage=measurement.get("batteryVoltage"), + humidity=measurement.get("humidity"), + temperature=measurement.get("temperature"), + model=sensor.get("productModel"), + rssi=measurement.get("rssi"), + ) device_data[unique_id] = { "id": unique_id, diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 026cca4ddff..a2a2bbf3a0e 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -11,11 +11,11 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT -from .coordinator import SensiboDataUpdateCoordinator +from .coordinator import MotionSensor, SensiboDataUpdateCoordinator class SensiboBaseEntity(CoordinatorEntity): - """Representation of a Sensibo numbers.""" + """Representation of a Sensibo entity.""" coordinator: SensiboDataUpdateCoordinator @@ -28,24 +28,35 @@ class SensiboBaseEntity(CoordinatorEntity): super().__init__(coordinator) self._device_id = device_id self._client = coordinator.client - device = coordinator.data.parsed[device_id] - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device["id"])}, - name=device["name"], - connections={(CONNECTION_NETWORK_MAC, device["mac"])}, - manufacturer="Sensibo", - configuration_url="https://home.sensibo.com/", - model=device["model"], - sw_version=device["fw_ver"], - hw_version=device["fw_type"], - suggested_area=device["name"], - ) @property def device_data(self) -> dict[str, Any]: """Return data for device.""" return self.coordinator.data.parsed[self._device_id] + +class SensiboDeviceBaseEntity(SensiboBaseEntity): + """Representation of a Sensibo device.""" + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator, device_id) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.device_data["id"])}, + name=self.device_data["name"], + connections={(CONNECTION_NETWORK_MAC, self.device_data["mac"])}, + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=self.device_data["model"], + sw_version=self.device_data["fw_ver"], + hw_version=self.device_data["fw_type"], + suggested_area=self.device_data["name"], + ) + async def async_send_command( self, command: str, params: dict[str, Any] ) -> dict[str, Any]: @@ -80,3 +91,35 @@ class SensiboBaseEntity(CoordinatorEntity): params["assumed_state"], ) return result + + +class SensiboMotionBaseEntity(SensiboBaseEntity): + """Representation of a Sensibo motion entity.""" + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + sensor_id: str, + sensor_data: MotionSensor, + name: str | None, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator, device_id) + self._sensor_id = sensor_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sensor_id)}, + name=f"{self.device_data['name']} Motion Sensor {name}", + via_device=(DOMAIN, device_id), + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=sensor_data.model, + sw_version=sensor_data.fw_ver, + hw_version=sensor_data.fw_type, + ) + + @property + def sensor_data(self) -> MotionSensor: + """Return data for device.""" + return self.device_data["motion_sensors"][self._sensor_id] diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 2aa38a41a7b..d3cc4d57830 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboBaseEntity +from .entity import SensiboDeviceBaseEntity @dataclass @@ -70,7 +70,7 @@ async def async_setup_entry( ) -class SensiboNumber(SensiboBaseEntity, NumberEntity): +class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): """Representation of a Sensibo numbers.""" entity_description: SensiboNumberEntityDescription diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index 3f615f06afe..cb569620cca 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator -from .entity import SensiboBaseEntity +from .entity import SensiboDeviceBaseEntity @dataclass @@ -62,7 +62,7 @@ async def async_setup_entry( ) -class SensiboSelect(SensiboBaseEntity, SelectEntity): +class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): """Representation of a Sensibo Select.""" entity_description: SensiboSelectEntityDescription diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py new file mode 100644 index 00000000000..cb5833ee985 --- /dev/null +++ b/homeassistant/components/sensibo/sensor.py @@ -0,0 +1,133 @@ +"""Sensor platform for Sensibo integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import MotionSensor, SensiboDataUpdateCoordinator +from .entity import SensiboMotionBaseEntity + + +@dataclass +class BaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[MotionSensor], StateType] + + +@dataclass +class SensiboSensorEntityDescription( + SensorEntityDescription, BaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +MOTION_SENSOR_TYPES: tuple[SensiboSensorEntityDescription, ...] = ( + SensiboSensorEntityDescription( + key="rssi", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + name="rssi", + icon="mdi:wifi", + value_fn=lambda data: data.rssi, + entity_registry_enabled_default=False, + ), + SensiboSensorEntityDescription( + key="battery_voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + name="Battery Voltage", + icon="mdi:battery", + value_fn=lambda data: data.battery_voltage, + ), + SensiboSensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + name="Humidity", + icon="mdi:water", + value_fn=lambda data: data.humidity, + ), + SensiboSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + name="Temperature", + icon="mdi:thermometer", + value_fn=lambda data: data.temperature, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) + for device_id, device_data in coordinator.data.parsed.items() + for sensor_id, sensor_data in device_data["motion_sensors"].items() + for description in MOTION_SENSOR_TYPES + if device_data["motion_sensors"] + ) + + +class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): + """Representation of a Sensibo Motion Sensor.""" + + entity_description: SensiboSensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + sensor_id: str, + sensor_data: MotionSensor, + entity_description: SensiboSensorEntityDescription, + ) -> None: + """Initiate Sensibo Motion Sensor.""" + super().__init__( + coordinator, + device_id, + sensor_id, + sensor_data, + entity_description.name, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{sensor_id}-{entity_description.key}" + self._attr_name = ( + f"{self.device_data['name']} Motion Sensor {entity_description.name}" + ) + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.entity_description.value_fn(self.sensor_data) From ea6da674df97758e31b1f77a64a183b9f91016e0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Mar 2022 11:30:27 -1000 Subject: [PATCH 0391/1054] Remove unneeded permissions check from subscribe entities (#68044) --- homeassistant/components/websocket_api/commands.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index e64ba46beb7..02121845ad6 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -278,13 +278,6 @@ def handle_subscribe_entities( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle subscribe entities command.""" - # Circular dep - # pylint: disable=import-outside-toplevel - from .permissions import SUBSCRIBE_ALLOWLIST - - if "state_changed" not in SUBSCRIBE_ALLOWLIST and not connection.user.is_admin: - raise Unauthorized - entity_ids = set(msg.get("entity_ids", [])) @callback From a902f0ee53ce6bf8baf8c47974c8a5f6422b7acd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2022 13:53:40 -0800 Subject: [PATCH 0392/1054] Fix switch light adding itself to devices (#68060) --- homeassistant/components/switch_as_x/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py index e6fc334caef..53128487d5c 100644 --- a/homeassistant/components/switch_as_x/light.py +++ b/homeassistant/components/switch_as_x/light.py @@ -113,4 +113,5 @@ class LightSwitch(LightEntity): # Add this entity to the wrapped switch's device registry = er.async_get(self.hass) - registry.async_update_entity(self.entity_id, device_id=self._device_id) + if registry.async_get(self.entity_id) is not None: + registry.async_update_entity(self.entity_id, device_id=self._device_id) From e59bf4f3afdea65cd2344469e4a9a06a1ab0a1f5 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Sat, 12 Mar 2022 17:54:26 -0500 Subject: [PATCH 0393/1054] Migrate SleepIQ unique IDs that are using sleeper name instead of sleeper ID (#68062) --- homeassistant/components/sleepiq/__init__.py | 57 +++++++++++++- homeassistant/components/sleepiq/entity.py | 2 +- .../components/sleepiq/test_binary_sensor.py | 7 +- tests/components/sleepiq/test_init.py | 76 ++++++++++++++++++- tests/components/sleepiq/test_sensor.py | 11 +-- 5 files changed, 138 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 17370d1b463..9b5c9c81683 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,5 +1,8 @@ """Support for SleepIQ from SleepNumber.""" +from __future__ import annotations + import logging +from typing import Any from asyncsleepiq import ( AsyncSleepIQ, @@ -10,14 +13,15 @@ from asyncsleepiq import ( import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, PRESSURE, Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER from .coordinator import ( SleepIQData, SleepIQDataUpdateCoordinator, @@ -87,6 +91,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SleepIQAPIException as err: raise ConfigEntryNotReady(str(err) or "Error reading from SleepIQ API") from err + await _async_migrate_unique_ids(hass, entry, gateway) + coordinator = SleepIQDataUpdateCoordinator(hass, gateway, email) pause_coordinator = SleepIQPauseUpdateCoordinator(hass, gateway, email) @@ -110,3 +116,48 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def _async_migrate_unique_ids( + hass: HomeAssistant, entry: ConfigEntry, gateway: AsyncSleepIQ +) -> None: + """Migrate old unique ids.""" + names_to_ids = { + sleeper.name: sleeper.sleeper_id + for bed in gateway.beds.values() + for sleeper in bed.sleepers + } + + bed_ids = {bed.id for bed in gateway.beds.values()} + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: + # Old format for sleeper entities was {bed_id}_{sleeper.name}_{sensor_type}..... + # New format is {sleeper.sleeper_id}_{sensor_type}.... + sensor_types = [IS_IN_BED, PRESSURE, SLEEP_NUMBER] + + old_unique_id = entity_entry.unique_id + parts = old_unique_id.split("_") + + # If it doesn't begin with a bed id or end with one of the sensor types, + # it doesn't need to be migrated + if parts[0] not in bed_ids or not old_unique_id.endswith(tuple(sensor_types)): + return None + + sensor_type = next(filter(old_unique_id.endswith, sensor_types), None) + sleeper_name = "_".join(parts[1:]).removesuffix(f"_{sensor_type}") + sleeper_id = names_to_ids.get(sleeper_name) + + if not sleeper_id: + return None + + new_unique_id = f"{sleeper_id}_{sensor_type}" + + _LOGGER.info( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index e610119e2a0..c73988ce638 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -78,4 +78,4 @@ class SleepIQSleeperEntity(SleepIQBedEntity): super().__init__(coordinator, bed) self._attr_name = f"SleepNumber {bed.name} {sleeper.name} {ENTITY_TYPES[name]}" - self._attr_unique_id = f"{bed.id}_{sleeper.name}_{name}" + self._attr_unique_id = f"{sleeper.sleeper_id}_{name}" diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index 2b265e19626..bce30ad2393 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -10,11 +10,12 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from tests.components.sleepiq.conftest import ( - BED_ID, BED_NAME, BED_NAME_LOWER, + SLEEPER_L_ID, SLEEPER_L_NAME, SLEEPER_L_NAME_LOWER, + SLEEPER_R_ID, SLEEPER_R_NAME, SLEEPER_R_NAME_LOWER, setup_platform, @@ -41,7 +42,7 @@ async def test_binary_sensors(hass, mock_asyncsleepiq): f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_is_in_bed" ) assert entity - assert entity.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_is_in_bed" + assert entity.unique_id == f"{SLEEPER_L_ID}_is_in_bed" state = hass.states.get( f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" @@ -58,4 +59,4 @@ async def test_binary_sensors(hass, mock_asyncsleepiq): f"binary_sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_is_in_bed" ) assert entity - assert entity.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_is_in_bed" + assert entity.unique_id == f"{SLEEPER_R_ID}_is_in_bed" diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 0aed23c4c50..e468734e063 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -5,14 +5,35 @@ from asyncsleepiq import ( SleepIQTimeoutException, ) -from homeassistant.components.sleepiq.const import DOMAIN +from homeassistant.components.sleepiq.const import ( + DOMAIN, + IS_IN_BED, + PRESSURE, + SLEEP_NUMBER, +) from homeassistant.components.sleepiq.coordinator import UPDATE_INTERVAL from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed -from tests.components.sleepiq.conftest import setup_platform +from tests.common import MockConfigEntry, async_fire_time_changed, mock_registry +from tests.components.sleepiq.conftest import ( + BED_ID, + SLEEPER_L_ID, + SLEEPER_L_NAME, + SLEEPER_L_NAME_LOWER, + SLEEPIQ_CONFIG, + setup_platform, +) + +ENTITY_IS_IN_BED = f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{IS_IN_BED}" +ENTITY_PRESSURE = f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{PRESSURE}" +ENTITY_SLEEP_NUMBER = ( + f"sensor.sleepnumber_{BED_ID}_{SLEEPER_L_NAME_LOWER}_{SLEEP_NUMBER}" +) async def test_unload_entry(hass: HomeAssistant, mock_asyncsleepiq) -> None: @@ -64,3 +85,52 @@ async def test_api_timeout(hass: HomeAssistant, mock_asyncsleepiq) -> None: mock_asyncsleepiq.init_beds.side_effect = SleepIQTimeoutException entry = await setup_platform(hass, None) assert not await hass.config_entries.async_setup(entry.entry_id) + + +async def test_unique_id_migration(hass: HomeAssistant, mock_asyncsleepiq) -> None: + """Test migration of sensor unique IDs.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data=SLEEPIQ_CONFIG, + unique_id=SLEEPIQ_CONFIG[CONF_USERNAME].lower(), + ) + + mock_entry.add_to_hass(hass) + + mock_registry( + hass, + { + ENTITY_IS_IN_BED: er.RegistryEntry( + entity_id=ENTITY_IS_IN_BED, + unique_id=f"{BED_ID}_{SLEEPER_L_NAME}_{IS_IN_BED}", + platform=DOMAIN, + config_entry_id=mock_entry.entry_id, + ), + ENTITY_PRESSURE: er.RegistryEntry( + entity_id=ENTITY_PRESSURE, + unique_id=f"{BED_ID}_{SLEEPER_L_NAME}_{PRESSURE}", + platform=DOMAIN, + config_entry_id=mock_entry.entry_id, + ), + ENTITY_SLEEP_NUMBER: er.RegistryEntry( + entity_id=ENTITY_SLEEP_NUMBER, + unique_id=f"{BED_ID}_{SLEEPER_L_NAME}_{SLEEP_NUMBER}", + platform=DOMAIN, + config_entry_id=mock_entry.entry_id, + ), + }, + ) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + ent_reg = er.async_get(hass) + + sensor_is_in_bed = ent_reg.async_get(ENTITY_IS_IN_BED) + assert sensor_is_in_bed.unique_id == f"{SLEEPER_L_ID}_{IS_IN_BED}" + + sensor_pressure = ent_reg.async_get(ENTITY_PRESSURE) + assert sensor_pressure.unique_id == f"{SLEEPER_L_ID}_{PRESSURE}" + + sensor_sleep_number = ent_reg.async_get(ENTITY_SLEEP_NUMBER) + assert sensor_sleep_number.unique_id == f"{SLEEPER_L_ID}_{SLEEP_NUMBER}" diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index c2d8648ebd5..68ee5319db6 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -4,11 +4,12 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.helpers import entity_registry as er from tests.components.sleepiq.conftest import ( - BED_ID, BED_NAME, BED_NAME_LOWER, + SLEEPER_L_ID, SLEEPER_L_NAME, SLEEPER_L_NAME_LOWER, + SLEEPER_R_ID, SLEEPER_R_NAME, SLEEPER_R_NAME_LOWER, setup_platform, @@ -34,7 +35,7 @@ async def test_sleepnumber_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_sleepnumber" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_sleep_number" + assert entry.unique_id == f"{SLEEPER_L_ID}_sleep_number" state = hass.states.get( f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" @@ -50,7 +51,7 @@ async def test_sleepnumber_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_sleepnumber" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_sleep_number" + assert entry.unique_id == f"{SLEEPER_R_ID}_sleep_number" async def test_pressure_sensors(hass, mock_asyncsleepiq): @@ -72,7 +73,7 @@ async def test_pressure_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_L_NAME_LOWER}_pressure" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_L_NAME}_pressure" + assert entry.unique_id == f"{SLEEPER_L_ID}_pressure" state = hass.states.get( f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_pressure" @@ -88,4 +89,4 @@ async def test_pressure_sensors(hass, mock_asyncsleepiq): f"sensor.sleepnumber_{BED_NAME_LOWER}_{SLEEPER_R_NAME_LOWER}_pressure" ) assert entry - assert entry.unique_id == f"{BED_ID}_{SLEEPER_R_NAME}_pressure" + assert entry.unique_id == f"{SLEEPER_R_ID}_pressure" From a911139c081b4dfb3f5eaa8c74ca69bcb6f33689 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Mar 2022 15:05:31 -0800 Subject: [PATCH 0394/1054] Bump frontend to 20220312.0 (#68061) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index baf61343040..61796f489b2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220301.1" + "home-assistant-frontend==20220312.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 25ba71bf1ee..31520803ff2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220301.1 +home-assistant-frontend==20220312.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index d796451c105..e5dfd69b4d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -806,7 +806,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.1 +home-assistant-frontend==20220312.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 254b306c727..dd27c3ff8d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220301.1 +home-assistant-frontend==20220312.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From a2ce395fc66727e746e1f3bbec1221223a3bebdd Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Sat, 12 Mar 2022 16:10:45 -0800 Subject: [PATCH 0395/1054] Add remote platform to Kaleidescape integration (#67959) --- .../components/kaleidescape/__init__.py | 2 +- .../components/kaleidescape/remote.py | 68 ++++++++ tests/components/kaleidescape/test_remote.py | 156 ++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/kaleidescape/remote.py create mode 100644 tests/components/kaleidescape/test_remote.py diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py index 64205ecf838..a66ae25d436 100644 --- a/homeassistant/components/kaleidescape/__init__.py +++ b/homeassistant/components/kaleidescape/__init__.py @@ -19,7 +19,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kaleidescape/remote.py b/homeassistant/components/kaleidescape/remote.py new file mode 100644 index 00000000000..61080052ee5 --- /dev/null +++ b/homeassistant/components/kaleidescape/remote.py @@ -0,0 +1,68 @@ +"""Sensor platform for Kaleidescape integration.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from kaleidescape import const as kaleidescape_const + +from homeassistant.components.remote import RemoteEntity +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN as KALEIDESCAPE_DOMAIN +from .entity import KaleidescapeEntity + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any + + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the platform from a config entry.""" + entities = [KaleidescapeRemote(hass.data[KALEIDESCAPE_DOMAIN][entry.entry_id])] + async_add_entities(entities) + + +VALID_COMMANDS = { + "select", + "up", + "down", + "left", + "right", + "cancel", + "replay", + "scan_forward", + "scan_reverse", + "go_movie_covers", + "menu_toggle", +} + + +class KaleidescapeRemote(KaleidescapeEntity, RemoteEntity): + """Representation of a Kaleidescape device.""" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_ON + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self._device.leave_standby() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self._device.enter_standby() + + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send a command to a device.""" + for cmd in command: + if cmd not in VALID_COMMANDS: + raise HomeAssistantError(f"{cmd} is not a known command") + await getattr(self._device, cmd)() diff --git a/tests/components/kaleidescape/test_remote.py b/tests/components/kaleidescape/test_remote.py new file mode 100644 index 00000000000..3573d04395d --- /dev/null +++ b/tests/components/kaleidescape/test_remote.py @@ -0,0 +1,156 @@ +"""Tests for Kaleidescape remote platform.""" + +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import MOCK_SERIAL + +from tests.common import MockConfigEntry + +ENTITY_ID = f"remote.kaleidescape_device_{MOCK_SERIAL}" + + +async def test_entity( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test entity attributes.""" + assert hass.states.get(ENTITY_ID) + + +async def test_commands( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test service calls.""" + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.leave_standby.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + assert mock_device.enter_standby.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["select"]}, + blocking=True, + ) + assert mock_device.select.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["up"]}, + blocking=True, + ) + assert mock_device.up.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["down"]}, + blocking=True, + ) + assert mock_device.down.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["left"]}, + blocking=True, + ) + assert mock_device.left.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["right"]}, + blocking=True, + ) + assert mock_device.right.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["cancel"]}, + blocking=True, + ) + assert mock_device.cancel.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["replay"]}, + blocking=True, + ) + assert mock_device.replay.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["scan_forward"]}, + blocking=True, + ) + assert mock_device.scan_forward.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["scan_reverse"]}, + blocking=True, + ) + assert mock_device.scan_reverse.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["go_movie_covers"]}, + blocking=True, + ) + assert mock_device.go_movie_covers.call_count == 1 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["menu_toggle"]}, + blocking=True, + ) + assert mock_device.menu_toggle.call_count == 1 + + +async def test_unknown_command( + hass: HomeAssistant, + mock_device: MagicMock, + mock_integration: MockConfigEntry, +) -> None: + """Test service calls.""" + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_COMMAND: ["bad"]}, + blocking=True, + ) + assert str(err.value) == "bad is not a known command" From c64b4d997b517875911e94623dd4324cc96a389f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 13 Mar 2022 00:18:58 +0000 Subject: [PATCH 0396/1054] [ci skip] Translation update --- .../components/abode/translations/fr.json | 2 +- .../accuweather/translations/fr.json | 2 +- .../components/adax/translations/fr.json | 4 +- .../components/aemet/translations/fr.json | 2 +- .../components/airly/translations/fr.json | 2 +- .../components/airnow/translations/fr.json | 2 +- .../components/airthings/translations/fr.json | 2 +- .../components/airvisual/translations/fr.json | 2 +- .../airvisual/translations/sensor.he.json | 3 +- .../components/airzone/translations/he.json | 18 +++ .../components/ambee/translations/fr.json | 2 +- .../ambient_station/translations/fr.json | 2 +- .../components/androidtv/translations/fr.json | 2 +- .../components/androidtv/translations/he.json | 10 +- .../components/apple_tv/translations/fr.json | 2 +- .../aseko_pool_live/translations/fr.json | 2 +- .../components/asuswrt/translations/fr.json | 2 +- .../components/august/translations/fr.json | 2 +- .../aussie_broadband/translations/fr.json | 4 +- .../components/axis/translations/fr.json | 2 +- .../azure_devops/translations/fr.json | 2 +- .../binary_sensor/translations/fr.json | 8 +- .../components/blink/translations/fr.json | 2 +- .../bmw_connected_drive/translations/fr.json | 2 +- .../components/bond/translations/fr.json | 2 +- .../components/bosch_shc/translations/fr.json | 2 +- .../components/braviatv/translations/fr.json | 2 +- .../components/broadlink/translations/fr.json | 4 +- .../components/brother/translations/fr.json | 2 +- .../components/brunt/translations/fr.json | 2 +- .../components/button/translations/fr.json | 2 +- .../components/climacell/translations/fr.json | 2 +- .../climacell/translations/sensor.he.json | 7 ++ .../cloudflare/translations/fr.json | 4 +- .../components/co2signal/translations/fr.json | 2 +- .../components/coinbase/translations/fr.json | 2 +- .../components/control4/translations/fr.json | 2 +- .../crownstone/translations/fr.json | 2 +- .../components/daikin/translations/fr.json | 6 +- .../components/deconz/translations/fr.json | 12 +- .../devolo_home_control/translations/fr.json | 2 +- .../devolo_home_network/translations/fr.json | 2 +- .../components/dexcom/translations/fr.json | 2 +- .../components/dlna_dmr/translations/fr.json | 2 +- .../components/dnsip/translations/fr.json | 4 +- .../components/doorbird/translations/en.json | 3 +- .../components/doorbird/translations/fr.json | 2 +- .../components/dunehd/translations/fr.json | 2 +- .../components/econet/translations/fr.json | 4 +- .../components/efergy/translations/en.json | 3 +- .../components/efergy/translations/fr.json | 2 +- .../components/elgato/translations/fr.json | 6 +- .../components/elkm1/translations/fr.json | 2 +- .../components/elmax/translations/fr.json | 4 +- .../components/enocean/translations/fr.json | 2 +- .../enphase_envoy/translations/fr.json | 2 +- .../environment_canada/translations/fr.json | 2 +- .../components/esphome/translations/fr.json | 2 +- .../components/ezviz/translations/fr.json | 4 +- .../fireservicerota/translations/fr.json | 4 +- .../flick_electric/translations/fr.json | 2 +- .../components/flipr/translations/fr.json | 2 +- .../components/flo/translations/fr.json | 2 +- .../components/flume/translations/fr.json | 2 +- .../components/foscam/translations/fr.json | 4 +- .../freedompro/translations/fr.json | 2 +- .../components/fritz/translations/fr.json | 2 +- .../components/fritzbox/translations/fr.json | 2 +- .../fritzbox_callmonitor/translations/fr.json | 2 +- .../components/fronius/translations/fr.json | 2 +- .../components/goalzero/translations/fr.json | 6 +- .../components/gogogate2/translations/fr.json | 2 +- .../components/group/translations/de.json | 65 ++++++++-- .../components/group/translations/he.json | 111 ++++++++++++++++++ .../components/group/translations/hu.json | 12 +- .../growatt_server/translations/fr.json | 2 +- .../components/habitica/translations/fr.json | 2 +- .../components/hlk_sw16/translations/fr.json | 2 +- .../homekit_controller/translations/fr.json | 6 +- .../components/honeywell/translations/fr.json | 2 +- .../huawei_lte/translations/fr.json | 4 +- .../components/hue/translations/fr.json | 10 +- .../huisbaasje/translations/fr.json | 2 +- .../hvv_departures/translations/fr.json | 2 +- .../components/hyperion/translations/fr.json | 2 +- .../components/iaqualink/translations/fr.json | 2 +- .../components/icloud/translations/fr.json | 2 +- .../components/insteon/translations/fr.json | 14 +-- .../components/iotawatt/translations/fr.json | 2 +- .../components/iqvia/translations/fr.json | 2 +- .../components/isy994/translations/fr.json | 2 +- .../components/jellyfin/translations/fr.json | 2 +- .../components/juicenet/translations/fr.json | 2 +- .../components/kmtronic/translations/fr.json | 2 +- .../components/knx/translations/he.json | 10 ++ .../components/kodi/translations/fr.json | 4 +- .../kostal_plenticore/translations/fr.json | 2 +- .../components/lcn/translations/fr.json | 4 +- .../components/life360/translations/fr.json | 6 +- .../components/light/translations/fr.json | 22 ++-- .../components/litejet/translations/fr.json | 2 +- .../litterrobot/translations/fr.json | 2 +- .../logi_circle/translations/fr.json | 2 +- .../components/luftdaten/translations/fr.json | 2 +- .../lutron_caseta/translations/fr.json | 6 +- .../components/mazda/translations/fr.json | 2 +- .../media_player/translations/fr.json | 4 +- .../components/melcloud/translations/fr.json | 2 +- .../components/mikrotik/translations/fr.json | 2 +- .../components/mjpeg/translations/fr.json | 4 +- .../mobile_app/translations/he.json | 8 ++ .../components/motioneye/translations/fr.json | 4 +- .../components/mqtt/translations/fr.json | 10 +- .../components/myq/translations/fr.json | 2 +- .../components/mysensors/translations/fr.json | 4 +- .../components/nam/translations/fr.json | 2 +- .../components/nest/translations/fr.json | 2 +- .../components/netatmo/translations/fr.json | 4 +- .../components/nexia/translations/fr.json | 2 +- .../nightscout/translations/fr.json | 2 +- .../nmap_tracker/translations/fr.json | 4 +- .../components/notion/translations/fr.json | 2 +- .../components/nuheat/translations/fr.json | 2 +- .../components/nuki/translations/fr.json | 2 +- .../components/omnilogic/translations/fr.json | 2 +- .../components/oncue/translations/fr.json | 2 +- .../opengarage/translations/fr.json | 2 +- .../components/openuv/translations/fr.json | 2 +- .../openweathermap/translations/fr.json | 2 +- .../components/overkiz/translations/fr.json | 2 +- .../ovo_energy/translations/fr.json | 2 +- .../components/owntracks/translations/fr.json | 2 +- .../philips_js/translations/fr.json | 2 +- .../components/picnic/translations/fr.json | 2 +- .../components/plugwise/translations/fr.json | 2 +- .../components/poolsense/translations/fr.json | 2 +- .../components/powerwall/translations/fr.json | 2 +- .../components/prosegur/translations/fr.json | 2 +- .../components/ps4/translations/fr.json | 6 +- .../components/pvoutput/translations/fr.json | 2 +- .../components/rachio/translations/fr.json | 2 +- .../rainforest_eagle/translations/fr.json | 2 +- .../rainmachine/translations/fr.json | 2 +- .../components/renault/translations/fr.json | 2 +- .../components/rfxtrx/translations/fr.json | 2 +- .../components/ridwell/translations/fr.json | 2 +- .../components/ring/translations/fr.json | 2 +- .../components/risco/translations/fr.json | 2 +- .../translations/fr.json | 2 +- .../components/roon/translations/fr.json | 2 +- .../components/rpi_power/translations/he.json | 1 + .../ruckus_unleashed/translations/fr.json | 2 +- .../components/sense/translations/fr.json | 2 +- .../components/sense/translations/he.json | 6 + .../components/senseme/translations/fr.json | 2 +- .../components/sensibo/translations/fr.json | 2 +- .../components/sensor/translations/fr.json | 22 ++-- .../components/sentry/translations/fr.json | 2 +- .../components/sharkiq/translations/fr.json | 2 +- .../components/shelly/translations/fr.json | 8 +- .../simplisafe/translations/fr.json | 2 +- .../components/sleepiq/translations/fr.json | 2 +- .../components/sma/translations/fr.json | 2 +- .../smart_meter_texas/translations/fr.json | 2 +- .../components/smarthab/translations/fr.json | 2 +- .../smartthings/translations/fr.json | 2 +- .../smartthings/translations/he.json | 9 +- .../components/smarttub/translations/fr.json | 2 +- .../components/solaredge/translations/fr.json | 2 +- .../somfy_mylink/translations/fr.json | 2 +- .../components/sonarr/translations/fr.json | 2 +- .../components/spider/translations/fr.json | 2 +- .../squeezebox/translations/fr.json | 2 +- .../srp_energy/translations/fr.json | 2 +- .../components/subaru/translations/fr.json | 2 +- .../surepetcare/translations/fr.json | 2 +- .../components/switch/translations/fr.json | 21 ++-- .../switch_as_x/translations/de.json | 14 +++ .../components/switchbot/translations/fr.json | 4 +- .../components/syncthing/translations/fr.json | 2 +- .../components/syncthru/translations/fr.json | 2 +- .../synology_dsm/translations/fr.json | 2 +- .../system_bridge/translations/fr.json | 2 +- .../components/tado/translations/fr.json | 2 +- .../components/tailscale/translations/fr.json | 2 +- .../tellduslive/translations/fr.json | 2 +- .../components/tile/translations/fr.json | 2 +- .../totalconnect/translations/fr.json | 2 +- .../components/tractive/translations/fr.json | 2 +- .../translations/fr.json | 2 +- .../transmission/translations/fr.json | 2 +- .../components/tuya/translations/fr.json | 6 +- .../components/unifi/translations/fr.json | 2 +- .../unifiprotect/translations/fr.json | 2 +- .../components/upb/translations/fr.json | 2 +- .../components/upcloud/translations/fr.json | 2 +- .../components/uptime/translations/ca.json | 13 ++ .../components/uptime/translations/de.json | 13 ++ .../components/uptime/translations/el.json | 13 ++ .../components/uptime/translations/et.json | 13 ++ .../components/uptime/translations/fr.json | 13 ++ .../components/uptime/translations/he.json | 13 ++ .../components/uptime/translations/it.json | 10 ++ .../uptimerobot/translations/fr.json | 2 +- .../components/vallox/translations/fr.json | 4 +- .../components/verisure/translations/fr.json | 2 +- .../components/vesync/translations/fr.json | 2 +- .../components/vicare/translations/fr.json | 2 +- .../components/vilfo/translations/fr.json | 2 +- .../vlc_telnet/translations/fr.json | 4 +- .../components/wallbox/translations/fr.json | 2 +- .../components/watttime/translations/fr.json | 2 +- .../components/whirlpool/translations/fr.json | 2 +- .../components/wilight/translations/fr.json | 4 +- .../components/wiz/translations/fr.json | 2 +- .../components/wolflink/translations/fr.json | 2 +- .../xiaomi_aqara/translations/fr.json | 2 +- .../yale_smart_alarm/translations/fr.json | 2 +- .../components/yeelight/translations/fr.json | 12 +- .../components/zha/translations/fr.json | 4 +- .../zoneminder/translations/fr.json | 4 +- .../components/zwave_js/translations/fr.json | 8 +- 222 files changed, 647 insertions(+), 328 deletions(-) create mode 100644 homeassistant/components/airzone/translations/he.json create mode 100644 homeassistant/components/climacell/translations/sensor.he.json create mode 100644 homeassistant/components/switch_as_x/translations/de.json create mode 100644 homeassistant/components/uptime/translations/ca.json create mode 100644 homeassistant/components/uptime/translations/de.json create mode 100644 homeassistant/components/uptime/translations/el.json create mode 100644 homeassistant/components/uptime/translations/et.json create mode 100644 homeassistant/components/uptime/translations/fr.json create mode 100644 homeassistant/components/uptime/translations/he.json create mode 100644 homeassistant/components/uptime/translations/it.json diff --git a/homeassistant/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json index d7b2935bbb9..a56c9732c6d 100644 --- a/homeassistant/components/abode/translations/fr.json +++ b/homeassistant/components/abode/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_mfa_code": "Code MFA non valide" }, "step": { diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index d1e545f5c68..1de45f8da22 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "requests_exceeded": "Le nombre autoris\u00e9 de requ\u00eates adress\u00e9es \u00e0 l'API AccuWeather a \u00e9t\u00e9 d\u00e9pass\u00e9. Vous devez attendre ou modifier la cl\u00e9 API." }, "step": { diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index dc704a4053e..edcdcdd2738 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -4,11 +4,11 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "heater_not_available": "Chauffage non disponible. Essayez de r\u00e9initialiser le chauffage en appuyant sur + et OK pendant quelques secondes.", "heater_not_found": "Chauffage introuvable. Essayez de rapprocher le radiateur de l'ordinateur Home Assistant.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "cloud": { diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index 5740048fc48..0d3d0e77a5e 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 d'API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "user": { diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 132cea46fbf..85d1a3b478e 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 d'API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "wrong_location": "Aucune station de mesure Airly dans cette zone." }, "step": { diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json index 686dedb9bb6..1cfe1652771 100644 --- a/homeassistant/components/airnow/translations/fr.json +++ b/homeassistant/components/airnow/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/airthings/translations/fr.json b/homeassistant/components/airthings/translations/fr.json index 1ad84e8bd99..89f12cfce73 100644 --- a/homeassistant/components/airthings/translations/fr.json +++ b/homeassistant/components/airthings/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index a52c2ad3d14..642fe40cc12 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "general_error": "Erreur inattendue", - "invalid_api_key": "Cl\u00e9 d'API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "location_not_found": "Emplacement introuvable" }, "step": { diff --git a/homeassistant/components/airvisual/translations/sensor.he.json b/homeassistant/components/airvisual/translations/sensor.he.json index 28ac8c5c3e4..7ed68fa47ca 100644 --- a/homeassistant/components/airvisual/translations/sensor.he.json +++ b/homeassistant/components/airvisual/translations/sensor.he.json @@ -2,7 +2,8 @@ "state": { "airvisual__pollutant_level": { "good": "\u05d8\u05d5\u05d1", - "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0" + "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0", + "unhealthy_sensitive": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/he.json b/homeassistant/components/airzone/translations/he.json new file mode 100644 index 00000000000..c3a67844fdd --- /dev/null +++ b/homeassistant/components/airzone/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json index 211a4f87366..cfa3e0b9946 100644 --- a/homeassistant/components/ambee/translations/fr.json +++ b/homeassistant/components/ambee/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/ambient_station/translations/fr.json b/homeassistant/components/ambient_station/translations/fr.json index 2991d0a966b..710786140c4 100644 --- a/homeassistant/components/ambient_station/translations/fr.json +++ b/homeassistant/components/ambient_station/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_key": "Cl\u00e9 d'API invalide", + "invalid_key": "Cl\u00e9 d'API non valide", "no_devices": "Aucun appareil trouv\u00e9 dans le compte" }, "step": { diff --git a/homeassistant/components/androidtv/translations/fr.json b/homeassistant/components/androidtv/translations/fr.json index c5e85b21781..ea79fddb98a 100644 --- a/homeassistant/components/androidtv/translations/fr.json +++ b/homeassistant/components/androidtv/translations/fr.json @@ -7,7 +7,7 @@ "error": { "adbkey_not_file": "Fichier de cl\u00e9 ADB introuvable", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "key_and_server": "Fournissez uniquement la cl\u00e9 ADB ou le serveur ADB", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/androidtv/translations/he.json b/homeassistant/components/androidtv/translations/he.json index 298416bccc5..81870f85c75 100644 --- a/homeassistant/components/androidtv/translations/he.json +++ b/homeassistant/components/androidtv/translations/he.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e8\u05e9\u05d9\u05de\u05ea \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd", - "exclude_unnamed_apps": "\u05d0\u05dc \u05ea\u05db\u05dc\u05d5\u05dc \u05d9\u05d9\u05e9\u05d5\u05dd \u05e2\u05dd \u05e9\u05dd \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2", - "get_sources": "\u05d4\u05d0\u05dd \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05d4\u05e4\u05d5\u05e2\u05dc\u05d9\u05dd \u05db\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", - "screencap": "\u05e7\u05d5\u05d1\u05e2 \u05d0\u05dd \u05d9\u05e9 \u05dc\u05de\u05e9\u05d5\u05da \u05d0\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05d4\u05d0\u05dc\u05d1\u05d5\u05dd \u05de\u05de\u05d4 \u05e9\u05de\u05d5\u05e6\u05d2 \u05e2\u05dc \u05d4\u05de\u05e1\u05da", + "exclude_unnamed_apps": "\u05d0\u05dc \u05ea\u05db\u05dc\u05d5\u05dc \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05e2\u05dd \u05e9\u05dd \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2 \u05d1\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", + "get_sources": "\u05d0\u05d7\u05d6\u05d5\u05e8 \u05d4\u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05d4\u05e4\u05d5\u05e2\u05dc\u05d9\u05dd \u05db\u05e8\u05e9\u05d9\u05de\u05ea \u05d4\u05de\u05e7\u05d5\u05e8\u05d5\u05ea", + "screencap": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05dc\u05db\u05d9\u05d3\u05ea \u05de\u05e1\u05da \u05e2\u05d1\u05d5\u05e8 \u05ea\u05de\u05d5\u05e0\u05ea \u05d0\u05dc\u05d1\u05d5\u05dd", "state_detection_rules": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1\u05d9\u05dd", - "turn_off_command": "\u05e4\u05e7\u05d5\u05d3\u05ea \u05de\u05e2\u05d8\u05e4\u05ea ADB \u05db\u05d3\u05d9 \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d0\u05ea \u05e4\u05e7\u05d5\u05d3\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc turn_off", - "turn_on_command": "\u05e4\u05e7\u05d5\u05d3\u05ea \u05de\u05e2\u05d8\u05e4\u05ea ADB \u05db\u05d3\u05d9 \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d0\u05ea \u05e4\u05e7\u05d5\u05d3\u05ea \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc turn_on" + "turn_off_command": "\u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 \u05db\u05d9\u05d1\u05d5\u05d9 \u05de\u05e2\u05d8\u05e4\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)", + "turn_on_command": "\u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 \u05d4\u05e4\u05e2\u05dc \u05de\u05e2\u05d8\u05e4\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)" }, "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" }, diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index 23fed83cfc4..510d22c79a5 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -16,7 +16,7 @@ }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/aseko_pool_live/translations/fr.json b/homeassistant/components/aseko_pool_live/translations/fr.json index 31df206cdda..ec1568e9330 100644 --- a/homeassistant/components/aseko_pool_live/translations/fr.json +++ b/homeassistant/components/aseko_pool_live/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json index 02ef17a4f33..0d53f3f24cf 100644 --- a/homeassistant/components/asuswrt/translations/fr.json +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "pwd_and_ssh": "Fournissez uniquement le mot de passe ou le fichier de cl\u00e9 SSH", "pwd_or_ssh": "Veuillez fournir un mot de passe ou un fichier de cl\u00e9 SSH", "ssh_not_file": "Fichier cl\u00e9 SSH non trouv\u00e9", diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json index 8b61f7b3267..78631c8daa1 100644 --- a/homeassistant/components/august/translations/fr.json +++ b/homeassistant/components/august/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/aussie_broadband/translations/fr.json b/homeassistant/components/aussie_broadband/translations/fr.json index 13178a5753e..d540e5fd2f9 100644 --- a/homeassistant/components/aussie_broadband/translations/fr.json +++ b/homeassistant/components/aussie_broadband/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { @@ -42,7 +42,7 @@ "options": { "abort": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/axis/translations/fr.json b/homeassistant/components/axis/translations/fr.json index ea3f93feb50..b22a14eeb69 100644 --- a/homeassistant/components/axis/translations/fr.json +++ b/homeassistant/components/axis/translations/fr.json @@ -9,7 +9,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "Appareil Axis: {name} ( {host} )", "step": { diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json index 17bc6104112..311ed28d8e9 100644 --- a/homeassistant/components/azure_devops/translations/fr.json +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "project_error": "Impossible d'obtenir les informations sur le projet." }, "flow_title": "{project_url}", diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index b6ffe195cd1..a9bd4ba30b1 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -49,7 +49,7 @@ "is_sound": "{entity_name} d\u00e9tecte du son", "is_tampered": "{entity_name} d\u00e9tecte une manipulation", "is_unsafe": "{entity_name} est dangereux", - "is_update": "{entity_name} a une mise \u00e0 jour disponible", + "is_update": "Une mise \u00e0 jour est disponible pour {entity_name}", "is_vibration": "{entity_name} d\u00e9tecte une vibration" }, "trigger_type": { @@ -88,7 +88,7 @@ "not_powered": "{entity_name} non aliment\u00e9", "not_present": "{entity_name} non pr\u00e9sent", "not_running": "{entity_name} n'est plus en cours d'ex\u00e9cution", - "not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter la falsification", + "not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter une manipulation", "not_unsafe": "{entity_name} est devenu s\u00fbr", "occupied": "{entity_name} est devenu occup\u00e9", "opened": "{entity_name} ouvert", @@ -99,11 +99,11 @@ "running": "{entity_name} commenc\u00e9 \u00e0 s'ex\u00e9cuter", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", - "tampered": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter une falsification", + "tampered": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter une manipulation", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", - "update": "{entity_name} a une mise \u00e0 jour disponible", + "update": "Une mise \u00e0 jour est disponible pour {entity_name}", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } }, diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json index bef14d641f7..bb1686e9700 100644 --- a/homeassistant/components/blink/translations/fr.json +++ b/homeassistant/components/blink/translations/fr.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_access_token": "Jeton d'acc\u00e8s non valide", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json index aadce398cdc..5181620d465 100644 --- a/homeassistant/components/bmw_connected_drive/translations/fr.json +++ b/homeassistant/components/bmw_connected_drive/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json index f968622e214..e821e6b85fc 100644 --- a/homeassistant/components/bond/translations/fr.json +++ b/homeassistant/components/bond/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "old_firmware": "Ancien micrologiciel non pris en charge sur l'appareil Bond - veuillez mettre \u00e0 niveau avant de continuer", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/bosch_shc/translations/fr.json b/homeassistant/components/bosch_shc/translations/fr.json index 43eeb04490d..c34f5549663 100644 --- a/homeassistant/components/bosch_shc/translations/fr.json +++ b/homeassistant/components/bosch_shc/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "pairing_failed": "L'appairage a \u00e9chou\u00e9\u00a0; veuillez v\u00e9rifier que le Bosch Smart Home Controller est en mode d'appairage (voyant clignotant) et que votre mot de passe est correct.", "session_error": "Erreur de session\u00a0: l'API renvoie un r\u00e9sultat non-OK.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index 0b0a0959b3e..d609f1a2fa1 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unsupported_model": "Votre mod\u00e8le de t\u00e9l\u00e9viseur n'est pas pris en charge." }, "step": { diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json index 665e62b77a5..e39b722d8c9 100644 --- a/homeassistant/components/broadlink/translations/fr.json +++ b/homeassistant/components/broadlink/translations/fr.json @@ -4,13 +4,13 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "not_supported": "Dispositif non pris en charge", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name} ( {model} \u00e0 {host} )", diff --git a/homeassistant/components/brother/translations/fr.json b/homeassistant/components/brother/translations/fr.json index ada9ca7385d..851d46f1772 100644 --- a/homeassistant/components/brother/translations/fr.json +++ b/homeassistant/components/brother/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", - "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." + "wrong_host": "Nom d'h\u00f4te ou adresse IP non valide." }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/brunt/translations/fr.json b/homeassistant/components/brunt/translations/fr.json index 611a57ea313..5368237550e 100644 --- a/homeassistant/components/brunt/translations/fr.json +++ b/homeassistant/components/brunt/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/button/translations/fr.json b/homeassistant/components/button/translations/fr.json index 5e6adf70da1..701770befcb 100644 --- a/homeassistant/components/button/translations/fr.json +++ b/homeassistant/components/button/translations/fr.json @@ -4,7 +4,7 @@ "press": "Appuyez sur le bouton {entity_name}" }, "trigger_type": { - "pressed": "{entity_name} a \u00e9t\u00e9 press\u00e9" + "pressed": "Appui sur {entity_name}" } }, "title": "Bouton" diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 0492c8a4d46..19b986a3db7 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/climacell/translations/sensor.he.json b/homeassistant/components/climacell/translations/sensor.he.json new file mode 100644 index 00000000000..2a509464928 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05e8\u05d2\u05d9\u05e9\u05d5\u05ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/fr.json b/homeassistant/components/cloudflare/translations/fr.json index 73c2d76b4fc..6b38336df24 100644 --- a/homeassistant/components/cloudflare/translations/fr.json +++ b/homeassistant/components/cloudflare/translations/fr.json @@ -7,8 +7,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_zone": "Zone invalide" + "invalid_auth": "Authentification non valide", + "invalid_zone": "Zone non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/co2signal/translations/fr.json b/homeassistant/components/co2signal/translations/fr.json index 1ed60fd3227..6467c1bf43a 100644 --- a/homeassistant/components/co2signal/translations/fr.json +++ b/homeassistant/components/co2signal/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "api_ratelimit": "Limite de d\u00e9bit API d\u00e9pass\u00e9e", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json index 74ee4c61f97..01cd7ec6dbf 100644 --- a/homeassistant/components/coinbase/translations/fr.json +++ b/homeassistant/components/coinbase/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_auth_key": "Identifiants API rejet\u00e9s par Coinbase en raison d'une cl\u00e9 API non valide.", "invalid_auth_secret": "Identifiants API rejet\u00e9s par Coinbase en raison d'un secret API non valide.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/control4/translations/fr.json b/homeassistant/components/control4/translations/fr.json index 7d9bd88a810..8fd08ac8a8a 100644 --- a/homeassistant/components/control4/translations/fr.json +++ b/homeassistant/components/control4/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/crownstone/translations/fr.json b/homeassistant/components/crownstone/translations/fr.json index 331add3d2a8..61a2f0da436 100644 --- a/homeassistant/components/crownstone/translations/fr.json +++ b/homeassistant/components/crownstone/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "account_not_verified": "Compte non v\u00e9rifi\u00e9. Veuillez activer votre compte via l'e-mail d'activation de Crownstone.", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 7a25805caf5..d021f758834 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -5,9 +5,9 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "api_password": "Authentification invalide, utilisez soit la cl\u00e9 d'API soit le mot de passe.", + "api_password": "Authentification non valide, utilisez soit la cl\u00e9 d'API soit le mot de passe.", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { @@ -17,7 +17,7 @@ "host": "H\u00f4te", "password": "Mot de passe" }, - "description": "Saisissez l'adresse IP de votre Daikin AC. \n\n Notez que Cl\u00e9 d'API et Mot de passe sont utilis\u00e9s respectivement par les p\u00e9riph\u00e9riques BRP072Cxx et SKYFi.", + "description": "Saisissez l'Adresse IP de votre Daikin AC. \n\nNotez que Cl\u00e9 d'API et Mot de passe sont utilis\u00e9s respectivement par les appareils BRP072Cxx et SKYFi.", "title": "Configurer Daikin AC" } } diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 464cc2e139e..39a32fc2434 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -42,10 +42,10 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", - "button_5": "5\u00e8me bouton", - "button_6": "6\u00e8me bouton", - "button_7": "7\u00e8me bouton", - "button_8": "8\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "button_7": "Septi\u00e8me bouton", + "button_8": "Huiti\u00e8me bouton", "close": "Ferm\u00e9", "dim_down": "Assombrir", "dim_up": "\u00c9claircir", @@ -70,8 +70,8 @@ "remote_button_quadruple_press": "Quadruple clic sur le bouton \" {subtype} \"", "remote_button_quintuple_press": "Quintuple clic sur le bouton \" {subtype} \"", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", - "remote_button_rotated_fast": "Bouton pivot\u00e9 rapidement \" {subtype} \"", - "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", + "remote_button_rotated_fast": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb tourn\u00e9 rapidement", + "remote_button_rotation_stopped": "La rotation du bouton \u00ab\u00a0{subtype}\u00a0\u00bb a cess\u00e9", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Triple clic sur le bouton \" {subtype} \"", diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 3a93f50e8e4..14a4bdb9c08 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "reauth_failed": "Veuillez utiliser le m\u00eame utilisateur mydevolo que pr\u00e9c\u00e9demment." }, "step": { diff --git a/homeassistant/components/devolo_home_network/translations/fr.json b/homeassistant/components/devolo_home_network/translations/fr.json index 311489bf927..50e601bb14a 100644 --- a/homeassistant/components/devolo_home_network/translations/fr.json +++ b/homeassistant/components/devolo_home_network/translations/fr.json @@ -17,7 +17,7 @@ "description": "Voulez-vous commencer la configuration\u00a0?" }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter l'appareil de r\u00e9seau domestique devolo portant le nom d'h\u00f4te \u00ab\u00a0{host_name}\u00a0\u00bb \u00e0 Home Assistant\u00a0?", + "description": "Voulez-vous ajouter l'appareil de r\u00e9seau domestique devolo portant le nom d'h\u00f4te `{host_name}` \u00e0 Home Assistant\u00a0?", "title": "Appareil r\u00e9seau domestique devolo d\u00e9couvert" } } diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json index 095c769a1be..316e39e2e5e 100644 --- a/homeassistant/components/dexcom/translations/fr.json +++ b/homeassistant/components/dexcom/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index 7bcc563f679..6554a3994d6 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -42,7 +42,7 @@ }, "options": { "error": { - "invalid_url": "URL invalide" + "invalid_url": "URL non valide" }, "step": { "init": { diff --git a/homeassistant/components/dnsip/translations/fr.json b/homeassistant/components/dnsip/translations/fr.json index cbecc060bd5..23c0d057ce4 100644 --- a/homeassistant/components/dnsip/translations/fr.json +++ b/homeassistant/components/dnsip/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_hostname": "Nom d'h\u00f4te invalide" + "invalid_hostname": "Nom d'h\u00f4te non valide" }, "step": { "user": { @@ -15,7 +15,7 @@ }, "options": { "error": { - "invalid_resolver": "Adresse IP invalide pour le r\u00e9solveur" + "invalid_resolver": "Adresse IP non valide pour le r\u00e9solveur" }, "step": { "init": { diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index c67658196c4..db1cea2d73f 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -3,8 +3,7 @@ "abort": { "already_configured": "Device is already configured", "link_local_address": "Link local addresses are not supported", - "not_doorbird_device": "This device is not a DoorBird", - "not_ipv4_address": "Only IPv4 addresess are supported" + "not_doorbird_device": "This device is not a DoorBird" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index 68165d762f9..40569250e5f 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/dunehd/translations/fr.json b/homeassistant/components/dunehd/translations/fr.json index 4d19f87ad45..0e8cb6d6ff8 100644 --- a/homeassistant/components/dunehd/translations/fr.json +++ b/homeassistant/components/dunehd/translations/fr.json @@ -6,7 +6,7 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" }, "step": { "user": { diff --git a/homeassistant/components/econet/translations/fr.json b/homeassistant/components/econet/translations/fr.json index 69b15c3857b..b03145b3c73 100644 --- a/homeassistant/components/econet/translations/fr.json +++ b/homeassistant/components/econet/translations/fr.json @@ -3,11 +3,11 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/efergy/translations/en.json b/homeassistant/components/efergy/translations/en.json index 0909241c423..aa76f9c0636 100644 --- a/homeassistant/components/efergy/translations/en.json +++ b/homeassistant/components/efergy/translations/en.json @@ -13,7 +13,8 @@ "user": { "data": { "api_key": "API Key" - } + }, + "title": "Efergy" } } } diff --git a/homeassistant/components/efergy/translations/fr.json b/homeassistant/components/efergy/translations/fr.json index 2e98eca19e7..a506454bc14 100644 --- a/homeassistant/components/efergy/translations/fr.json +++ b/homeassistant/components/efergy/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/elgato/translations/fr.json b/homeassistant/components/elgato/translations/fr.json index 5f3e99b0425..50588b11cbd 100644 --- a/homeassistant/components/elgato/translations/fr.json +++ b/homeassistant/components/elgato/translations/fr.json @@ -14,11 +14,11 @@ "host": "H\u00f4te", "port": "Port" }, - "description": "Configurez votre Elgato Key Light pour l'int\u00e9grer \u00e0 Home Assistant." + "description": "Configurez votre Elgato Light pour l'int\u00e9grer \u00e0 Home Assistant." }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter l'Elgato Key Light avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant?", - "title": "Appareil Elgato Key Light d\u00e9couvert" + "description": "Voulez-vous ajouter l'Elgato Light portant le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant\u00a0?", + "title": "Appareil Elgato Light d\u00e9couvert" } } } diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 87193a9adf7..07b5d132968 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{mac_address} ({host})", diff --git a/homeassistant/components/elmax/translations/fr.json b/homeassistant/components/elmax/translations/fr.json index 0f19428b5bf..7fe57a8290f 100644 --- a/homeassistant/components/elmax/translations/fr.json +++ b/homeassistant/components/elmax/translations/fr.json @@ -4,8 +4,8 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "bad_auth": "Authentification invalide", - "invalid_auth": "Authentification invalide", + "bad_auth": "Authentification non valide", + "invalid_auth": "Authentification non valide", "invalid_pin": "Le code PIN fourni n\u2019est pas valide", "network_error": "Une erreur r\u00e9seau s'est produite", "no_panel_online": "Aucun panneau de contr\u00f4le Elmax en ligne n'a \u00e9t\u00e9 trouv\u00e9.", diff --git a/homeassistant/components/enocean/translations/fr.json b/homeassistant/components/enocean/translations/fr.json index d2eda66257e..a5aa07870af 100644 --- a/homeassistant/components/enocean/translations/fr.json +++ b/homeassistant/components/enocean/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_dongle_path": "Lien vers la cl\u00e9 USB invalide", + "invalid_dongle_path": "Chemin d'acc\u00e8s \u00e0 la cl\u00e9 non valide", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { diff --git a/homeassistant/components/enphase_envoy/translations/fr.json b/homeassistant/components/enphase_envoy/translations/fr.json index 165c54c67d1..b11196812b2 100644 --- a/homeassistant/components/enphase_envoy/translations/fr.json +++ b/homeassistant/components/enphase_envoy/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{serial} ({host})", diff --git a/homeassistant/components/environment_canada/translations/fr.json b/homeassistant/components/environment_canada/translations/fr.json index d09b1eec095..20e95a1e646 100644 --- a/homeassistant/components/environment_canada/translations/fr.json +++ b/homeassistant/components/environment_canada/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "bad_station_id": "L'ID de station est invalide, manquant ou introuvable dans la base de donn\u00e9es d'ID de station", + "bad_station_id": "L'ID de station est non valide, manquant ou introuvable dans la base de donn\u00e9es des ID de stations", "cannot_connect": "\u00c9chec de connexion", "error_response": "R\u00e9ponse d'Environnement Canada par erreur", "too_many_attempts": "Les connexions \u00e0 Environnement Canada sont limit\u00e9es en termes de taux; R\u00e9essayez dans 60 secondes", diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 7125ef9c395..330c2823409 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, diff --git a/homeassistant/components/ezviz/translations/fr.json b/homeassistant/components/ezviz/translations/fr.json index 2d1e4b5cb04..475eb0bcfc7 100644 --- a/homeassistant/components/ezviz/translations/fr.json +++ b/homeassistant/components/ezviz/translations/fr.json @@ -7,8 +7,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" + "invalid_auth": "Authentification non valide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" }, "flow_title": "{serial}", "step": { diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json index 477e8df621c..bf663e8ad90 100644 --- a/homeassistant/components/fireservicerota/translations/fr.json +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -8,14 +8,14 @@ "default": "Authentification r\u00e9ussie" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth": { "data": { "password": "Mot de passe" }, - "description": "Les jetons d'authentification sont invalides, connectez-vous pour les recr\u00e9er." + "description": "Les jetons d'authentification ne sont plus valides, connectez-vous pour les recr\u00e9er." }, "user": { "data": { diff --git a/homeassistant/components/flick_electric/translations/fr.json b/homeassistant/components/flick_electric/translations/fr.json index fc7605c0975..9757b7cc39b 100644 --- a/homeassistant/components/flick_electric/translations/fr.json +++ b/homeassistant/components/flick_electric/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/flipr/translations/fr.json b/homeassistant/components/flipr/translations/fr.json index faa7c920577..7b0bad8b9e3 100644 --- a/homeassistant/components/flipr/translations/fr.json +++ b/homeassistant/components/flipr/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_flipr_id_found": "Aucun identifiant Flipr n'est associ\u00e9 \u00e0 votre compte pour le moment. Vous devez d'abord v\u00e9rifier qu'il fonctionne avec l'application mobile de Flipr.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/flo/translations/fr.json b/homeassistant/components/flo/translations/fr.json index 45620fe7795..bb317c2149f 100644 --- a/homeassistant/components/flo/translations/fr.json +++ b/homeassistant/components/flo/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/flume/translations/fr.json b/homeassistant/components/flume/translations/fr.json index 43157234eff..588bacc687e 100644 --- a/homeassistant/components/flume/translations/fr.json +++ b/homeassistant/components/flume/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json index 7c0bb8398da..f728c6d7414 100644 --- a/homeassistant/components/foscam/translations/fr.json +++ b/homeassistant/components/foscam/translations/fr.json @@ -5,8 +5,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_response": "R\u00e9ponse invalide de l\u2019appareil", + "invalid_auth": "Authentification non valide", + "invalid_response": "R\u00e9ponse de l'appareil non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/freedompro/translations/fr.json b/homeassistant/components/freedompro/translations/fr.json index 090c95fa3c2..cbd28831a2a 100644 --- a/homeassistant/components/freedompro/translations/fr.json +++ b/homeassistant/components/freedompro/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index 44c3c2159b5..3219164d98b 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -10,7 +10,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "upnp_not_configured": "Param\u00e8tres UPnP manquants sur l'appareil." }, "flow_title": "{name}", diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 8a75221ea88..69f024e26d7 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -8,7 +8,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/fr.json b/homeassistant/components/fritzbox_callmonitor/translations/fr.json index 2d1cadb8a48..134633e2802 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/fr.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/fr.json @@ -6,7 +6,7 @@ "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fronius/translations/fr.json b/homeassistant/components/fronius/translations/fr.json index 3eba668c232..e7ea85962bd 100644 --- a/homeassistant/components/fronius/translations/fr.json +++ b/homeassistant/components/fronius/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 8b3c3514606..b554ec1ea1d 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unknown": "Erreur inattendue" }, "step": { @@ -20,7 +20,7 @@ "host": "H\u00f4te", "name": "Nom" }, - "description": "Tout d'abord, vous devez t\u00e9l\u00e9charger l'application Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\n Suivez les instructions pour connecter votre Yeti \u00e0 votre r\u00e9seau Wifi. Ensuite, r\u00e9cup\u00e9rez l'adresse IP de votre routeur. DHCP doit \u00eatre configur\u00e9 dans les param\u00e8tres de votre routeur pour le p\u00e9riph\u00e9rique afin de garantir que l'adresse IP de l'h\u00f4te ne change pas. Reportez-vous au manuel d'utilisation de votre routeur.", + "description": "Vous devez tout d'abord t\u00e9l\u00e9charger l'application Goal Zero\u00a0: https://www.goalzero.com/product-features/yeti-app/\n\nSuivez les instructions pour connecter votre Yeti \u00e0 votre r\u00e9seau Wi-Fi. Il est recommand\u00e9 d'utiliser la r\u00e9servation DHCP sur votre routeur, sans quoi l'appareil risque de devenir indisponible le temps que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gogogate2/translations/fr.json b/homeassistant/components/gogogate2/translations/fr.json index 94cee628a79..d51dd6f2fde 100644 --- a/homeassistant/components/gogogate2/translations/fr.json +++ b/homeassistant/components/gogogate2/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{device} ({ip_address})", "step": { diff --git a/homeassistant/components/group/translations/de.json b/homeassistant/components/group/translations/de.json index d67de225e66..c06dd2a4a8f 100644 --- a/homeassistant/components/group/translations/de.json +++ b/homeassistant/components/group/translations/de.json @@ -1,10 +1,20 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder", + "name": "Name" + }, + "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist.", + "title": "Neue Gruppe" + }, "cover": { "data": { - "entities": "Gruppenmitglieder", - "name": "Gruppenname" + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" }, "description": "Gruppenoptionen ausw\u00e4hlen" }, @@ -16,8 +26,9 @@ }, "fan": { "data": { - "entities": "Gruppenmitglieder", - "name": "Gruppenname" + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" }, "description": "Gruppenoptionen ausw\u00e4hlen" }, @@ -35,8 +46,9 @@ }, "light": { "data": { - "entities": "Gruppenmitglieder", - "name": "Gruppenname" + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" }, "description": "Gruppenoptionen ausw\u00e4hlen" }, @@ -48,8 +60,9 @@ }, "media_player": { "data": { - "entities": "Gruppenmitglieder", - "name": "Gruppenname" + "entities": "Mitglieder", + "name": "Name", + "title": "Neue Gruppe" }, "description": "Gruppenoptionen ausw\u00e4hlen" }, @@ -58,6 +71,42 @@ "entities": "Gruppenmitglieder" }, "description": "Gruppenoptionen ausw\u00e4hlen" + }, + "user": { + "data": { + "group_type": "Gruppentyp" + }, + "title": "Neue Gruppe" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder" + } + }, + "cover_options": { + "data": { + "entities": "Mitglieder" + } + }, + "fan_options": { + "data": { + "entities": "Mitglieder" + } + }, + "light_options": { + "data": { + "entities": "Mitglieder" + } + }, + "media_player_options": { + "data": { + "entities": "Mitglieder" + } } } }, diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index 798a8e1e7c6..06c1a8e0fa8 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -1,4 +1,115 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd" + }, + "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc.", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "cover": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "cover_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "fan": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "fan_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "init": { + "data": { + "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "light": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "light_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "media_player": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "media_player_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "user": { + "data": { + "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" + }, + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + } + } + }, + "options": { + "step": { + "binary_sensor_options": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "cover_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "fan_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "light_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "media_player_options": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" + } + } + } + }, "state": { "_": { "closed": "\u05e1\u05d2\u05d5\u05e8", diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index 92d9c7b06fe..08c7d5e5afb 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -4,7 +4,7 @@ "binary_sensor": { "data": { "all": "Minden entit\u00e1s", - "entities": "Csoporttagok", + "entities": "A csoport tagjai", "name": "N\u00e9v" }, "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van.", @@ -85,27 +85,27 @@ "binary_sensor_options": { "data": { "all": "Minden entit\u00e1s", - "entities": "Csoporttagok" + "entities": "A csoport tagjai" } }, "cover_options": { "data": { - "entities": "Csoporttagok" + "entities": "A csoport tagjai" } }, "fan_options": { "data": { - "entities": "Csoporttagok" + "entities": "A csoport tagjai" } }, "light_options": { "data": { - "entities": "Csoporttagok" + "entities": "A csoport tagjai" } }, "media_player_options": { "data": { - "entities": "Csoporttagok" + "entities": "A csoport tagjai" } } } diff --git a/homeassistant/components/growatt_server/translations/fr.json b/homeassistant/components/growatt_server/translations/fr.json index 939111c4151..f86c8041502 100644 --- a/homeassistant/components/growatt_server/translations/fr.json +++ b/homeassistant/components/growatt_server/translations/fr.json @@ -4,7 +4,7 @@ "no_plants": "Aucune plante n'a \u00e9t\u00e9 trouv\u00e9e sur ce compte" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "plant": { diff --git a/homeassistant/components/habitica/translations/fr.json b/homeassistant/components/habitica/translations/fr.json index c17151ea29e..9f04d9ed588 100644 --- a/homeassistant/components/habitica/translations/fr.json +++ b/homeassistant/components/habitica/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_credentials": "Authentification invalide", + "invalid_credentials": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/hlk_sw16/translations/fr.json b/homeassistant/components/hlk_sw16/translations/fr.json index 45620fe7795..bb317c2149f 100644 --- a/homeassistant/components/hlk_sw16/translations/fr.json +++ b/homeassistant/components/hlk_sw16/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 18f3e82aa76..01286ceb991 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -7,7 +7,7 @@ "already_paired": "Cet accessoire est d\u00e9j\u00e0 associ\u00e9 \u00e0 un autre appareil. R\u00e9initialisez l\u2019accessoire et r\u00e9essayez.", "ignored_model": "La prise en charge de HomeKit pour ce mod\u00e8le est bloqu\u00e9e car une int\u00e9gration native plus compl\u00e8te est disponible.", "invalid_config_entry": "Cet appareil est pr\u00eat \u00e0 \u00eatre coupl\u00e9, mais il existe d\u00e9j\u00e0 une entr\u00e9e de configuration en conflit dans Home Assistant \u00e0 supprimer.", - "invalid_properties": "Propri\u00e9t\u00e9s invalides annonc\u00e9es par l'appareil.", + "invalid_properties": "Propri\u00e9t\u00e9s annonc\u00e9es par l'appareil non valides.", "no_devices": "Aucun appareil non appair\u00e9 n'a pu \u00eatre trouv\u00e9" }, "error": { @@ -65,8 +65,8 @@ }, "trigger_type": { "double_press": "\" {subtype} \" appuy\u00e9 deux fois", - "long_press": "\" {subtype} \" enfonc\u00e9 et maintenu", - "single_press": "\" {subtype} \" press\u00e9" + "long_press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9 et maintenu", + "single_press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9" } }, "title": "Accessoire HomeKit" diff --git a/homeassistant/components/honeywell/translations/fr.json b/homeassistant/components/honeywell/translations/fr.json index fbe3def3113..ac11cf20576 100644 --- a/homeassistant/components/honeywell/translations/fr.json +++ b/homeassistant/components/honeywell/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index ca360ffc077..1f5042d3aa1 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -9,8 +9,8 @@ "connection_timeout": "D\u00e9lai de connexion d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", - "invalid_auth": "Authentification invalide", - "invalid_url": "URL invalide", + "invalid_auth": "Authentification non valide", + "invalid_url": "URL non valide", "login_attempts_exceeded": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", "response_error": "Erreur inconnue de l'appareil", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index 418354ceb96..ec9d104aa65 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -51,16 +51,16 @@ "turn_on": "Allumer" }, "trigger_type": { - "double_short_release": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", - "initial_press": "Bouton \" {subtype} \" appuy\u00e9 initialement", - "long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "double_short_release": "Les deux \u00ab\u00a0{subtype}\u00a0\u00bb sont rel\u00e2ch\u00e9s", + "initial_press": "D\u00e9but de l'appui du bouton \u00ab\u00a0{subtype}\u00a0\u00bb", + "long_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_double_button_long_press": "Les deux \"{sous-type}\" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s apr\u00e8s un appui long", "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", - "repeat": "Bouton \" {subtype} \" maintenu enfonc\u00e9", - "short_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui court" + "repeat": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb maintenu enfonc\u00e9", + "short_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui court" } }, "options": { diff --git a/homeassistant/components/huisbaasje/translations/fr.json b/homeassistant/components/huisbaasje/translations/fr.json index aa84ec33d8c..744b9c6a862 100644 --- a/homeassistant/components/huisbaasje/translations/fr.json +++ b/homeassistant/components/huisbaasje/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/hvv_departures/translations/fr.json b/homeassistant/components/hvv_departures/translations/fr.json index 0c7fd03f148..10a471223f4 100644 --- a/homeassistant/components/hvv_departures/translations/fr.json +++ b/homeassistant/components/hvv_departures/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_results": "Aucun r\u00e9sultat. Essayez avec une autre station / adresse" }, "step": { diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index e3286cabfb4..13b04413f0f 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -20,7 +20,7 @@ "create_token": "Cr\u00e9er automatiquement un nouveau jeton", "token": "Ou fournir un jeton pr\u00e9existant" }, - "description": "Configurer l'autorisation sur votre serveur Hyperion Ambilight" + "description": "Configurez l'autorisation vers votre serveur Hyperion Ambilight" }, "confirm": { "description": "Voulez-vous ajouter cet Hyperion Ambilight \u00e0 Home Assistant\u00a0?\n\n**H\u00f4te\u00a0:** {host}\n**Port\u00a0:** {port}\n**ID\u00a0:** {id}", diff --git a/homeassistant/components/iaqualink/translations/fr.json b/homeassistant/components/iaqualink/translations/fr.json index ec3e7352a63..92327990f71 100644 --- a/homeassistant/components/iaqualink/translations/fr.json +++ b/homeassistant/components/iaqualink/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index 1978eb58232..8e5ec918cb0 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "send_verification_code": "\u00c9chec de l'envoi du code de v\u00e9rification", "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" }, diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index a0bd2f4048e..2feb3c5d9c9 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -54,9 +54,9 @@ "data": { "address": "Adresse de l'appareil (par exemple 1a2b3c)", "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)", - "subcat": "Sous-cat\u00e9gorie de p\u00e9riph\u00e9rique (par exemple 0x0a)" + "subcat": "Sous-cat\u00e9gorie d'appareil (par exemple 0x0a)" }, - "description": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.", + "description": "Ajouter un remplacement d'appareil.", "title": "Insteon" }, "add_x10": { @@ -81,27 +81,27 @@ }, "init": { "data": { - "add_override": "Ajoutez un remplacement de p\u00e9riph\u00e9rique.", + "add_override": "Ajouter un remplacement d'appareil.", "add_x10": "Ajouter un appareil X10.", "change_hub_config": "Modifier la configuration du Hub.", "remove_override": "Supprimer un remplacement d'appareil", - "remove_x10": "Retirez un p\u00e9riph\u00e9rique X10." + "remove_x10": "Retirer un appareil X10." }, "description": "S\u00e9lectionnez une option \u00e0 configurer.", "title": "Insteon" }, "remove_override": { "data": { - "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" + "address": "S\u00e9lectionnez l'adresse d'un appareil \u00e0 retirer" }, "description": "Supprimer un remplacement d'appareil", "title": "Insteon" }, "remove_x10": { "data": { - "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" + "address": "S\u00e9lectionnez l'adresse d'un appareil \u00e0 retirer" }, - "description": "Retirer un p\u00e9riph\u00e9rique X10", + "description": "Retirer un appareil X10", "title": "Insteon" } } diff --git a/homeassistant/components/iotawatt/translations/fr.json b/homeassistant/components/iotawatt/translations/fr.json index 1452beb4465..2c93cf7432f 100644 --- a/homeassistant/components/iotawatt/translations/fr.json +++ b/homeassistant/components/iotawatt/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/iqvia/translations/fr.json b/homeassistant/components/iqvia/translations/fr.json index a967ff490e8..97adf4c35a6 100644 --- a/homeassistant/components/iqvia/translations/fr.json +++ b/homeassistant/components/iqvia/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_zip_code": "Code postal invalide" + "invalid_zip_code": "Code postal non valide" }, "step": { "user": { diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index e7e330b4e3a..c9e53845dfb 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_host": "L'entr\u00e9e d'h\u00f4te n'\u00e9tait pas au format URL complet, par exemple http://192.168.10.100:80", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/jellyfin/translations/fr.json b/homeassistant/components/jellyfin/translations/fr.json index c797b7e93a3..5037fd507f0 100644 --- a/homeassistant/components/jellyfin/translations/fr.json +++ b/homeassistant/components/jellyfin/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/juicenet/translations/fr.json b/homeassistant/components/juicenet/translations/fr.json index f4e5bfa53d5..8688b5b4963 100644 --- a/homeassistant/components/juicenet/translations/fr.json +++ b/homeassistant/components/juicenet/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/kmtronic/translations/fr.json b/homeassistant/components/kmtronic/translations/fr.json index 7c0050bfad8..2cddd25f517 100644 --- a/homeassistant/components/kmtronic/translations/fr.json +++ b/homeassistant/components/kmtronic/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/knx/translations/he.json b/homeassistant/components/knx/translations/he.json index 3c338886e22..ef11ad342ee 100644 --- a/homeassistant/components/knx/translations/he.json +++ b/homeassistant/components/knx/translations/he.json @@ -13,11 +13,21 @@ "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4" } + }, + "routing": { + "data": { + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1" + } } } }, "options": { "step": { + "init": { + "data": { + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1 \u05d5\u05d2\u05d9\u05dc\u05d5\u05d9" + } + }, "tunnel": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json index e94282fa393..34838232435 100644 --- a/homeassistant/components/kodi/translations/fr.json +++ b/homeassistant/components/kodi/translations/fr.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_uuid": "L'instance Kodi n'a pas d'identifiant unique. Cela est probablement d\u00fb \u00e0 une ancienne version de Kodi (17.x ou inf\u00e9rieure). Vous pouvez configurer l'int\u00e9gration manuellement ou passer \u00e0 une version plus r\u00e9cente de Kodi.", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/kostal_plenticore/translations/fr.json b/homeassistant/components/kostal_plenticore/translations/fr.json index a06ade90e9a..66888e8879b 100644 --- a/homeassistant/components/kostal_plenticore/translations/fr.json +++ b/homeassistant/components/kostal_plenticore/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/lcn/translations/fr.json b/homeassistant/components/lcn/translations/fr.json index 7a2202e58a7..75e7527701a 100644 --- a/homeassistant/components/lcn/translations/fr.json +++ b/homeassistant/components/lcn/translations/fr.json @@ -2,9 +2,9 @@ "device_automation": { "trigger_type": { "fingerprint": "code d'empreinte digitale re\u00e7u", - "send_keys": "code \u00e9metteur re\u00e7u", + "send_keys": "cl\u00e9s d'envoi re\u00e7ues", "transmitter": "code \u00e9metteur re\u00e7u", - "transponder": "code transpodeur re\u00e7u" + "transponder": "code transpondeur re\u00e7u" } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index cb86d8c6590..f58b789b267 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "create_entry": { @@ -9,8 +9,8 @@ }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", - "invalid_auth": "Authentification invalide", - "invalid_username": "Nom d'utilisateur invalide", + "invalid_auth": "Authentification non valide", + "invalid_username": "Nom d'utilisateur non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index 7a58a267ed7..bcb6cac594e 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -1,28 +1,28 @@ { "device_automation": { "action_type": { - "brightness_decrease": "Diminue la luminosit\u00e9 de {entity_name}", - "brightness_increase": "Augmentez la luminosit\u00e9 de {entity_name}", - "flash": "Flash {entity_name}", + "brightness_decrease": "Diminuer la luminosit\u00e9 de {entity_name}", + "brightness_increase": "Augmenter la luminosit\u00e9 de {entity_name}", + "flash": "Faire clignoter {entity_name}", "toggle": "Basculer {entity_name}", "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est \u00e9teint", - "is_on": "{entity_name} est allum\u00e9" + "is_off": "{entity_name} est \u00e9teinte", + "is_on": "{entity_name} est allum\u00e9e" }, "trigger_type": { - "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", - "turned_on": "{entity_name} activ\u00e9" + "changed_states": "{entity_name} a \u00e9t\u00e9 allum\u00e9e ou \u00e9teinte", + "toggled": "{entity_name} a \u00e9t\u00e9 allum\u00e9e ou \u00e9teinte", + "turned_off": "{entity_name} a \u00e9t\u00e9 \u00e9teinte", + "turned_on": "{entity_name} a \u00e9t\u00e9 allum\u00e9e" } }, "state": { "_": { - "off": "D\u00e9sactiv\u00e9", - "on": "Activ\u00e9" + "off": "\u00c9teinte", + "on": "Allum\u00e9e" } }, "title": "Lumi\u00e8re" diff --git a/homeassistant/components/litejet/translations/fr.json b/homeassistant/components/litejet/translations/fr.json index b8ca0adb3d4..b5f533d5fce 100644 --- a/homeassistant/components/litejet/translations/fr.json +++ b/homeassistant/components/litejet/translations/fr.json @@ -11,7 +11,7 @@ "data": { "port": "Port" }, - "description": "Connectez le port RS232-2 du LiteJet \u00e0 votre ordinateur et entrez le chemin d'acc\u00e8s au p\u00e9riph\u00e9rique de port s\u00e9rie. \n\n Le LiteJet MCP doit \u00eatre configur\u00e9 pour 19,2 K bauds, 8 bits de donn\u00e9es, 1 bit d'arr\u00eat, sans parit\u00e9 et pour transmettre un \u00abCR\u00bb apr\u00e8s chaque r\u00e9ponse.", + "description": "Connectez le port RS232-2 du LiteJet \u00e0 votre ordinateur puis entrez le chemin d'acc\u00e8s au p\u00e9riph\u00e9rique sur port s\u00e9rie. \n\nLe LiteJet MCP doit \u00eatre configur\u00e9 pour 19,2\u00a0kilobauds, 8\u00a0bits de donn\u00e9es, 1\u00a0bit d'arr\u00eat, sans parit\u00e9 et pour transmettre un \u00ab\u00a0CR\u00a0\u00bb apr\u00e8s chaque r\u00e9ponse.", "title": "Connectez-vous \u00e0 LiteJet" } } diff --git a/homeassistant/components/litterrobot/translations/fr.json b/homeassistant/components/litterrobot/translations/fr.json index aa84ec33d8c..744b9c6a862 100644 --- a/homeassistant/components/litterrobot/translations/fr.json +++ b/homeassistant/components/litterrobot/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 61898cd6ca8..5c4bb6fab9d 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -9,7 +9,7 @@ "error": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "auth": { diff --git a/homeassistant/components/luftdaten/translations/fr.json b/homeassistant/components/luftdaten/translations/fr.json index 3acc1803fde..4eacd984ad8 100644 --- a/homeassistant/components/luftdaten/translations/fr.json +++ b/homeassistant/components/luftdaten/translations/fr.json @@ -3,7 +3,7 @@ "error": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_sensor": "Capteur non disponible ou invalide" + "invalid_sensor": "Capteur non disponible ou non valide" }, "step": { "user": { diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json index cdf584fcc00..c5f259167d2 100644 --- a/homeassistant/components/lutron_caseta/translations/fr.json +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -66,11 +66,11 @@ "stop_2": "Arr\u00eat 2", "stop_3": "Arr\u00eat 3", "stop_4": "Arr\u00eat 4", - "stop_all": "Arr\u00eate tout" + "stop_all": "Arr\u00eater tout" }, "trigger_type": { - "press": "\" {subtype} \" appuy\u00e9", - "release": "\" {subtype} \" publi\u00e9" + "press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9", + "release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json index 4ce19467267..8024852de46 100644 --- a/homeassistant/components/mazda/translations/fr.json +++ b/homeassistant/components/mazda/translations/fr.json @@ -7,7 +7,7 @@ "error": { "account_locked": "Compte bloqu\u00e9. Veuillez r\u00e9essayer plus tard.", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index d34d4fc9da5..17a6cbf92b3 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -12,8 +12,8 @@ "idle": "{entity_name} devient inactif", "paused": "{entity_name} est mis en pause", "playing": "{entity_name} commence \u00e0 jouer", - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", - "turned_on": "{entity_name} activ\u00e9" + "turned_off": "{entity_name} a \u00e9t\u00e9 \u00e9teint", + "turned_on": "{entity_name} a \u00e9t\u00e9 allum\u00e9" } }, "state": { diff --git a/homeassistant/components/melcloud/translations/fr.json b/homeassistant/components/melcloud/translations/fr.json index 9846f14fe7e..95d3433fb88 100644 --- a/homeassistant/components/melcloud/translations/fr.json +++ b/homeassistant/components/melcloud/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json index 5632ab5b8dd..5d0f2786eff 100644 --- a/homeassistant/components/mikrotik/translations/fr.json +++ b/homeassistant/components/mikrotik/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "name_exists": "Le nom existe" }, "step": { diff --git a/homeassistant/components/mjpeg/translations/fr.json b/homeassistant/components/mjpeg/translations/fr.json index dcfadb77328..f54c60631a6 100644 --- a/homeassistant/components/mjpeg/translations/fr.json +++ b/homeassistant/components/mjpeg/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { @@ -23,7 +23,7 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "init": { diff --git a/homeassistant/components/mobile_app/translations/he.json b/homeassistant/components/mobile_app/translations/he.json index e213f54137c..022accb4abc 100644 --- a/homeassistant/components/mobile_app/translations/he.json +++ b/homeassistant/components/mobile_app/translations/he.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "install_app": "\u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd \u05db\u05d3\u05d9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e2\u05dd Home Assistant. \u05d9\u05e9 \u05dc\u05e2\u05d9\u05d9\u05df [\u05d1\u05de\u05e1\u05de\u05db\u05d9\u05dd]({apps_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05e8\u05e9\u05d9\u05de\u05d4 \u05e9\u05dc \u05d9\u05d9\u05e9\u05d5\u05de\u05d9\u05dd \u05ea\u05d5\u05d0\u05de\u05d9\u05dd." + }, "step": { "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05e8\u05db\u05d9\u05d1 \u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3?" } } }, + "device_automation": { + "action_type": { + "notify": "\u05e9\u05dc\u05d9\u05d7\u05ea \u05d4\u05d5\u05d3\u05e2\u05d4" + } + }, "title": "\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3" } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/fr.json b/homeassistant/components/motioneye/translations/fr.json index 1d9daffcb4d..e498cf9987a 100644 --- a/homeassistant/components/motioneye/translations/fr.json +++ b/homeassistant/components/motioneye/translations/fr.json @@ -6,8 +6,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "invalid_url": "URL invalide", + "invalid_auth": "Authentification non valide", + "invalid_url": "URL non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 13bfce8dd5e..b84aec15a80 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -44,15 +44,15 @@ "button_long_release": "\"{subtype}\" relach\u00e9 apr\u00e8s un appui long", "button_quadruple_press": "\"{subtype}\" quadruple cliqu\u00e9", "button_quintuple_press": "\"{subtype}\" quintuple cliqu\u00e9", - "button_short_press": "\" {subtype} \" press\u00e9", - "button_short_release": "\"{subtype}\" relach\u00e9", - "button_triple_press": "\"{subtype}\" triple-cliqu\u00e9" + "button_short_press": "\u00ab\u00a0{subtype}\u00a0\u00bb enfonc\u00e9", + "button_short_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9", + "button_triple_press": "\u00ab\u00a0{subtype}\u00a0\u00bb triple-cliqu\u00e9" } }, "options": { "error": { - "bad_birth": "Topic de naissance invalide", - "bad_will": "Topic de testament invalide", + "bad_birth": "Sujet de la naissance non valide.", + "bad_will": "Sujet du testament non valide.", "cannot_connect": "\u00c9chec de connexion" }, "step": { diff --git a/homeassistant/components/myq/translations/fr.json b/homeassistant/components/myq/translations/fr.json index 6aa94c54577..27f57ff60b1 100644 --- a/homeassistant/components/myq/translations/fr.json +++ b/homeassistant/components/myq/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index e104c69e815..c9d64ee73e6 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -5,7 +5,7 @@ "cannot_connect": "\u00c9chec de connexion", "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9", "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_device": "Appareil non valide", "invalid_ip": "Adresse IP non valide", "invalid_persistence_file": "Fichier de persistance non valide", @@ -24,7 +24,7 @@ "cannot_connect": "\u00c9chec de connexion", "duplicate_persistence_file": "Fichier de persistance d\u00e9j\u00e0 utilis\u00e9", "duplicate_topic": "Sujet d\u00e9j\u00e0 utilis\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_device": "Appareil non valide", "invalid_ip": "Adresse IP non valide", "invalid_persistence_file": "Fichier de persistance non valide", diff --git a/homeassistant/components/nam/translations/fr.json b/homeassistant/components/nam/translations/fr.json index 58a626f4071..8c5331d0e1c 100644 --- a/homeassistant/components/nam/translations/fr.json +++ b/homeassistant/components/nam/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{host}", diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 54c9c700d3e..d639009dff8 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -15,7 +15,7 @@ "error": { "bad_project_id": "Veuillez saisir un ID de projet Cloud valide (v\u00e9rifiez Cloud\u00a0Console)", "internal_error": "Erreur interne lors de la validation du code", - "invalid_pin": "Code PIN invalide", + "invalid_pin": "Code PIN non valide", "subscriber_error": "Erreur d'abonn\u00e9 inconnue, voir les journaux", "timeout": "D\u00e9lai de la validation du code expir\u00e9", "unknown": "Erreur inattendue", diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 0edeef3dcfc..7fefb68f629 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -23,8 +23,8 @@ "device_automation": { "trigger_subtype": { "away": "absent", - "hg": "garde-gel", - "schedule": "horaire" + "hg": "hors-gel", + "schedule": "programm\u00e9" }, "trigger_type": { "alarm_started": "{entity_name} a d\u00e9tect\u00e9 une alarme", diff --git a/homeassistant/components/nexia/translations/fr.json b/homeassistant/components/nexia/translations/fr.json index b76672cd017..3955d07ff7a 100644 --- a/homeassistant/components/nexia/translations/fr.json +++ b/homeassistant/components/nexia/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json index bec90374906..0b99785652d 100644 --- a/homeassistant/components/nightscout/translations/fr.json +++ b/homeassistant/components/nightscout/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "Nightscout", diff --git a/homeassistant/components/nmap_tracker/translations/fr.json b/homeassistant/components/nmap_tracker/translations/fr.json index 59e02ea14cc..0a4e75a9813 100644 --- a/homeassistant/components/nmap_tracker/translations/fr.json +++ b/homeassistant/components/nmap_tracker/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_hosts": "H\u00f4tes invalides" + "invalid_hosts": "H\u00f4tes non valides" }, "step": { "user": { @@ -20,7 +20,7 @@ }, "options": { "error": { - "invalid_hosts": "H\u00f4tes invalides" + "invalid_hosts": "H\u00f4tes non valides" }, "step": { "init": { diff --git a/homeassistant/components/notion/translations/fr.json b/homeassistant/components/notion/translations/fr.json index 111fc918818..c5c0145e972 100644 --- a/homeassistant/components/notion/translations/fr.json +++ b/homeassistant/components/notion/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_devices": "Aucun appareil trouv\u00e9 sur le compte", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/nuheat/translations/fr.json b/homeassistant/components/nuheat/translations/fr.json index 6c50dae47d5..9c0c76d6553 100644 --- a/homeassistant/components/nuheat/translations/fr.json +++ b/homeassistant/components/nuheat/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_thermostat": "Le num\u00e9ro de s\u00e9rie du thermostat n'est pas valide.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/nuki/translations/fr.json b/homeassistant/components/nuki/translations/fr.json index 360e374888c..e6950517033 100644 --- a/homeassistant/components/nuki/translations/fr.json +++ b/homeassistant/components/nuki/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/omnilogic/translations/fr.json b/homeassistant/components/omnilogic/translations/fr.json index 4a8b293aebf..3567530d77e 100644 --- a/homeassistant/components/omnilogic/translations/fr.json +++ b/homeassistant/components/omnilogic/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/oncue/translations/fr.json b/homeassistant/components/oncue/translations/fr.json index b6704aabae1..dc122998917 100644 --- a/homeassistant/components/oncue/translations/fr.json +++ b/homeassistant/components/oncue/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/opengarage/translations/fr.json b/homeassistant/components/opengarage/translations/fr.json index 571a2c68b22..3601e6d46bd 100644 --- a/homeassistant/components/opengarage/translations/fr.json +++ b/homeassistant/components/opengarage/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index 93bf5e8a108..99a616427a7 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_api_key": "Cl\u00e9 d'API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json index 92283d8dff0..efa76d4d7e4 100644 --- a/homeassistant/components/openweathermap/translations/fr.json +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API invalide" + "invalid_api_key": "Cl\u00e9 d'API non valide" }, "step": { "user": { diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 87a27a29a5e..f6a56db80a5 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "server_in_maintenance": "Le serveur est ferm\u00e9 pour maintenance", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 2c2482d9f5f..3a915229883 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -3,7 +3,7 @@ "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{username}", "step": { diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index 0d9eb4d0860..9d651c66e94 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -5,7 +5,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { - "default": "\n\nSous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences - > Connexion. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP priv\u00e9 \n - H\u00f4te: {webhook_url} \n - Identification: \n - Nom d'utilisateur: `''` \n - ID de p\u00e9riph\u00e9rique: `''` \n\n Sur iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche - > param\u00e8tres. Modifiez les param\u00e8tres suivants: \n - Mode: HTTP \n - URL: {webhook_url} \n - Activer l'authentification \n - ID utilisateur: `''` \n\n {secret} \n \n Voir [la documentation]({docs_url}) pour plus d'informations." + "default": "\n\nSous Android, ouvrez [l'application OwnTracks]({android_url}), acc\u00e9dez \u00e0 Pr\u00e9f\u00e9rences -> Connexion. Modifiez les param\u00e8tres suivants\u00a0:\n - Mode\u00a0: HTTP priv\u00e9 \n - H\u00f4te\u00a0: {webhook_url} \n - Identification\u00a0: \n - Nom d'utilisateur\u00a0: `''` \n - ID de p\u00e9riph\u00e9rique\u00a0: `''` \n\nSous iOS, ouvrez [l'application OwnTracks]({ios_url}), appuyez sur l'ic\u00f4ne (i) en haut \u00e0 gauche -> param\u00e8tres. Modifiez les param\u00e8tres suivants\u00a0:\n - Mode\u00a0: HTTP \n - URL\u00a0: {webhook_url} \n - Activer l'authentification \n - ID utilisateur\u00a0: `''` \n\n{secret}\n \nConsultez [la documentation]({docs_url}) pour plus d'informations." }, "step": { "user": { diff --git a/homeassistant/components/philips_js/translations/fr.json b/homeassistant/components/philips_js/translations/fr.json index 8cc5d187743..3e5d451bbc3 100644 --- a/homeassistant/components/philips_js/translations/fr.json +++ b/homeassistant/components/philips_js/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_pin": "NIP invalide", + "invalid_pin": "PIN non valide", "pairing_failure": "Association impossible: {error_id}", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json index 794a33ffe75..77d9756b5b8 100644 --- a/homeassistant/components/picnic/translations/fr.json +++ b/homeassistant/components/picnic/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "different_account": "Le compte doit \u00eatre le m\u00eame que celui utilis\u00e9 pour configurer l'int\u00e9gration", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 5370a18ba56..c6098db43f0 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/poolsense/translations/fr.json b/homeassistant/components/poolsense/translations/fr.json index 0d0d3069e2b..4a32776bccb 100644 --- a/homeassistant/components/poolsense/translations/fr.json +++ b/homeassistant/components/poolsense/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index 27af1e3e119..f7510cf46e4 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue", "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, diff --git a/homeassistant/components/prosegur/translations/fr.json b/homeassistant/components/prosegur/translations/fr.json index 42061673128..5a0c38508d3 100644 --- a/homeassistant/components/prosegur/translations/fr.json +++ b/homeassistant/components/prosegur/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ps4/translations/fr.json b/homeassistant/components/ps4/translations/fr.json index c4765c31c4a..8fc216fd20b 100644 --- a/homeassistant/components/ps4/translations/fr.json +++ b/homeassistant/components/ps4/translations/fr.json @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "Informations d\u2019identification n\u00e9cessaires. Appuyez sur \u00ab\u00a0Envoyer\u00a0\u00bb puis dans la PS4 2\u00e8me \u00e9cran App, actualisez les p\u00e9riph\u00e9riques et s\u00e9lectionnez le dispositif \u00ab\u00a0Home Assistant\u00a0\u00bb pour continuer.", + "description": "Informations d'identification requises. Appuyez sur \u00ab\u00a0Envoyer\u00a0\u00bb, puis dans l'application PS4 Second Screen, actualisez les appareils et s\u00e9lectionnez l'appareil \u00ab\u00a0Home Assistant\u00a0\u00bb pour continuer.", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "Nom", "region": "R\u00e9gion" }, - "description": "Entrez vos informations PlayStation 4. Pour \"Code PIN\", acc\u00e9dez \u00e0 \"Param\u00e8tres\" sur votre console PlayStation 4. Ensuite, acc\u00e9dez \u00e0 \"Param\u00e8tres de connexion de l'application mobile\" et s\u00e9lectionnez \"Ajouter un p\u00e9riph\u00e9rique\". Entrez le code PIN qui est affich\u00e9. Consultez la documentation pour plus d'informations.", + "description": "Saisissez les informations de votre PlayStation\u00a04. Pour le Code PIN, acc\u00e9dez aux \u00ab\u00a0Param\u00e8tres\u00a0\u00bb sur votre console PlayStation\u00a04, puis acc\u00e9dez \u00e0 \u00ab\u00a0Param\u00e8tres de connexion de l'application mobile\u00a0\u00bb et s\u00e9lectionnez \u00ab\u00a0Ajouter un appareil\u00a0\u00bb. Entrez le Code PIN affich\u00e9. Consultez la [documentation](https://www.home-assistant.io/components/ps4/) pour plus d'informations.", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "Adresse IP (laissez vide si vous utilisez la d\u00e9couverte automatique).", "mode": "Mode de configuration" }, - "description": "S\u00e9lectionnez le mode de configuration. Le champ Adresse IP peut rester vide si vous s\u00e9lectionnez D\u00e9couverte automatique, car les p\u00e9riph\u00e9riques seront automatiquement d\u00e9couverts.", + "description": "S\u00e9lectionnez le mode de configuration. Le champ Adresse IP peut \u00eatre laiss\u00e9 vide si vous s\u00e9lectionnez D\u00e9couverte automatique, car les appareils seront automatiquement d\u00e9couverts.", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pvoutput/translations/fr.json b/homeassistant/components/pvoutput/translations/fr.json index 1e10597e67a..5ebf1cf3eb8 100644 --- a/homeassistant/components/pvoutput/translations/fr.json +++ b/homeassistant/components/pvoutput/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/rachio/translations/fr.json b/homeassistant/components/rachio/translations/fr.json index 343256cea9a..88de6eb89f7 100644 --- a/homeassistant/components/rachio/translations/fr.json +++ b/homeassistant/components/rachio/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/rainforest_eagle/translations/fr.json b/homeassistant/components/rainforest_eagle/translations/fr.json index 9631ff6cc93..cb86945aaca 100644 --- a/homeassistant/components/rainforest_eagle/translations/fr.json +++ b/homeassistant/components/rainforest_eagle/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index db8bac46b59..d9a6c011755 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{ip}", "step": { diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index 0ad6ec9d85d..32dcac2929f 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_credentials": "Authentification invalide" + "invalid_credentials": "Authentification non valide" }, "step": { "kamereon": { diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index edae1557066..5eaff1ce406 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -46,7 +46,7 @@ }, "trigger_type": { "command": "Commande re\u00e7ue\u00a0: {subtype}", - "status": "Statut re\u00e7u\u00a0: {subtype}" + "status": "\u00c9tat re\u00e7u\u00a0: {subtype}" } }, "one": "Vide", diff --git a/homeassistant/components/ridwell/translations/fr.json b/homeassistant/components/ridwell/translations/fr.json index 09b73beb37c..82bdb749d13 100644 --- a/homeassistant/components/ridwell/translations/fr.json +++ b/homeassistant/components/ridwell/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ring/translations/fr.json b/homeassistant/components/ring/translations/fr.json index c86cd78564c..01bbd6587c3 100644 --- a/homeassistant/components/ring/translations/fr.json +++ b/homeassistant/components/ring/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/risco/translations/fr.json b/homeassistant/components/risco/translations/fr.json index 0b33b841e1d..c83e0f8263f 100644 --- a/homeassistant/components/risco/translations/fr.json +++ b/homeassistant/components/risco/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/fr.json b/homeassistant/components/rituals_perfume_genie/translations/fr.json index 223028898c7..ba1d542bfc9 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/fr.json +++ b/homeassistant/components/rituals_perfume_genie/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index 94e31ba445f..b0fb3e6784b 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/rpi_power/translations/he.json b/homeassistant/components/rpi_power/translations/he.json index a4e4e475087..efaad0a039b 100644 --- a/homeassistant/components/rpi_power/translations/he.json +++ b/homeassistant/components/rpi_power/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d3\u05e8\u05d5\u05e9\u05d4 \u05dc\u05e8\u05db\u05d9\u05d1 \u05d6\u05d4, \u05d9\u05e9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05d4\u05dc\u05d9\u05d1\u05d4 \u05e9\u05dc\u05da \u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d5\u05d4\u05d7\u05d5\u05de\u05e8\u05d4 \u05e0\u05ea\u05de\u05db\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/ruckus_unleashed/translations/fr.json b/homeassistant/components/ruckus_unleashed/translations/fr.json index 45620fe7795..bb317c2149f 100644 --- a/homeassistant/components/ruckus_unleashed/translations/fr.json +++ b/homeassistant/components/ruckus_unleashed/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index 725073d5c66..000240517b9 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sense/translations/he.json b/homeassistant/components/sense/translations/he.json index b46ec81b6d5..8c4cc7fb508 100644 --- a/homeassistant/components/sense/translations/he.json +++ b/homeassistant/components/sense/translations/he.json @@ -14,6 +14,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, + "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 Sense \u05e6\u05e8\u05d9\u05da \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da {email}.", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, "user": { @@ -22,6 +23,11 @@ "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df" } + }, + "validation": { + "data": { + "code": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea" + } } } } diff --git a/homeassistant/components/senseme/translations/fr.json b/homeassistant/components/senseme/translations/fr.json index fb185b7ee53..bebe137a135 100644 --- a/homeassistant/components/senseme/translations/fr.json +++ b/homeassistant/components/senseme/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide" + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" }, "flow_title": "{name} - {model} ({host})", "step": { diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 0509886e5ce..09e2e5e5045 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "incorrect_api_key": "Cl\u00e9 d'API non valide pour le compte s\u00e9lectionn\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_devices": "Aucun appareil d\u00e9couvert", "no_username": "Impossible d'obtenir le nom d'utilisateur" }, diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index 8914886b61b..fd9cb2aeecb 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -2,15 +2,15 @@ "device_automation": { "condition_type": { "is_apparent_power": "Puissance apparente actuelle de {entity_name}", - "is_battery_level": "Niveau de la batterie de {entity_name}", + "is_battery_level": "Niveau de la batterie actuel de {entity_name}", "is_carbon_dioxide": "Niveau actuel de concentration de dioxyde de carbone {entity_name}", "is_carbon_monoxide": "Niveau actuel de concentration de monoxyde de carbone {entity_name}", "is_current": "Courant actuel pour {entity_name}", "is_energy": "\u00c9nergie actuelle pour {entity_name}", "is_frequency": "Fr\u00e9quence actuelle de {entity_name}", "is_gas": "Gaz actuel de {entity_name}", - "is_humidity": "Humidit\u00e9 de {entity_name}", - "is_illuminance": "\u00c9clairement de {entity_name}", + "is_humidity": "Humidit\u00e9 actuelle de {entity_name}", + "is_illuminance": "\u00c9clairement lumineux actuel de {entity_name}", "is_nitrogen_dioxide": "Niveau actuel de concentration en dioxyde d'azote de {entity_name}", "is_nitrogen_monoxide": "Niveau actuel de concentration en monoxyde d'azote de {entity_name}", "is_nitrous_oxide": "Niveau actuel de concentration d'oxyde nitreux de {entity_name}", @@ -18,25 +18,25 @@ "is_pm1": "Niveau de concentration actuel de {entity_name}", "is_pm10": "Niveau de concentration actuel de {entity_name}", "is_pm25": "Niveau de concentration actuel de {entity_name}", - "is_power": "Puissance de {entity_name}", + "is_power": "Puissance actuelle de {entity_name}", "is_power_factor": "Facteur de puissance actuel pour {entity_name}", - "is_pressure": "Pression de {entity_name}", + "is_pressure": "Pression actuelle de {entity_name}", "is_reactive_power": "Puissance r\u00e9active actuelle de {entity_name}", - "is_signal_strength": "Force du signal de {entity_name}", + "is_signal_strength": "Force du signal actuelle de {entity_name}", "is_sulphur_dioxide": "Niveau de concentration actuel de {entity_name}", - "is_temperature": "Temp\u00e9rature de {entity_name}", - "is_value": "La valeur actuelle de {entity_name}", + "is_temperature": "Temp\u00e9rature actuelle de {entity_name}", + "is_value": "Valeur actuelle de {entity_name}", "is_volatile_organic_compounds": "Niveau actuel de concentration en compos\u00e9s organiques volatils de {entity_name}", "is_voltage": "Tension actuelle pour {entity_name}" }, "trigger_type": { - "apparent_power": "{entity_name} changement de puissance apparente", + "apparent_power": "Variation de la puissance apparente de {entity_name}", "battery_level": "{entity_name} modification du niveau de batterie", "carbon_dioxide": "{entity_name} changements de concentration de dioxyde de carbone", "carbon_monoxide": "{entity_name} changements de concentration de monoxyde de carbone", "current": "{entity_name} changement de courant", "energy": "{entity_name} changement d'\u00e9nergie", - "frequency": "{entity_name} changements de fr\u00e9quence", + "frequency": "Variation de la fr\u00e9quence de {entity_name}", "gas": "{entity_name} changements de gaz", "humidity": "{entity_name} modification de l'humidit\u00e9", "illuminance": "{entity_name} modification de l'\u00e9clairement", @@ -50,7 +50,7 @@ "power": "{entity_name} modification de la puissance", "power_factor": "{entity_name} changement de facteur de puissance", "pressure": "{entity_name} modification de la pression", - "reactive_power": "{entity_name} changements de puissance r\u00e9active", + "reactive_power": "Variation de la puissance r\u00e9active de {entity_name}", "signal_strength": "{entity_name} modification de la force du signal", "sulphur_dioxide": "{entity_name} changements de concentration de dioxyde de soufre", "temperature": "{entity_name} modification de temp\u00e9rature", diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index 913da4b373d..acad5566ec3 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "bad_dsn": "DSN invalide", + "bad_dsn": "DSN non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sharkiq/translations/fr.json b/homeassistant/components/sharkiq/translations/fr.json index 552715f0d1c..dfa145af8ab 100644 --- a/homeassistant/components/sharkiq/translations/fr.json +++ b/homeassistant/components/sharkiq/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index 83b66645157..cd19af62bad 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "Shelly: {name}", @@ -40,13 +40,13 @@ "btn_down": "{sous-type} bouton en bas", "btn_up": "{sous-type} bouton haut", "double": "{subtype} double-cliqu\u00e9", - "double_push": "{subtype} double pression", + "double_push": "Double pression sur {subtype}", "long": "{subtype} long cliqu\u00e9", - "long_push": "{subtype} appui long", + "long_push": "Pression longue sur {subtype}", "long_single": "{subtype} clic long et simple clic", "single": "{subtype} simple clic", "single_long": "{subtype} simple clic, puis un clic long", - "single_push": "{subtype} simple pression", + "single_push": "Pression simple sur {subtype}", "triple": "{subtype} cliqu\u00e9 trois fois" } } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 912f3e05c01..d50ac11f851 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "still_awaiting_mfa": "En attente de clic sur le message \u00e9lectronique d'authentification multi facteur", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/sleepiq/translations/fr.json b/homeassistant/components/sleepiq/translations/fr.json index cab4affaac1..9c43b00feba 100644 --- a/homeassistant/components/sleepiq/translations/fr.json +++ b/homeassistant/components/sleepiq/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/sma/translations/fr.json b/homeassistant/components/sma/translations/fr.json index 46b3be072f7..c52ca19fafc 100644 --- a/homeassistant/components/sma/translations/fr.json +++ b/homeassistant/components/sma/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "cannot_retrieve_device_info": "Connexion r\u00e9ussie, mais impossible de r\u00e9cup\u00e9rer les informations sur l'appareil", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/smart_meter_texas/translations/fr.json b/homeassistant/components/smart_meter_texas/translations/fr.json index aa84ec33d8c..744b9c6a862 100644 --- a/homeassistant/components/smart_meter_texas/translations/fr.json +++ b/homeassistant/components/smart_meter_texas/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/smarthab/translations/fr.json b/homeassistant/components/smarthab/translations/fr.json index 5b74c827287..5e3381a50e6 100644 --- a/homeassistant/components/smarthab/translations/fr.json +++ b/homeassistant/components/smarthab/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "service": "Erreur de connexion \u00e0 SmartHab. V\u00e9rifiez votre connexion. Le service peut \u00eatre indisponible.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/smartthings/translations/fr.json b/homeassistant/components/smartthings/translations/fr.json index 6051cbbabce..4978c5d160c 100644 --- a/homeassistant/components/smartthings/translations/fr.json +++ b/homeassistant/components/smartthings/translations/fr.json @@ -8,7 +8,7 @@ "app_setup_error": "Impossible de configurer la SmartApp. Veuillez r\u00e9essayer.", "token_forbidden": "Le jeton n'a pas les port\u00e9es OAuth requises.", "token_invalid_format": "Le jeton doit \u00eatre au format UID / GUID", - "token_unauthorized": "Le jeton est invalide ou n'est plus autoris\u00e9.", + "token_unauthorized": "Le jeton n'est pas valide ou n'est plus autoris\u00e9.", "webhook_error": "SmartThings n'a pas pu valider le point de terminaison configur\u00e9 en \u00ab\u00a0base_url\u00a0\u00bb. Veuillez consulter les exigences du composant." }, "step": { diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index 8b7c47304ff..73f9c8491e1 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -12,17 +12,22 @@ "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc webhook. \u05e0\u05d0 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05ea\u05d5\u05d1\u05ea \u05d4-webhook \u05e0\u05d2\u05d9\u05e9\u05d4 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05d5\u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." }, "step": { + "authorize": { + "title": "\u05d4\u05e8\u05e9\u05d0\u05ea Home Assistant" + }, "pat": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea]({token_url}) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}). \u05e4\u05e2\u05d5\u05dc\u05d4 \u05d6\u05d5 \u05ea\u05e9\u05de\u05e9 \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc Home Assistant \u05d1\u05d7\u05e9\u05d1\u05d5\u05df SmartThings \u05e9\u05dc\u05da." + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea]({token_url}) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}). \u05e4\u05e2\u05d5\u05dc\u05d4 \u05d6\u05d5 \u05ea\u05e9\u05de\u05e9 \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc Home Assistant \u05d1\u05d7\u05e9\u05d1\u05d5\u05df SmartThings \u05e9\u05dc\u05da.", + "title": "\u05d4\u05d6\u05e0\u05ea \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea" }, "select_location": { "data": { "location_id": "\u05de\u05d9\u05e7\u05d5\u05dd" }, - "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4-SmartThings \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05dc-Home Assistant. \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05d9\u05e4\u05ea\u05d7 \u05d7\u05dc\u05d5\u05df \u05d7\u05d3\u05e9 \u05d5\u05e0\u05d1\u05e7\u05e9 \u05de\u05de\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d5\u05dc\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 Home Assistant \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05e9\u05e0\u05d1\u05d7\u05e8." + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4-SmartThings \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05dc-Home Assistant. \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05d9\u05e4\u05ea\u05d7 \u05d7\u05dc\u05d5\u05df \u05d7\u05d3\u05e9 \u05d5\u05e0\u05d1\u05e7\u05e9 \u05de\u05de\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d5\u05dc\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 Home Assistant \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05e9\u05e0\u05d1\u05d7\u05e8.", + "title": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05d9\u05e7\u05d5\u05dd" }, "user": { "description": "SmartThings \u05d9\u05d5\u05d2\u05d3\u05e8 \u05dc\u05e9\u05dc\u05d5\u05d7 \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05d3\u05d7\u05d9\u05e4\u05d4 \u05dc-Home Assistant \u05d1\u05db\u05ea\u05d5\u05d1\u05ea:\n> {webhook_url}\n\n\u05d0\u05dd \u05d4\u05d3\u05d1\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05db\u05d5\u05df, \u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", diff --git a/homeassistant/components/smarttub/translations/fr.json b/homeassistant/components/smarttub/translations/fr.json index ec219736281..9796fb079aa 100644 --- a/homeassistant/components/smarttub/translations/fr.json +++ b/homeassistant/components/smarttub/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index 369bac39a1a..fb4e12811fa 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -6,7 +6,7 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "could_not_connect": "Impossible de se connecter \u00e0 l'API solaredge", - "invalid_api_key": "Cl\u00e9 d'API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "site_not_active": "The site n'est pas actif" }, "step": { diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json index 46781252523..cc257e16451 100644 --- a/homeassistant/components/somfy_mylink/translations/fr.json +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{mac} ({ip})", diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json index 7e9cc805102..0793adc1b9f 100644 --- a/homeassistant/components/sonarr/translations/fr.json +++ b/homeassistant/components/sonarr/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/spider/translations/fr.json b/homeassistant/components/spider/translations/fr.json index 8658343db6a..a5e5d021cd2 100644 --- a/homeassistant/components/spider/translations/fr.json +++ b/homeassistant/components/spider/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/squeezebox/translations/fr.json b/homeassistant/components/squeezebox/translations/fr.json index 1dca7a14308..7dda170361d 100644 --- a/homeassistant/components/squeezebox/translations/fr.json +++ b/homeassistant/components/squeezebox/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_server_found": "Impossible de d\u00e9couvrir automatiquement le serveur.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json index 4ce2a6dbbea..b8544d4d469 100644 --- a/homeassistant/components/srp_energy/translations/fr.json +++ b/homeassistant/components/srp_energy/translations/fr.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_account": "L'ID de compte doit \u00eatre un num\u00e9ro \u00e0 9 chiffres", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json index 473492ddd07..fafcf81157b 100644 --- a/homeassistant/components/subaru/translations/fr.json +++ b/homeassistant/components/subaru/translations/fr.json @@ -8,7 +8,7 @@ "bad_pin_format": "Le code PIN doit \u00eatre compos\u00e9 de 4 chiffres", "cannot_connect": "\u00c9chec de connexion", "incorrect_pin": "PIN incorrect", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "pin": { diff --git a/homeassistant/components/surepetcare/translations/fr.json b/homeassistant/components/surepetcare/translations/fr.json index b6704aabae1..dc122998917 100644 --- a/homeassistant/components/surepetcare/translations/fr.json +++ b/homeassistant/components/surepetcare/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index 7df7aa84f6a..ec357e10c72 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -4,25 +4,26 @@ "init": { "data": { "entity_id": "Entit\u00e9 du commutateur" - } + }, + "description": "S\u00e9lectionnez le commutateur correspondant \u00e0 l'interrupteur d'\u00e9clairage." } } }, "device_automation": { "action_type": { "toggle": "Basculer {entity_name}", - "turn_off": "\u00c9teindre {entity_name}", - "turn_on": "Allumer {entity_name}" + "turn_off": "D\u00e9sactiver {entity_name}", + "turn_on": "Activer {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est \u00e9teint", - "is_on": "{entity_name} est allum\u00e9" + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9" }, "trigger_type": { - "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "turned_off": "{entity_name} \u00e9teint", - "turned_on": "{entity_name} allum\u00e9" + "changed_states": "{entity_name} a \u00e9t\u00e9 activ\u00e9 ou d\u00e9sactiv\u00e9", + "toggled": "{entity_name} a \u00e9t\u00e9 activ\u00e9 ou d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} a \u00e9t\u00e9 d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} a \u00e9t\u00e9 activ\u00e9" } }, "state": { @@ -31,5 +32,5 @@ "on": "Activ\u00e9" } }, - "title": "Interrupteur" + "title": "Commutateur" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/de.json b/homeassistant/components/switch_as_x/translations/de.json new file mode 100644 index 00000000000..63a99ad40e2 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "Switch-Entit\u00e4t", + "target_domain": "Typ" + }, + "title": "Mache einen Schalter zu..." + } + } + }, + "title": "Schalter als X" +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index d760e6eeb1a..41b52f253ac 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -28,8 +28,8 @@ "data": { "retry_count": "Nombre de nouvelles tentatives", "retry_timeout": "D\u00e9lai d'attente entre les tentatives", - "scan_timeout": "Combien de temps pour rechercher les donn\u00e9es publicitaires", - "update_time": "Temps entre les mises \u00e0 jour (secondes)" + "scan_timeout": "Dur\u00e9e de la recherche de donn\u00e9es publicitaires", + "update_time": "Dur\u00e9e (en secondes) entre deux mises \u00e0 jour" } } } diff --git a/homeassistant/components/syncthing/translations/fr.json b/homeassistant/components/syncthing/translations/fr.json index 99c31269565..f0d93986445 100644 --- a/homeassistant/components/syncthing/translations/fr.json +++ b/homeassistant/components/syncthing/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/syncthru/translations/fr.json b/homeassistant/components/syncthru/translations/fr.json index 1936e2f4a16..0aaae24fe5c 100644 --- a/homeassistant/components/syncthru/translations/fr.json +++ b/homeassistant/components/syncthru/translations/fr.json @@ -4,7 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "invalid_url": "URL invalide", + "invalid_url": "URL non valide", "syncthru_not_supported": "L'appareil ne prend pas en charge SyncThru", "unknown_state": "\u00c9tat de l'imprimante inconnu, v\u00e9rifiez l'URL et la connectivit\u00e9 r\u00e9seau" }, diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 34766766828..c6488ea7356 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "missing_data": "Donn\u00e9es manquantes: veuillez r\u00e9essayer plus tard ou utilisez une autre configuration", "otp_failed": "\u00c9chec de l'authentification en deux \u00e9tapes, r\u00e9essayez avec un nouveau code d'acc\u00e8s", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json index 1c2a1b23c9c..cbcad2d0330 100644 --- a/homeassistant/components/system_bridge/translations/fr.json +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json index d862a27f392..2e10dc38a2e 100644 --- a/homeassistant/components/tado/translations/fr.json +++ b/homeassistant/components/tado/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "no_homes": "Il n\u2019y a pas de maisons li\u00e9es \u00e0 ce compte tado.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/tailscale/translations/fr.json b/homeassistant/components/tailscale/translations/fr.json index eb307c84de9..1163ec2d151 100644 --- a/homeassistant/components/tailscale/translations/fr.json +++ b/homeassistant/components/tailscale/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index 63416018e78..adcef0c1fd4 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -7,7 +7,7 @@ "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "auth": { diff --git a/homeassistant/components/tile/translations/fr.json b/homeassistant/components/tile/translations/fr.json index a700249d3f7..c178454e48a 100644 --- a/homeassistant/components/tile/translations/fr.json +++ b/homeassistant/components/tile/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index a538750f455..f6dcefac2bb 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "usercode": "Code d'utilisateur non valide pour cet utilisateur \u00e0 cet emplacement" }, "step": { diff --git a/homeassistant/components/tractive/translations/fr.json b/homeassistant/components/tractive/translations/fr.json index ef1a08c15e9..47282699f6f 100644 --- a/homeassistant/components/tractive/translations/fr.json +++ b/homeassistant/components/tractive/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/fr.json b/homeassistant/components/trafikverket_weatherstation/translations/fr.json index d988b3aba97..55a88c6a746 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/fr.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "invalid_station": "Impossible de trouver une station m\u00e9t\u00e9o avec le nom sp\u00e9cifi\u00e9", "more_stations": "Trouv\u00e9 plusieurs stations m\u00e9t\u00e9o avec le nom sp\u00e9cifi\u00e9" }, diff --git a/homeassistant/components/transmission/translations/fr.json b/homeassistant/components/transmission/translations/fr.json index 64efb47c8c3..f027b6909e8 100644 --- a/homeassistant/components/transmission/translations/fr.json +++ b/homeassistant/components/transmission/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" }, "step": { diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index 0f8dd7b6742..c28e62e7ab4 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -2,11 +2,11 @@ "config": { "abort": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "login_error": "Erreur de connexion ( {code} ): {msg}" }, "flow_title": "Configuration Tuya", @@ -45,7 +45,7 @@ "cannot_connect": "\u00c9chec de connexion" }, "error": { - "dev_multi_type": "Plusieurs p\u00e9riph\u00e9riques s\u00e9lectionn\u00e9s \u00e0 configurer doivent \u00eatre du m\u00eame type", + "dev_multi_type": "Si plusieurs appareils sont s\u00e9lectionn\u00e9s pour \u00eatre configur\u00e9s, ils doivent tous \u00eatre du m\u00eame type", "dev_not_config": "Type d'appareil non configurable", "dev_not_found": "Appareil non trouv\u00e9" }, diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 58b27b164d0..d8f1bc0a84c 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -6,7 +6,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "faulty_credentials": "Authentification invalide", + "faulty_credentials": "Authentification non valide", "service_unavailable": "\u00c9chec de connexion", "unknown_client_mac": "Aucun client disponible sur cette adresse MAC" }, diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index 4f14db0005d..592a18427b7 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "protect_version": "La version minimale requise est la v1.20.0. Veuillez mettre \u00e0 jour UniFi Protect, puis r\u00e9essayer.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/upb/translations/fr.json b/homeassistant/components/upb/translations/fr.json index 6f96f42f3dd..72d149a0ee3 100644 --- a/homeassistant/components/upb/translations/fr.json +++ b/homeassistant/components/upb/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_upb_file": "Fichier d'exportation UPB UPStart manquant ou invalide, v\u00e9rifiez le nom et le chemin du fichier.", + "invalid_upb_file": "Fichier d'exportation UPB UPStart manquant ou non valide, v\u00e9rifiez le nom et le chemin d'acc\u00e8s du fichier.", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/upcloud/translations/fr.json b/homeassistant/components/upcloud/translations/fr.json index e4fe2301f6a..43a2c619388 100644 --- a/homeassistant/components/upcloud/translations/fr.json +++ b/homeassistant/components/upcloud/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/uptime/translations/ca.json b/homeassistant/components/uptime/translations/ca.json new file mode 100644 index 00000000000..fe8852d4488 --- /dev/null +++ b/homeassistant/components/uptime/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, + "title": "Temps en funcionament" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/de.json b/homeassistant/components/uptime/translations/de.json new file mode 100644 index 00000000000..1aa173edbd8 --- /dev/null +++ b/homeassistant/components/uptime/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + }, + "title": "Betriebszeit" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/el.json b/homeassistant/components/uptime/translations/el.json new file mode 100644 index 00000000000..d70141f2173 --- /dev/null +++ b/homeassistant/components/uptime/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + } + } + }, + "title": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/et.json b/homeassistant/components/uptime/translations/et.json new file mode 100644 index 00000000000..f1b40328dab --- /dev/null +++ b/homeassistant/components/uptime/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. lubatud on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas alustan seadistamist?" + } + } + }, + "title": "T\u00f6\u00f6aeg" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/fr.json b/homeassistant/components/uptime/translations/fr.json new file mode 100644 index 00000000000..a550e33f51f --- /dev/null +++ b/homeassistant/components/uptime/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + }, + "title": "Dur\u00e9e de fonctionnement" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/he.json b/homeassistant/components/uptime/translations/he.json new file mode 100644 index 00000000000..d22746eb70d --- /dev/null +++ b/homeassistant/components/uptime/translations/he.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + }, + "title": "\u05d6\u05de\u05df \u05e4\u05e2\u05d9\u05dc\u05d5\u05ea" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/it.json b/homeassistant/components/uptime/translations/it.json new file mode 100644 index 00000000000..cba8d0a264f --- /dev/null +++ b/homeassistant/components/uptime/translations/it.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "title": "Tempo di funzionamento" +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/fr.json b/homeassistant/components/uptimerobot/translations/fr.json index 85393f5dc30..674180a1d90 100644 --- a/homeassistant/components/uptimerobot/translations/fr.json +++ b/homeassistant/components/uptimerobot/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API invalide", + "invalid_api_key": "Cl\u00e9 d'API non valide", "reauth_failed_matching_account": "La cl\u00e9 API que vous avez fournie ne correspond pas \u00e0 l\u2019ID de compte pour la configuration existante.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/vallox/translations/fr.json b/homeassistant/components/vallox/translations/fr.json index 4aa4f53e076..c9823756185 100644 --- a/homeassistant/components/vallox/translations/fr.json +++ b/homeassistant/components/vallox/translations/fr.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_host": "Nom d'h\u00f4te ou adresse IP invalide", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json index f0272878190..f3c26abb74a 100644 --- a/homeassistant/components/verisure/translations/fr.json +++ b/homeassistant/components/verisure/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/vesync/translations/fr.json b/homeassistant/components/vesync/translations/fr.json index 00128122217..c71842b3558 100644 --- a/homeassistant/components/vesync/translations/fr.json +++ b/homeassistant/components/vesync/translations/fr.json @@ -4,7 +4,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/vicare/translations/fr.json b/homeassistant/components/vicare/translations/fr.json index 69c8b127bb0..260dbb25006 100644 --- a/homeassistant/components/vicare/translations/fr.json +++ b/homeassistant/components/vicare/translations/fr.json @@ -5,7 +5,7 @@ "unknown": "Erreur inattendue" }, "error": { - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json index bfd455fe379..10289500383 100644 --- a/homeassistant/components/vilfo/translations/fr.json +++ b/homeassistant/components/vilfo/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/vlc_telnet/translations/fr.json b/homeassistant/components/vlc_telnet/translations/fr.json index 7dd5fabfdca..d8f6f374f89 100644 --- a/homeassistant/components/vlc_telnet/translations/fr.json +++ b/homeassistant/components/vlc_telnet/translations/fr.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "{host}", diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json index dc7972fd01d..b9499f72b15 100644 --- a/homeassistant/components/wallbox/translations/fr.json +++ b/homeassistant/components/wallbox/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "reauth_invalid": "\u00c9chec de la r\u00e9authentification\u00a0; Le num\u00e9ro de s\u00e9rie ne correspond pas \u00e0 l'original", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/watttime/translations/fr.json b/homeassistant/components/watttime/translations/fr.json index 91a6c126817..5a31bb3bc2a 100644 --- a/homeassistant/components/watttime/translations/fr.json +++ b/homeassistant/components/watttime/translations/fr.json @@ -5,7 +5,7 @@ "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue", "unknown_coordinates": "Aucune donn\u00e9e pour la latitude/longitude" }, diff --git a/homeassistant/components/whirlpool/translations/fr.json b/homeassistant/components/whirlpool/translations/fr.json index 63e63fd1953..59ffb2aa537 100644 --- a/homeassistant/components/whirlpool/translations/fr.json +++ b/homeassistant/components/whirlpool/translations/fr.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json index 0fb00748e25..0a3851bb816 100644 --- a/homeassistant/components/wilight/translations/fr.json +++ b/homeassistant/components/wilight/translations/fr.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "not_supported_device": "Ce WiLight n'est actuellement pas pris en charge", + "not_supported_device": "Cette WiLight n'est actuellement pas prise en charge", "not_wilight_device": "Cet appareil n'est pas WiLight" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous configurer WiLight {name} ? \n\n Il prend en charge: {components}", + "description": "Voulez-vous configurer la WiLight {name}\u00a0?\n\n Elle prend en charge\u00a0: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wiz/translations/fr.json b/homeassistant/components/wiz/translations/fr.json index 562a6a7f765..290bda2405c 100644 --- a/homeassistant/components/wiz/translations/fr.json +++ b/homeassistant/components/wiz/translations/fr.json @@ -6,7 +6,7 @@ "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { - "bulb_time_out": "Impossible de se connecter \u00e0 l'ampoule. Peut-\u00eatre que l'ampoule est hors ligne ou qu'une mauvaise adresse IP a \u00e9t\u00e9 saisie. Veuillez allumer la lumi\u00e8re et r\u00e9essayer\u00a0!", + "bulb_time_out": "Impossible de se connecter \u00e0 l'ampoule. L'ampoule est peut-\u00eatre hors ligne ou bien une mauvaise adresse IP a \u00e9t\u00e9 saisie. Veuillez allumer la lumi\u00e8re et r\u00e9essayer\u00a0!", "cannot_connect": "\u00c9chec de connexion", "no_ip": "Adresse IP non valide", "no_wiz_light": "L'ampoule ne peut pas \u00eatre connect\u00e9e via l'int\u00e9gration de la plate-forme WiZ.", diff --git a/homeassistant/components/wolflink/translations/fr.json b/homeassistant/components/wolflink/translations/fr.json index 6e3348c3647..7b5916f237d 100644 --- a/homeassistant/components/wolflink/translations/fr.json +++ b/homeassistant/components/wolflink/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", + "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index f64f691f78d..04be4e35e2f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -6,7 +6,7 @@ "not_xiaomi_aqara": "Ce n'est pas une passerelle Xiaomi Aqara, l'appareil d\u00e9couvert ne correspond pas aux passerelles connues" }, "error": { - "discovery_error": "Impossible de d\u00e9couvrir une passerelle Xiaomi Aqara, essayez d'utiliser l'IP du p\u00e9riph\u00e9rique ex\u00e9cutant HomeAssistant comme interface", + "discovery_error": "Aucune passerelle Xiaomi Aqara d\u00e9couverte, essayez d'utiliser en tant qu'interface l'adresse IP de l'appareil ex\u00e9cutant Home Assistant", "invalid_host": "Adresse IP non valide, consultez https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interface r\u00e9seau non valide", "invalid_key": "Cl\u00e9 de passerelle non valide", diff --git a/homeassistant/components/yale_smart_alarm/translations/fr.json b/homeassistant/components/yale_smart_alarm/translations/fr.json index 40530caa974..f003f11b161 100644 --- a/homeassistant/components/yale_smart_alarm/translations/fr.json +++ b/homeassistant/components/yale_smart_alarm/translations/fr.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index 9682f0d9b6f..b2153e6aec0 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion" }, - "flow_title": "{model} {host}", + "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { "description": "Voulez-vous configurer {model} ({host})\u00a0?" @@ -21,7 +21,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Si vous laissez l'adresse IP vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } }, @@ -29,10 +29,10 @@ "step": { "init": { "data": { - "model": "Mod\u00e8le (facultatif)", - "nightlight_switch": "Utiliser l'interrupteur de la veilleuse", - "save_on_change": "Sauvegarder le statut lors d'un changement", - "transition": "Temps de transition (ms)", + "model": "Mod\u00e8le", + "nightlight_switch": "Utiliser le commutateur de veilleuse", + "save_on_change": "Enregistrer l'\u00e9tat lors d'un changement", + "transition": "Dur\u00e9e de transition (en millisecondes)", "use_music_mode": "Activer le mode musique" }, "description": "Si vous ne pr\u00e9cisez pas le mod\u00e8le, il sera automatiquement d\u00e9tect\u00e9." diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index a5e890bcaf4..1044b726b27 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -48,7 +48,7 @@ "zha_options": { "consider_unavailable_battery": "Consid\u00e9rer les appareils aliment\u00e9s par batterie indisponibles apr\u00e8s (secondes)", "consider_unavailable_mains": "Consid\u00e9rer les appareils aliment\u00e9s par le secteur indisponibles apr\u00e8s (secondes)", - "default_light_transition": "Temps de transition de la lumi\u00e8re par d\u00e9faut (en secondes)", + "default_light_transition": "Dur\u00e9e par d\u00e9faut de transition de la lumi\u00e8re (en secondes)", "enable_identify_on_join": "Activer l'effet d'identification quand les appareils rejoignent le r\u00e9seau", "title": "Options g\u00e9n\u00e9rales" } @@ -56,7 +56,7 @@ "device_automation": { "action_type": { "squawk": "Hurlement", - "warn": "Pr\u00e9venir" + "warn": "Avertissement" }, "trigger_subtype": { "both_buttons": "Les deux boutons", diff --git a/homeassistant/components/zoneminder/translations/fr.json b/homeassistant/components/zoneminder/translations/fr.json index 7c730786384..79205bef2d6 100644 --- a/homeassistant/components/zoneminder/translations/fr.json +++ b/homeassistant/components/zoneminder/translations/fr.json @@ -4,7 +4,7 @@ "auth_fail": "L'identifiant ou le mot de passe est incorrect.", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "create_entry": { "default": "Serveur Zoneminder ajout\u00e9." @@ -13,7 +13,7 @@ "auth_fail": "L'identifiant ou le mot de passe est incorrect.", "cannot_connect": "\u00c9chec de connexion", "connection_error": "\u00c9chec de la connexion \u00e0 un serveur ZoneMinder.", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification non valide" }, "flow_title": "ZoneMinder", "step": { diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 6d447b41a22..2645e573b6d 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -15,7 +15,7 @@ "error": { "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS. V\u00e9rifiez la configuration.", "cannot_connect": "\u00c9chec de connexion", - "invalid_ws_url": "URL websocket invalide", + "invalid_ws_url": "URL websocket non valide", "unknown": "Erreur inattendue" }, "flow_title": "{name}", @@ -65,8 +65,8 @@ "device_automation": { "action_type": { "clear_lock_usercode": "Effacer le code utilisateur sur {entity_name}", - "ping": "Pinger l'appareil", - "refresh_value": "Actualisez la ou les valeurs de {entity_name}", + "ping": "Envoyer un \u00ab\u00a0ping\u00a0\u00bb \u00e0 l'appareil", + "refresh_value": "Actualiser la ou les valeurs de {entity_name}", "reset_meter": "R\u00e9initialiser les compteurs sur {subtype}", "set_config_parameter": "D\u00e9finir la valeur du param\u00e8tre de configuration {subtype}", "set_lock_usercode": "D\u00e9finir un code utilisateur sur {entity_name}", @@ -101,7 +101,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_ws_url": "URL websocket invalide", + "invalid_ws_url": "URL websocket non valide", "unknown": "Erreur inattendue" }, "progress": { From 0bd65db31cfcc07b79882e44f555b22899471ec6 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sat, 12 Mar 2022 21:32:35 -0500 Subject: [PATCH 0397/1054] Add switch platform to the Mazda integration (#68025) --- homeassistant/components/mazda/__init__.py | 25 ++++---- homeassistant/components/mazda/switch.py | 70 ++++++++++++++++++++++ tests/components/mazda/test_switch.py | 69 +++++++++++++++++++++ 3 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/mazda/switch.py create mode 100644 tests/components/mazda/test_switch.py diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 1fd0c3bd618..2af4e46bb1a 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -43,6 +43,7 @@ PLATFORMS = [ Platform.DEVICE_TRACKER, Platform.LOCK, Platform.SENSOR, + Platform.SWITCH, ] @@ -120,6 +121,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: service_call.service, ) + if service_call.service in ("start_charging", "stop_charging"): + _LOGGER.warning( + "The mazda.%s service is deprecated and has been replaced by a switch entity; " + "Please use the charging switch entity instead", + service_call.service, + ) + api_method = getattr(api_client, service_call.service) try: if service_call.service == "send_poi": @@ -213,17 +221,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Register services for service in SERVICES: - if service == "send_poi": - hass.services.async_register( - DOMAIN, - service, - async_handle_service_call, - schema=service_schema_send_poi, - ) - else: - hass.services.async_register( - DOMAIN, service, async_handle_service_call, schema=service_schema - ) + hass.services.async_register( + DOMAIN, + service, + async_handle_service_call, + schema=service_schema_send_poi if service == "send_poi" else service_schema, + ) return True diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py new file mode 100644 index 00000000000..3ab3028425f --- /dev/null +++ b/homeassistant/components/mazda/switch.py @@ -0,0 +1,70 @@ +"""Platform for Mazda switch integration.""" +from pymazda import Client as MazdaAPIClient + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import MazdaEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the switch platform.""" + client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + + async_add_entities( + MazdaChargingSwitch(client, coordinator, index) + for index, data in enumerate(coordinator.data) + if data["isElectric"] + ) + + +class MazdaChargingSwitch(MazdaEntity, SwitchEntity): + """Class for the charging switch.""" + + _attr_icon = "mdi:ev-station" + + def __init__( + self, + client: MazdaAPIClient, + coordinator: DataUpdateCoordinator, + index: int, + ) -> None: + """Initialize Mazda charging switch.""" + super().__init__(client, coordinator, index) + + self._attr_name = f"{self.vehicle_name} Charging" + self._attr_unique_id = self.vin + + @property + def is_on(self): + """Return true if the vehicle is charging.""" + return self.data["evStatus"]["chargeInfo"]["charging"] + + async def refresh_status_and_write_state(self): + """Request a status update, retrieve it through the coordinator, and write the state.""" + await self.client.refresh_vehicle_status(self.vehicle_id) + + await self.coordinator.async_request_refresh() + + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs): + """Start charging the vehicle.""" + await self.client.start_charging(self.vehicle_id) + + await self.refresh_status_and_write_state() + + async def async_turn_off(self, **kwargs): + """Stop charging the vehicle.""" + await self.client.stop_charging(self.vehicle_id) + + await self.refresh_status_and_write_state() diff --git a/tests/components/mazda/test_switch.py b/tests/components/mazda/test_switch.py new file mode 100644 index 00000000000..c0fc6f15fb2 --- /dev/null +++ b/tests/components/mazda/test_switch.py @@ -0,0 +1,69 @@ +"""The switch tests for the Mazda Connected Services integration.""" + +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.helpers import entity_registry as er + +from . import init_integration + + +async def test_switch_setup(hass): + """Test setup of the switch entity.""" + await init_integration(hass, electric_vehicle=True) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("switch.my_mazda3_charging") + assert entry + assert entry.unique_id == "JM000000000000000" + + state = hass.states.get("switch.my_mazda3_charging") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charging" + assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" + + assert state.state == STATE_ON + + +async def test_start_charging(hass): + """Test turning on the charging switch.""" + client_mock = await init_integration(hass, electric_vehicle=True) + + client_mock.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.my_mazda3_charging"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.start_charging.assert_called_once() + client_mock.refresh_vehicle_status.assert_called_once() + client_mock.get_vehicle_status.assert_called_once() + client_mock.get_ev_vehicle_status.assert_called_once() + + +async def test_stop_charging(hass): + """Test turning off the charging switch.""" + client_mock = await init_integration(hass, electric_vehicle=True) + + client_mock.reset_mock() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.my_mazda3_charging"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.stop_charging.assert_called_once() + client_mock.refresh_vehicle_status.assert_called_once() + client_mock.get_vehicle_status.assert_called_once() + client_mock.get_ev_vehicle_status.assert_called_once() From d4d47faabb6470fbc7be59b8896cbb3f87bedaaa Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Mar 2022 23:04:08 -0800 Subject: [PATCH 0398/1054] Update google-cloud-pubsub to 2.11.0 (#68074) --- homeassistant/components/google_pubsub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index f4f966538dd..c6690edb52d 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -2,7 +2,7 @@ "domain": "google_pubsub", "name": "Google Pub/Sub", "documentation": "https://www.home-assistant.io/integrations/google_pubsub", - "requirements": ["google-cloud-pubsub==2.10.0"], + "requirements": ["google-cloud-pubsub==2.11.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index e5dfd69b4d9..4306aafa948 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -719,7 +719,7 @@ goodwe==0.2.15 google-api-python-client==2.38.0 # homeassistant.components.google_pubsub -google-cloud-pubsub==2.10.0 +google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud google-cloud-texttospeech==2.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd27c3ff8d3..3f9d2f9e128 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -499,7 +499,7 @@ goodwe==0.2.15 google-api-python-client==2.38.0 # homeassistant.components.google_pubsub -google-cloud-pubsub==2.10.0 +google-cloud-pubsub==2.11.0 # homeassistant.components.nest google-nest-sdm==1.8.0 From 4ea0f7d32175765afdd63bebf1c47c2479c21b16 Mon Sep 17 00:00:00 2001 From: Christopher Thornton Date: Sat, 12 Mar 2022 23:46:35 -0800 Subject: [PATCH 0399/1054] Default somfy_mylink shade's _attr_is_closed to `None` (#68053) The base `Cover` entity requires an explicit value for `_attr_is_closed`. Since the `SomfyShade` is an assumed state, we don't know by default whether the shade is open or not, so we need to explicitly return `None` for `_attr_is_closed` --- homeassistant/components/somfy_mylink/cover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index 7ac0aace47f..d1b09175deb 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -79,6 +79,7 @@ class SomfyShade(RestoreEntity, CoverEntity): self._attr_unique_id = target_id self._attr_name = name self._reverse = reverse + self._attr_is_closed = None self._attr_device_class = device_class self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._target_id)}, From cd3f931dedf9981303fd41c8016164cb27efd89b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 09:04:00 +0100 Subject: [PATCH 0400/1054] Add base entity to Switch as X (#68057) --- .../components/switch_as_x/entity.py | 106 ++++++++++++++++++ homeassistant/components/switch_as_x/light.py | 84 +------------- 2 files changed, 111 insertions(+), 79 deletions(-) create mode 100644 homeassistant/components/switch_as_x/entity.py diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py new file mode 100644 index 00000000000..040a5d35232 --- /dev/null +++ b/homeassistant/components/switch_as_x/entity.py @@ -0,0 +1,106 @@ +"""Base entity for the Switch as X integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import Event, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import Entity, ToggleEntity +from homeassistant.helpers.event import async_track_state_change_event + + +class BaseEntity(Entity): + """Represents a Switch as a X.""" + + _attr_should_poll = False + + def __init__( + self, + name: str, + switch_entity_id: str, + unique_id: str | None, + device_id: str | None = None, + ) -> None: + """Initialize Light Switch.""" + self._device_id = device_id + self._attr_name = name + self._attr_unique_id = unique_id + self._switch_entity_id = switch_entity_id + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self._switch_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + + self._attr_available = True + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def _async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + self.async_state_changed_listener(event) + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._switch_entity_id], _async_state_changed_listener + ) + ) + + # Call once on adding + _async_state_changed_listener() + + # Add this entity to the wrapped switch's device + registry = er.async_get(self.hass) + if registry.async_get(self.entity_id) is not None: + registry.async_update_entity(self.entity_id, device_id=self._device_id) + + +class BaseToggleEntity(BaseEntity, ToggleEntity): + """Represents a Switch as a ToggleEntity.""" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + self._attr_is_on = state.state == STATE_ON diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py index 53128487d5c..93dba7c8551 100644 --- a/homeassistant/components/switch_as_x/light.py +++ b/homeassistant/components/switch_as_x/light.py @@ -1,23 +1,14 @@ """Light support for switch entities.""" from __future__ import annotations -from typing import Any - from homeassistant.components.light import COLOR_MODE_ONOFF, LightEntity -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_ENTITY_ID, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event + +from .entity import BaseToggleEntity async def async_setup_entry( @@ -26,7 +17,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Initialize Light Switch config entry.""" - registry = er.async_get(hass) entity_id = er.async_validate_entity_id( registry, config_entry.options[CONF_ENTITY_ID] @@ -46,72 +36,8 @@ async def async_setup_entry( ) -class LightSwitch(LightEntity): +class LightSwitch(BaseToggleEntity, LightEntity): """Represents a Switch as a Light.""" _attr_color_mode = COLOR_MODE_ONOFF - _attr_should_poll = False _attr_supported_color_modes = {COLOR_MODE_ONOFF} - - def __init__( - self, - name: str, - switch_entity_id: str, - unique_id: str | None, - device_id: str | None = None, - ) -> None: - """Initialize Light Switch.""" - self._device_id = device_id - self._attr_name = name - self._attr_unique_id = unique_id - self._switch_entity_id = switch_entity_id - - async def async_turn_on(self, **kwargs: Any) -> None: - """Forward the turn_on command to the switch in this light switch.""" - await self.hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: self._switch_entity_id}, - blocking=True, - context=self._context, - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Forward the turn_off command to the switch in this light switch.""" - await self.hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: self._switch_entity_id}, - blocking=True, - context=self._context, - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener(event: Event | None = None) -> None: - """Handle child updates.""" - if ( - state := self.hass.states.get(self._switch_entity_id) - ) is None or state.state == STATE_UNAVAILABLE: - self._attr_available = False - return - - self._attr_available = True - self._attr_is_on = state.state == STATE_ON - self.async_write_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, [self._switch_entity_id], async_state_changed_listener - ) - ) - - # Call once on adding - async_state_changed_listener() - - # Add this entity to the wrapped switch's device - registry = er.async_get(self.hass) - if registry.async_get(self.entity_id) is not None: - registry.async_update_entity(self.entity_id, device_id=self._device_id) From 36acb09caea09e7aba7ac20520c82a2394e41956 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 09:52:47 +0100 Subject: [PATCH 0401/1054] Update sqlalchemy to 1.4.32 (#68075) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/components/webostv/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index a6a746f019e..d4ac7fa91eb 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.27"], + "requirements": ["sqlalchemy==1.4.32"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index dfc58474366..d8ced1625ad 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.27"], + "requirements": ["sqlalchemy==1.4.32"], "codeowners": ["@dgomes"], "iot_class": "local_polling" } diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index a60a12aba30..a51d2427b2c 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.1.3", "sqlalchemy==1.4.27"], + "requirements": ["aiowebostv==0.1.3", "sqlalchemy==1.4.32"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{"st": "urn:lge-com:service:webos-second-screen:1"}], "quality_scale": "platinum", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 31520803ff2..7e50f177ffc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.27.1 scapy==2.4.5 -sqlalchemy==1.4.27 +sqlalchemy==1.4.32 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.12.2 diff --git a/requirements_all.txt b/requirements_all.txt index 4306aafa948..85719d56c34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2195,7 +2195,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql # homeassistant.components.webostv -sqlalchemy==1.4.27 +sqlalchemy==1.4.32 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f9d2f9e128..03732799208 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1405,7 +1405,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql # homeassistant.components.webostv -sqlalchemy==1.4.27 +sqlalchemy==1.4.32 # homeassistant.components.srp_energy srpenergy==1.3.6 From 89cf914d3f2098db8150ae45379d55b421f863b2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 09:54:23 +0100 Subject: [PATCH 0402/1054] Update sentry-sdk to 1.5.7 (#68077) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 2b67f1fa312..ff8f2bb7ec8 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.5.6"], + "requirements": ["sentry-sdk==1.5.7"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 85719d56c34..31194641256 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ sendgrid==6.8.2 sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.6 +sentry-sdk==1.5.7 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03732799208..2cd9e5011cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ securetar==2022.2.0 sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.6 +sentry-sdk==1.5.7 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From f5e43d99e41cf66afaa87e16ffa6f9e535e7d392 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 09:54:41 +0100 Subject: [PATCH 0403/1054] Update google-cloud-texttospeech to 2.11.0 (#68076) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 83801d50354..87da1f55fca 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.10.0"], + "requirements": ["google-cloud-texttospeech==2.11.0"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 31194641256..620b8c619a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,7 +722,7 @@ google-api-python-client==2.38.0 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.10.0 +google-cloud-texttospeech==2.11.0 # homeassistant.components.nest google-nest-sdm==1.8.0 From 693e9caa324ac04b6e35b1e67181d693a44c3a07 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 13:03:52 +0100 Subject: [PATCH 0404/1054] Update wled to 0.13.1 (#68084) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index eb99b8519a9..6b72bbf905c 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.13.0"], + "requirements": ["wled==0.13.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 620b8c619a6..4d19294b1da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2417,7 +2417,7 @@ wirelesstagpy==0.8.1 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.0 +wled==0.13.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cd9e5011cd..ee378ddc975 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1543,7 +1543,7 @@ wiffi==1.1.0 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.0 +wled==0.13.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 From 334fa33fabbca2fa188e4adee86d531a83688f40 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Sun, 13 Mar 2022 15:31:57 +0100 Subject: [PATCH 0405/1054] Update home_connect to 0.7.0 (#68089) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 5667d539902..784042f884c 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "dependencies": ["http"], "codeowners": ["@DavidMStraub"], - "requirements": ["homeconnect==0.6.3"], + "requirements": ["homeconnect==0.7.0"], "config_flow": true, "iot_class": "cloud_push", "loggers": ["homeconnect"] diff --git a/requirements_all.txt b/requirements_all.txt index 4d19294b1da..88b64d4aef4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -812,7 +812,7 @@ home-assistant-frontend==20220312.0 # homeassistant-pyozw==0.1.10 # homeassistant.components.home_connect -homeconnect==0.6.3 +homeconnect==0.7.0 # homeassistant.components.homematicip_cloud homematicip==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee378ddc975..c5831991fdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ home-assistant-frontend==20220312.0 # homeassistant-pyozw==0.1.10 # homeassistant.components.home_connect -homeconnect==0.6.3 +homeconnect==0.7.0 # homeassistant.components.homematicip_cloud homematicip==1.0.2 From c35aaef980ffc2c079a2fff44b6ea0be632eac42 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 15:32:56 +0100 Subject: [PATCH 0406/1054] Fix Efergy tests (#68086) --- tests/components/efergy/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/components/efergy/__init__.py b/tests/components/efergy/__init__.py index ddddc56f4e4..5d77acc6838 100644 --- a/tests/components/efergy/__init__.py +++ b/tests/components/efergy/__init__.py @@ -1,12 +1,11 @@ """Tests for Efergy integration.""" from unittest.mock import AsyncMock, patch -from pyefergy import Efergy, exceptions +from pyefergy import exceptions from homeassistant.components.efergy import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture @@ -56,10 +55,6 @@ async def mock_responses( ): """Mock responses from Efergy.""" base_url = "https://engage.efergy.com/mobile_proxy/" - api = Efergy( - token, session=async_get_clientsession(hass), utc_offset="America/New_York" - ) - assert api._utc_offset == 300 if error: aioclient_mock.get( f"{base_url}getInstant?token={token}", From bfb8765752c7849825d6d7fcbfb69852cd48897f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Mar 2022 04:33:55 -1000 Subject: [PATCH 0407/1054] Bump pyisy to 3.0.5 (#68069) * Bump pyisy to 3.0.4 - Fixes #66003 - Changelog: https://github.com/automicus/PyISY/compare/v3.0.1...v3.0.4 * again --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index fe0fa9720ca..89e66533913 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.1"], + "requirements": ["pyisy==3.0.5"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 88b64d4aef4..43718353a09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1549,7 +1549,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.1 +pyisy==3.0.5 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5831991fdf..3976b5a73ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1014,7 +1014,7 @@ pyiqvia==2021.11.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.1 +pyisy==3.0.5 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 4ea921e57a76754c453367b7a478bb28e36b4123 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Sun, 13 Mar 2022 12:27:45 -0400 Subject: [PATCH 0408/1054] Clean up SleepIQ migration (#68092) --- homeassistant/components/sleepiq/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 9b5c9c81683..c12eace2f03 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -144,7 +144,7 @@ async def _async_migrate_unique_ids( if parts[0] not in bed_ids or not old_unique_id.endswith(tuple(sensor_types)): return None - sensor_type = next(filter(old_unique_id.endswith, sensor_types), None) + sensor_type = next(filter(old_unique_id.endswith, sensor_types)) sleeper_name = "_".join(parts[1:]).removesuffix(f"_{sensor_type}") sleeper_id = names_to_ids.get(sleeper_name) @@ -153,7 +153,7 @@ async def _async_migrate_unique_ids( new_unique_id = f"{sleeper_id}_{sensor_type}" - _LOGGER.info( + _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", old_unique_id, new_unique_id, From 25f2e4bd9975e8727fdf5ba4e9b19ad45e44c62c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 19:34:02 +0100 Subject: [PATCH 0409/1054] Small improvements for Switch as X (#68079) * Small improvements forr Switch as X * Test improvements * Remove intregration tests from config flow test --- .../components/switch_as_x/__init__.py | 11 +- .../components/switch_as_x/config_flow.py | 19 ++- homeassistant/components/switch_as_x/const.py | 7 + tests/components/switch_as_x/conftest.py | 16 ++ .../switch_as_x/test_config_flow.py | 79 +++++----- tests/components/switch_as_x/test_init.py | 55 ++++--- tests/components/switch_as_x/test_light.py | 148 ++++++++++++------ 7 files changed, 218 insertions(+), 117 deletions(-) create mode 100644 homeassistant/components/switch_as_x/const.py create mode 100644 tests/components/switch_as_x/conftest.py diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 2c647b1e953..1da70f8029f 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -11,12 +11,11 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.event import async_track_entity_registry_updated_event +from .const import CONF_TARGET_DOMAIN from .light import LightSwitch __all__ = ["LightSwitch"] -DOMAIN = "switch_as_x" - _LOGGER = logging.getLogger(__name__) @@ -72,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # If the tracked switch is no longer in the device, remove our config entry # from the device if ( - not (entity_entry := registry.async_get(data["entity_id"])) + not (entity_entry := registry.async_get(data[CONF_ENTITY_ID])) or not device_registry.async_get(device_id) or entity_entry.device_id == device_id ): @@ -91,12 +90,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_id = async_add_to_device(hass, entry, entity_id) - hass.config_entries.async_setup_platforms(entry, (entry.options["target_domain"],)) + hass.config_entries.async_setup_platforms( + entry, (entry.options[CONF_TARGET_DOMAIN],) + ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( - entry, (entry.options["target_domain"],) + entry, (entry.options[CONF_TARGET_DOMAIN],) ) diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index d59aecbc060..c81a449eea7 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -6,19 +6,26 @@ from typing import Any import voluptuous as vol +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.helpers import helper_config_entry_flow, selector -from . import DOMAIN +from .const import CONF_TARGET_DOMAIN, DOMAIN CONFIG_FLOW = { "user": helper_config_entry_flow.HelperFlowStep( vol.Schema( { - vol.Required("entity_id"): selector.selector( - {"entity": {"domain": "switch"}} + vol.Required(CONF_ENTITY_ID): selector.selector( + {"entity": {"domain": Platform.SWITCH}} ), - vol.Required("target_domain"): selector.selector( - {"select": {"options": ["light"]}} + vol.Required(CONF_TARGET_DOMAIN): selector.selector( + { + "select": { + "options": [ + {"value": Platform.LIGHT, "label": "Light"}, + ] + } + } ), } ) @@ -36,5 +43,5 @@ class SwitchAsXConfigFlowHandler( def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" return helper_config_entry_flow.wrapped_entity_config_entry_title( - self.hass, options["entity_id"] + self.hass, options[CONF_ENTITY_ID] ) diff --git a/homeassistant/components/switch_as_x/const.py b/homeassistant/components/switch_as_x/const.py new file mode 100644 index 00000000000..4963d6fa60b --- /dev/null +++ b/homeassistant/components/switch_as_x/const.py @@ -0,0 +1,7 @@ +"""Constants for the Switch as X integration.""" + +from typing import Final + +DOMAIN: Final = "switch_as_x" + +CONF_TARGET_DOMAIN: Final = "target_domain" diff --git a/tests/components/switch_as_x/conftest.py b/tests/components/switch_as_x/conftest.py new file mode 100644 index 00000000000..f722292fc89 --- /dev/null +++ b/tests/components/switch_as_x/conftest.py @@ -0,0 +1,16 @@ +"""Fixtures for the Switch as X integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.switch_as_x.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 98770610f48..b2a05faa998 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -1,17 +1,21 @@ """Test the Switch as X config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.switch_as_x import DOMAIN, async_setup_entry +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from homeassistant.helpers import entity_registry as er -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_config_flow(hass: HomeAssistant, target_domain) -> None: +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_config_flow( + hass: HomeAssistant, + target_domain: Platform, + mock_setup_entry: AsyncMock, +) -> None: """Test the config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -19,50 +23,38 @@ async def test_config_flow(hass: HomeAssistant, target_domain) -> None: assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - with patch( - "homeassistant.components.switch_as_x.async_setup_entry", - wraps=async_setup_entry, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "entity_id": "switch.ceiling", - "target_domain": target_domain, - }, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, + }, + ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { - "entity_id": "switch.ceiling", - "target_domain": target_domain, + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, } assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry.data == {} assert config_entry.options == { - "entity_id": "switch.ceiling", - "target_domain": target_domain, + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, } - # Check the wrapped switch has a state and is added to the registry - state = hass.states.get(f"{target_domain}.ceiling") - assert state.state == "unavailable" - # Name copied from config entry title - assert state.name == "ceiling" - - # Check the light is added to the entity registry - registry = er.async_get(hass) - entity_entry = registry.async_get(f"{target_domain}.ceiling") - assert entity_entry.unique_id == config_entry.entry_id - - -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_options(hass: HomeAssistant, target_domain) -> None: +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_options( + hass: HomeAssistant, + target_domain: Platform, + mock_setup_entry: AsyncMock, +) -> None: """Test reconfiguring.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -70,15 +62,14 @@ async def test_options(hass: HomeAssistant, target_domain) -> None: assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - with patch( - "homeassistant.components.switch_as_x.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"entity_id": "switch.ceiling", "target_domain": target_domain}, - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, + }, + ) + await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 8eafc417c04..dc4d418d060 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -3,22 +3,28 @@ from unittest.mock import patch import pytest -from homeassistant.components.switch_as_x import DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_config_entry_unregistered_uuid(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_config_entry_unregistered_uuid( + hass: HomeAssistant, target_domain: str +) -> None: """Test light switch setup from config entry with unknown entity registry id.""" fake_uuid = "a266a680b608c32770e6c45bfe6b8411" config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": fake_uuid, "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: fake_uuid, + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) @@ -30,8 +36,8 @@ async def test_config_entry_unregistered_uuid(hass: HomeAssistant, target_domain assert len(hass.states.async_all()) == 0 -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_entity_registry_events(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) -> None: """Test entity registry events are tracked.""" registry = er.async_get(hass) registry_entry = registry.async_get_or_create("switch", "test", "unique") @@ -41,7 +47,10 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain): config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": registry_entry.id, "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: registry_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) @@ -50,22 +59,22 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{target_domain}.abc").state == "on" + assert hass.states.get(f"{target_domain}.abc").state == STATE_ON # Change entity_id new_switch_entity_id = f"{switch_entity_id}_new" registry.async_update_entity(switch_entity_id, new_entity_id=new_switch_entity_id) - hass.states.async_set(new_switch_entity_id, "off") + hass.states.async_set(new_switch_entity_id, STATE_OFF) await hass.async_block_till_done() # Check tracking the new entity_id await hass.async_block_till_done() - assert hass.states.get(f"{target_domain}.abc").state == "off" + assert hass.states.get(f"{target_domain}.abc").state == STATE_OFF # The old entity_id should no longer be tracked - hass.states.async_set(switch_entity_id, "on") + hass.states.async_set(switch_entity_id, STATE_ON) await hass.async_block_till_done() - assert hass.states.get(f"{target_domain}.abc").state == "off" + assert hass.states.get(f"{target_domain}.abc").state == STATE_OFF # Check changing name does not reload the config entry with patch( @@ -84,8 +93,10 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain): assert len(hass.config_entries.async_entries("switch_as_x")) == 0 -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_device_registry_config_entry_1(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_device_registry_config_entry_1( + hass: HomeAssistant, target_domain: str +) -> None: """Test we add our config entry to the tracked switch's device.""" device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) @@ -111,7 +122,10 @@ async def test_device_registry_config_entry_1(hass: HomeAssistant, target_domain switch_as_x_config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": switch_entity_entry.id, "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) @@ -137,8 +151,10 @@ async def test_device_registry_config_entry_1(hass: HomeAssistant, target_domain assert switch_as_x_config_entry.entry_id not in device_entry.config_entries -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_device_registry_config_entry_2(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_device_registry_config_entry_2( + hass: HomeAssistant, target_domain: str +) -> None: """Test we add our config entry to the tracked switch's device.""" device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) @@ -160,7 +176,10 @@ async def test_device_registry_config_entry_2(hass: HomeAssistant, target_domain switch_as_x_config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": switch_entity_entry.id, "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 13ae058f8d9..09fd7808459 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -2,26 +2,44 @@ import pytest from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_EFFECT_LIST, + ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE_VALUE, COLOR_MODE_ONOFF, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, ) -from homeassistant.components.switch_as_x import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -from tests.components.light import common -from tests.components.switch import common as switch_common -async def test_default_state(hass): +async def test_default_state(hass: HomeAssistant) -> None: """Test light switch default state.""" config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": "switch.test", "target_domain": "light"}, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.LIGHT, + }, title="Christmas Tree Lights", ) config_entry.add_to_hass(hass) @@ -32,87 +50,120 @@ async def test_default_state(hass): assert state is not None assert state.state == "unavailable" assert state.attributes["supported_features"] == 0 - assert state.attributes.get("brightness") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("effect_list") is None - assert state.attributes.get("effect") is None + assert state.attributes.get(ATTR_BRIGHTNESS) is None + assert state.attributes.get(ATTR_HS_COLOR) is None + assert state.attributes.get(ATTR_COLOR_TEMP) is None + assert state.attributes.get(ATTR_WHITE_VALUE) is None + assert state.attributes.get(ATTR_EFFECT_LIST) is None + assert state.attributes.get(ATTR_EFFECT) is None assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_ONOFF] assert state.attributes.get(ATTR_COLOR_MODE) is None -async def test_light_service_calls(hass): +async def test_light_service_calls(hass: HomeAssistant) -> None: """Test service calls to light.""" await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": "switch.decorative_lights", "target_domain": "light"}, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.LIGHT, + }, title="decorative_lights", ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("light.decorative_lights").state == "on" + assert hass.states.get("light.decorative_lights").state == STATE_ON - await common.async_toggle(hass, "light.decorative_lights") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "light.decorative_lights"}, + blocking=True, + ) - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.decorative_lights").state == "off" + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("light.decorative_lights").state == STATE_OFF - await common.async_turn_on(hass, "light.decorative_lights") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "light.decorative_lights"}, + blocking=True, + ) - assert hass.states.get("switch.decorative_lights").state == "on" - assert hass.states.get("light.decorative_lights").state == "on" + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("light.decorative_lights").state == STATE_ON assert ( hass.states.get("light.decorative_lights").attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_ONOFF ) - await common.async_turn_off(hass, "light.decorative_lights") - await hass.async_block_till_done() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "light.decorative_lights"}, + blocking=True, + ) - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.decorative_lights").state == "off" + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("light.decorative_lights").state == STATE_OFF -async def test_switch_service_calls(hass): +async def test_switch_service_calls(hass: HomeAssistant) -> None: """Test service calls to switch.""" await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": "switch.decorative_lights", "target_domain": "light"}, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.LIGHT, + }, title="decorative_lights", ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get("light.decorative_lights").state == "on" + assert hass.states.get("light.decorative_lights").state == STATE_ON - await switch_common.async_turn_off(hass, "switch.decorative_lights") - await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.decorative_lights").state == "off" + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("light.decorative_lights").state == STATE_OFF - await switch_common.async_turn_on(hass, "switch.decorative_lights") - await hass.async_block_till_done() + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) - assert hass.states.get("switch.decorative_lights").state == "on" - assert hass.states.get("light.decorative_lights").state == "on" + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("light.decorative_lights").state == STATE_ON -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_config_entry_entity_id(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_config_entry_entity_id( + hass: HomeAssistant, target_domain: Platform +) -> None: """Test light switch setup from config entry with entity id.""" config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": "switch.abc", "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: "switch.abc", + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) @@ -124,6 +175,7 @@ async def test_config_entry_entity_id(hass: HomeAssistant, target_domain): assert DOMAIN in hass.config.components state = hass.states.get(f"{target_domain}.abc") + assert state assert state.state == "unavailable" # Name copied from config entry title assert state.name == "ABC" @@ -131,11 +183,12 @@ async def test_config_entry_entity_id(hass: HomeAssistant, target_domain): # Check the light is added to the entity registry registry = er.async_get(hass) entity_entry = registry.async_get(f"{target_domain}.abc") + assert entity_entry assert entity_entry.unique_id == config_entry.entry_id -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_config_entry_uuid(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: """Test light switch setup from config entry with entity registry id.""" registry = er.async_get(hass) registry_entry = registry.async_get_or_create("switch", "test", "unique") @@ -143,7 +196,10 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain): config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": registry_entry.id, "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: registry_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) @@ -155,8 +211,8 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain): assert hass.states.get(f"{target_domain}.abc") -@pytest.mark.parametrize("target_domain", ("light",)) -async def test_device(hass: HomeAssistant, target_domain): +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: """Test the entity is added to the wrapped entity's device.""" device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) @@ -174,7 +230,10 @@ async def test_device(hass: HomeAssistant, target_domain): switch_as_x_config_entry = MockConfigEntry( data={}, domain=DOMAIN, - options={"entity_id": switch_entity_entry.id, "target_domain": target_domain}, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, title="ABC", ) @@ -184,4 +243,5 @@ async def test_device(hass: HomeAssistant, target_domain): await hass.async_block_till_done() entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry assert entity_entry.device_id == switch_entity_entry.device_id From cc046e64f571fa407dc9940cd2bbb30c7c36d33e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 19:36:36 +0100 Subject: [PATCH 0410/1054] Update freezegun to 1.2.0 (#68090) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0e78d456e95..5c02473e0e1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt codecov==2.1.12 coverage==6.3.2 -freezegun==1.1.0 +freezegun==1.2.0 mock-open==1.4.0 mypy==0.940 pre-commit==2.17.0 From b138a759b61077e17f596c7fcfefe574a94d5e5f Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 13 Mar 2022 19:37:59 +0100 Subject: [PATCH 0411/1054] Update plugwise module to 0.16.8 (#68082) --- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/sensor.py | 6 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../all_data.json | 33 +++++++++---------- .../fixtures/anna_heatpump/all_data.json | 14 +++----- tests/components/plugwise/test_diagnostics.py | 33 +++++++++---------- tests/components/plugwise/test_sensor.py | 3 +- 8 files changed, 42 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 4f1417ae018..f0624f8c026 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.16.6"], + "requirements": ["plugwise==0.16.8"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 4ee75e21a45..9099ee96990 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -267,7 +267,7 @@ async def async_setup_entry( """Set up the Smile sensors from a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[PlugwiseSensorEnity] = [] + entities: list[PlugwiseSensorEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in SENSORS: if ( @@ -277,7 +277,7 @@ async def async_setup_entry( continue entities.append( - PlugwiseSensorEnity( + PlugwiseSensorEntity( coordinator, device_id, description, @@ -287,7 +287,7 @@ async def async_setup_entry( async_add_entities(entities) -class PlugwiseSensorEnity(PlugwiseEntity, SensorEntity): +class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): """Represent Plugwise Sensors.""" def __init__( diff --git a/requirements_all.txt b/requirements_all.txt index 43718353a09..fa5ab8613fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1214,7 +1214,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.16.6 +plugwise==0.16.8 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3976b5a73ba..9cbb9bccff8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -799,7 +799,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.16.6 +plugwise==0.16.8 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 65b96074cc0..92b2321621d 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -20,7 +20,7 @@ "model": "Lisa", "name": "Zone Lisa Bios", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": [ @@ -66,7 +66,7 @@ "mode": "heat", "sensors": { "temperature": 16.5, - "setpoint": 13, + "setpoint": 13.0, "battery": 67 } }, @@ -79,7 +79,7 @@ "model": "Tom/Floor", "name": "Floor kraan", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { @@ -98,12 +98,12 @@ "model": "Tom/Floor", "name": "Bios Cv Thermostatic Radiator ", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { "temperature": 17.2, - "setpoint": 13, + "setpoint": 13.0, "battery": 62, "temperature_difference": -0.2, "valve_position": 0.0 @@ -118,7 +118,7 @@ "model": "Lisa", "name": "Zone Lisa WK", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": [ @@ -194,12 +194,12 @@ "model": "Tom/Floor", "name": "Thermostatic Radiator Jessie", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { "temperature": 17.1, - "setpoint": 15, + "setpoint": 15.0, "battery": 62, "temperature_difference": 0.1, "valve_position": 0.0 @@ -255,9 +255,6 @@ "model": "Unknown", "name": "OnOff", "vendor": null, - "lower_bound": 10, - "upper_bound": 90, - "resolution": 1, "binary_sensors": { "heating_state": true }, @@ -360,7 +357,7 @@ "model": "Lisa", "name": "Zone Thermostat Jessie", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": [ @@ -406,7 +403,7 @@ "mode": "auto", "sensors": { "temperature": 17.2, - "setpoint": 15, + "setpoint": 15.0, "battery": 37 } }, @@ -419,12 +416,12 @@ "model": "Tom/Floor", "name": "Thermostatic Radiator Badkamer", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { "temperature": 19.1, - "setpoint": 14, + "setpoint": 14.0, "battery": 51, "temperature_difference": -0.4, "valve_position": 0.0 @@ -439,7 +436,7 @@ "model": "Lisa", "name": "Zone Thermostat Badkamer", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": [ @@ -485,7 +482,7 @@ "mode": "auto", "sensors": { "temperature": 18.9, - "setpoint": 14, + "setpoint": 14.0, "battery": 92 } }, @@ -519,7 +516,7 @@ "model": "Tom/Floor", "name": "CV Kraan Garage", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "preset_modes": [ diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index e9e62b77bb0..d6b0744744d 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -16,9 +16,6 @@ "model": "Generic heater", "name": "OpenTherm", "vendor": "Techneco", - "lower_bound": -10, - "upper_bound": 40, - "resolution": 1, "compressor_state": true, "binary_sensors": { "dhw_state": false, @@ -37,8 +34,7 @@ }, "switches": { "dhw_cm_switch": false - }, - "cooling_active": false + } }, "015ae9ea3f964e668e490fa39da3870b": { "class": "gateway", @@ -65,8 +61,8 @@ "model": "Anna", "name": "Anna", "vendor": "Plugwise", - "lower_bound": 4, - "upper_bound": 30, + "lower_bound": 4.0, + "upper_bound": 30.0, "resolution": 0.1, "preset_modes": [ "no_frost", @@ -107,10 +103,10 @@ "mode": "heat", "sensors": { "temperature": 19.3, - "setpoint": 21, + "setpoint": 21.0, "illuminance": 86.0, "cooling_activation_outdoor_temperature": 21.0, - "cooling_deactivation_threshold": 4 + "cooling_deactivation_threshold": 4.0 } } } diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index 67ab7728d1c..8d544bfb363 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -40,7 +40,7 @@ async def test_diagnostics( "model": "Lisa", "name": "Zone Lisa Bios", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], @@ -63,7 +63,7 @@ async def test_diagnostics( "last_used": "Badkamer Schema", "schedule_temperature": 15.0, "mode": "heat", - "sensors": {"temperature": 16.5, "setpoint": 13, "battery": 67}, + "sensors": {"temperature": 16.5, "setpoint": 13.0, "battery": 67}, }, "b310b72a0e354bfab43089919b9a88bf": { "class": "thermo_sensor", @@ -74,7 +74,7 @@ async def test_diagnostics( "model": "Tom/Floor", "name": "Floor kraan", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { @@ -93,12 +93,12 @@ async def test_diagnostics( "model": "Tom/Floor", "name": "Bios Cv Thermostatic Radiator ", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { "temperature": 17.2, - "setpoint": 13, + "setpoint": 13.0, "battery": 62, "temperature_difference": -0.2, "valve_position": 0.0, @@ -113,7 +113,7 @@ async def test_diagnostics( "model": "Lisa", "name": "Zone Lisa WK", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], @@ -160,12 +160,12 @@ async def test_diagnostics( "model": "Tom/Floor", "name": "Thermostatic Radiator Jessie", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { "temperature": 17.1, - "setpoint": 15, + "setpoint": 15.0, "battery": 62, "temperature_difference": 0.1, "valve_position": 0.0, @@ -216,9 +216,6 @@ async def test_diagnostics( "model": "Unknown", "name": "OnOff", "vendor": None, - "lower_bound": 10, - "upper_bound": 90, - "resolution": 1, "binary_sensors": {"heating_state": True}, "sensors": { "water_temperature": 70.0, @@ -307,7 +304,7 @@ async def test_diagnostics( "model": "Lisa", "name": "Zone Thermostat Jessie", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], @@ -330,7 +327,7 @@ async def test_diagnostics( "last_used": "CV Jessie", "schedule_temperature": 15.0, "mode": "auto", - "sensors": {"temperature": 17.2, "setpoint": 15, "battery": 37}, + "sensors": {"temperature": 17.2, "setpoint": 15.0, "battery": 37}, }, "680423ff840043738f42cc7f1ff97a36": { "class": "thermo_sensor", @@ -341,12 +338,12 @@ async def test_diagnostics( "model": "Tom/Floor", "name": "Thermostatic Radiator Badkamer", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "sensors": { "temperature": 19.1, - "setpoint": 14, + "setpoint": 14.0, "battery": 51, "temperature_difference": -0.4, "valve_position": 0.0, @@ -361,7 +358,7 @@ async def test_diagnostics( "model": "Lisa", "name": "Zone Thermostat Badkamer", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], @@ -384,7 +381,7 @@ async def test_diagnostics( "last_used": "Badkamer Schema", "schedule_temperature": 15.0, "mode": "auto", - "sensors": {"temperature": 18.9, "setpoint": 14, "battery": 92}, + "sensors": {"temperature": 18.9, "setpoint": 14.0, "battery": 92}, }, "675416a629f343c495449970e2ca37b5": { "class": "router", @@ -413,7 +410,7 @@ async def test_diagnostics( "model": "Tom/Floor", "name": "CV Kraan Garage", "vendor": "Plugwise", - "lower_bound": 0, + "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index 6f5309d3810..edb479354bb 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -56,8 +56,7 @@ async def test_anna_as_smt_climate_sensor_entities( async def test_anna_climate_sensor_entities( hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry ) -> None: - """Test creation of climate related sensor entities as single master thermostat.""" - mock_smile_anna.single_master_thermostat.return_value = False + """Test creation of climate related sensor entities.""" state = hass.states.get("sensor.opentherm_outdoor_temperature") assert state assert float(state.state) == 3.0 From 0b1663b150d04428b4485c8c25086981c5076d7d Mon Sep 17 00:00:00 2001 From: Dmitry Kosenkov <1144095+Junker@users.noreply.github.com> Date: Mon, 14 Mar 2022 01:40:09 +0700 Subject: [PATCH 0412/1054] Update slixmpp to 1.8.0.1 (#68080) fix for python 3.10 --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 840f2cd677d..d5b08b6bc23 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.7.1"], + "requirements": ["slixmpp==1.8.0.1"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"] diff --git a/requirements_all.txt b/requirements_all.txt index fa5ab8613fb..58eb464e382 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2148,7 +2148,7 @@ skybellpy==0.6.3 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.7.1 +slixmpp==1.8.0.1 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 From 276d795bf798f3ab8342206a5f8698c5afebeb19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Mar 2022 23:10:40 +0100 Subject: [PATCH 0413/1054] Remove unused types for emoji (#68091) --- requirements_test.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5c02473e0e1..856d1827a84 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -35,7 +35,6 @@ types-backports==0.1.3 types-certifi==0.1.4 types-chardet==0.1.5 types-decorator==0.1.7 -types-emoji==1.2.4 types-enum34==0.1.8 types-ipaddress==0.1.5 types-pkg-resources==0.1.3 From 65821f9492c6ec4a8f817a1651bc00052f490f49 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 14 Mar 2022 00:21:06 +0000 Subject: [PATCH 0414/1054] [ci skip] Translation update --- .../components/apple_tv/translations/he.json | 2 +- .../components/elkm1/translations/he.json | 2 ++ .../evil_genius_labs/translations/he.json | 1 + .../components/group/translations/nl.json | 16 +++++----- .../components/group/translations/pt.json | 29 +++++++++++++++++++ .../kaleidescape/translations/he.json | 20 +++++++++++++ .../components/nam/translations/he.json | 2 +- .../components/powerwall/translations/he.json | 8 +++-- .../components/season/translations/he.json | 7 +++++ .../components/season/translations/pt.json | 7 +++++ .../switch_as_x/translations/he.json | 11 +++++++ .../switch_as_x/translations/pt.json | 11 +++++++ .../components/twinkly/translations/he.json | 2 +- .../components/uptime/translations/hu.json | 13 +++++++++ .../components/uptime/translations/ja.json | 13 +++++++++ .../components/uptime/translations/nl.json | 13 +++++++++ .../components/uptime/translations/pt-BR.json | 13 +++++++++ .../components/uptime/translations/pt.json | 13 +++++++++ .../components/uptime/translations/ru.json | 13 +++++++++ .../uptime/translations/zh-Hant.json | 13 +++++++++ 20 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/kaleidescape/translations/he.json create mode 100644 homeassistant/components/season/translations/he.json create mode 100644 homeassistant/components/season/translations/pt.json create mode 100644 homeassistant/components/switch_as_x/translations/he.json create mode 100644 homeassistant/components/switch_as_x/translations/pt.json create mode 100644 homeassistant/components/uptime/translations/hu.json create mode 100644 homeassistant/components/uptime/translations/ja.json create mode 100644 homeassistant/components/uptime/translations/nl.json create mode 100644 homeassistant/components/uptime/translations/pt-BR.json create mode 100644 homeassistant/components/uptime/translations/pt.json create mode 100644 homeassistant/components/uptime/translations/ru.json create mode 100644 homeassistant/components/uptime/translations/zh-Hant.json diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 209cd7069f0..03e7dc5d4fe 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -14,7 +14,7 @@ "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "pair_with_pin": { "data": { diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json index eb49e33c019..8d7b5c9b945 100644 --- a/homeassistant/components/elkm1/translations/he.json +++ b/homeassistant/components/elkm1/translations/he.json @@ -15,6 +15,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "temperature_unit": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d4\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05e9\u05d1\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 ElkM1.", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" @@ -23,6 +24,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", + "temperature_unit": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d4\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05e9\u05d1\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 ElkM1.", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" diff --git a/homeassistant/components/evil_genius_labs/translations/he.json b/homeassistant/components/evil_genius_labs/translations/he.json index 00011f86933..ab2db7eb7d0 100644 --- a/homeassistant/components/evil_genius_labs/translations/he.json +++ b/homeassistant/components/evil_genius_labs/translations/he.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index e1eb8b0914e..c636c56f744 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -12,8 +12,8 @@ }, "cover": { "data": { - "entities": "Groepsleden", - "name": "Groepsnaam", + "entities": "Leden", + "name": "Naam", "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" @@ -26,8 +26,8 @@ }, "fan": { "data": { - "entities": "Groepsleden", - "name": "Groepsnaam", + "entities": "Leden", + "name": "Naam", "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" @@ -46,8 +46,8 @@ }, "light": { "data": { - "entities": "Groepsleden", - "name": "Groepsnaam", + "entities": "Leden", + "name": "Naam", "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" @@ -60,8 +60,8 @@ }, "media_player": { "data": { - "entities": "Groepsleden", - "name": "Groepsnaam", + "entities": "Leden", + "name": "Naam", "title": "Nieuwe groep" }, "description": "Selecteer groepsopties" diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 1dfe38b7b71..9333d19b997 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -1,4 +1,33 @@ { + "config": { + "step": { + "media_player": { + "data": { + "entities": "Membros", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "fan_options": { + "data": { + "entities": "Membros" + } + }, + "light_options": { + "data": { + "entities": "Membros" + } + }, + "media_player_options": { + "data": { + "entities": "Membros" + } + } + } + }, "state": { "_": { "closed": "Fechada", diff --git a/homeassistant/components/kaleidescape/translations/he.json b/homeassistant/components/kaleidescape/translations/he.json new file mode 100644 index 00000000000..79d265193d5 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{model} ({name})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/he.json b/homeassistant/components/nam/translations/he.json index dd6dcc60585..80ebc5ae4fd 100644 --- a/homeassistant/components/nam/translations/he.json +++ b/homeassistant/components/nam/translations/he.json @@ -9,7 +9,7 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Nettigo Air Monitor \u05d1-{host}?" diff --git a/homeassistant/components/powerwall/translations/he.json b/homeassistant/components/powerwall/translations/he.json index c4a69c402c4..53849afa25b 100644 --- a/homeassistant/components/powerwall/translations/he.json +++ b/homeassistant/components/powerwall/translations/he.json @@ -10,8 +10,11 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{ip_address}", + "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc-powerwall" + }, "reauth_confim": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" @@ -23,7 +26,8 @@ "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d9\u05d0 \u05d1\u05d3\u05e8\u05da \u05db\u05dc\u05dc 5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05d9\u05d3\u05d5\u05e8\u05d9 \u05e2\u05d1\u05d5\u05e8 Backup Gateway \u05d5\u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05d4 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d8\u05e1\u05dc\u05d4 \u05d0\u05d5 \u05d1-5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05de\u05e6\u05d0\u05d4 \u05d1\u05ea\u05d5\u05da \u05d4\u05d3\u05dc\u05ea \u05e2\u05d1\u05d5\u05e8 Backup Gateway 2." + "description": "\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05d4\u05d9\u05d0 \u05d1\u05d3\u05e8\u05da \u05db\u05dc\u05dc 5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05d9\u05d3\u05d5\u05e8\u05d9 \u05e2\u05d1\u05d5\u05e8 Backup Gateway \u05d5\u05e0\u05d9\u05ea\u05df \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d5\u05ea\u05d4 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d8\u05e1\u05dc\u05d4 \u05d0\u05d5 \u05d1-5 \u05d4\u05ea\u05d5\u05d5\u05d9\u05dd \u05d4\u05d0\u05d7\u05e8\u05d5\u05e0\u05d9\u05dd \u05e9\u05dc \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05e0\u05de\u05e6\u05d0\u05d4 \u05d1\u05ea\u05d5\u05da \u05d4\u05d3\u05dc\u05ea \u05e2\u05d1\u05d5\u05e8 Backup Gateway 2.", + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc-powerwall" } } } diff --git a/homeassistant/components/season/translations/he.json b/homeassistant/components/season/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/season/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/pt.json b/homeassistant/components/season/translations/pt.json new file mode 100644 index 00000000000..b7bb07e9522 --- /dev/null +++ b/homeassistant/components/season/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/he.json b/homeassistant/components/switch_as_x/translations/he.json new file mode 100644 index 00000000000..8ca833876fe --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pt.json b/homeassistant/components/switch_as_x/translations/pt.json new file mode 100644 index 00000000000..558030dc42d --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "target_domain": "Tipo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/he.json b/homeassistant/components/twinkly/translations/he.json index db9e846ce56..6dd018f5c7c 100644 --- a/homeassistant/components/twinkly/translations/he.json +++ b/homeassistant/components/twinkly/translations/he.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7 (\u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP) \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05de\u05e0\u05e6\u05e0\u05e5 \u05e9\u05dc\u05da" + "host": "\u05de\u05d0\u05e8\u05d7" } } } diff --git a/homeassistant/components/uptime/translations/hu.json b/homeassistant/components/uptime/translations/hu.json new file mode 100644 index 00000000000..241c28b48ea --- /dev/null +++ b/homeassistant/components/uptime/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/ja.json b/homeassistant/components/uptime/translations/ja.json new file mode 100644 index 00000000000..99dc644a0e5 --- /dev/null +++ b/homeassistant/components/uptime/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, + "title": "\u30a2\u30c3\u30d7\u30bf\u30a4\u30e0" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/nl.json b/homeassistant/components/uptime/translations/nl.json new file mode 100644 index 00000000000..786fec37f6b --- /dev/null +++ b/homeassistant/components/uptime/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u beginnen met instellen?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pt-BR.json b/homeassistant/components/uptime/translations/pt-BR.json new file mode 100644 index 00000000000..fdec46961b5 --- /dev/null +++ b/homeassistant/components/uptime/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Tempo de atividade" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pt.json b/homeassistant/components/uptime/translations/pt.json new file mode 100644 index 00000000000..a61cff40967 --- /dev/null +++ b/homeassistant/components/uptime/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 foi configurado. S\u00f3 \u00e9 possivel existir uma \u00fanica configura\u00e7\u00e3o." + }, + "step": { + "user": { + "description": "Quer iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Tempo de atividade" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/ru.json b/homeassistant/components/uptime/translations/ru.json new file mode 100644 index 00000000000..7c6270221a7 --- /dev/null +++ b/homeassistant/components/uptime/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, + "title": "\u0412\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u0435\u0440\u0432\u0435\u0440\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/zh-Hant.json b/homeassistant/components/uptime/translations/zh-Hant.json new file mode 100644 index 00000000000..ed0b902348c --- /dev/null +++ b/homeassistant/components/uptime/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, + "title": "\u904b\u4f5c\u6642\u9593" +} \ No newline at end of file From a6c189b450ea268486e84b1a2310d1f7d20c6f24 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 14 Mar 2022 01:30:37 +0100 Subject: [PATCH 0415/1054] Add Pure devices to Sensibo (#67695) --- homeassistant/components/sensibo/climate.py | 2 - .../components/sensibo/coordinator.py | 9 ++ homeassistant/components/sensibo/number.py | 1 - homeassistant/components/sensibo/select.py | 2 +- homeassistant/components/sensibo/sensor.py | 92 ++++++++++++++++--- 5 files changed, 90 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 9299952024c..cec25d2a918 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -103,8 +103,6 @@ async def async_setup_entry( entities = [ SensiboClimate(coordinator, device_id) for device_id, device_data in coordinator.data.parsed.items() - # Remove none climate devices - if device_data["hvac_modes"] and device_data["temp"] ] async_add_entities(entities) diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 7d37ae7f235..b79ae9d0923 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -167,6 +167,12 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): rssi=measurement.get("rssi"), ) + # Add information for pure devices + pure_conf = dev["pureBoostConfig"] + pure_sensitivity = pure_conf.get("sensitivity") if pure_conf else None + pure_boost_enabled = pure_conf.get("enabled") if pure_conf else None + pm25 = dev["measurements"].get("pm25") + device_data[unique_id] = { "id": unique_id, "mac": mac, @@ -200,6 +206,9 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): "calibration_hum": calibration_hum, "full_capabilities": capabilities, "motion_sensors": motion_sensors, + "pure_sensitivity": pure_sensitivity, + "pure_boost_enabled": pure_boost_enabled, + "pm25": pm25, } return SensiboData(raw=data, parsed=device_data) diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index d3cc4d57830..001269ae168 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -66,7 +66,6 @@ async def async_setup_entry( SensiboNumber(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() for description in NUMBER_TYPES - if device_data["hvac_modes"] and device_data["temp"] ) diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index cb569620cca..d211a9dd223 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -58,7 +58,7 @@ async def async_setup_entry( SensiboSelect(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() for description in SELECT_TYPES - if device_data["hvac_modes"] and description.key in device_data["full_features"] + if description.key in device_data["full_features"] ) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index cb5833ee985..21f2bf2b5da 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -12,6 +13,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -24,25 +26,39 @@ from homeassistant.helpers.typing import StateType from .const import DOMAIN from .coordinator import MotionSensor, SensiboDataUpdateCoordinator -from .entity import SensiboMotionBaseEntity +from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity @dataclass -class BaseEntityDescriptionMixin: +class MotionBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" value_fn: Callable[[MotionSensor], StateType] @dataclass -class SensiboSensorEntityDescription( - SensorEntityDescription, BaseEntityDescriptionMixin +class DeviceBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[dict[str, Any]], StateType] + + +@dataclass +class SensiboMotionSensorEntityDescription( + SensorEntityDescription, MotionBaseEntityDescriptionMixin ): """Describes Sensibo Motion sensor entity.""" -MOTION_SENSOR_TYPES: tuple[SensiboSensorEntityDescription, ...] = ( - SensiboSensorEntityDescription( +@dataclass +class SensiboDeviceSensorEntityDescription( + SensorEntityDescription, DeviceBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( + SensiboMotionSensorEntityDescription( key="rssi", device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, @@ -53,7 +69,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboSensorEntityDescription, ...] = ( value_fn=lambda data: data.rssi, entity_registry_enabled_default=False, ), - SensiboSensorEntityDescription( + SensiboMotionSensorEntityDescription( key="battery_voltage", device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -63,7 +79,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboSensorEntityDescription, ...] = ( icon="mdi:battery", value_fn=lambda data: data.battery_voltage, ), - SensiboSensorEntityDescription( + SensiboMotionSensorEntityDescription( key="humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, @@ -72,7 +88,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboSensorEntityDescription, ...] = ( icon="mdi:water", value_fn=lambda data: data.humidity, ), - SensiboSensorEntityDescription( + SensiboMotionSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, @@ -82,6 +98,23 @@ MOTION_SENSOR_TYPES: tuple[SensiboSensorEntityDescription, ...] = ( value_fn=lambda data: data.temperature, ), ) +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="pm25", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + name="PM2.5", + icon="mdi:air-filter", + value_fn=lambda data: data["pm25"], + ), + SensiboDeviceSensorEntityDescription( + key="pure_sensitivity", + name="Pure Sensitivity", + icon="mdi:air-filter", + value_fn=lambda data: data["pure_sensitivity"], + ), +) async def async_setup_entry( @@ -91,19 +124,28 @@ async def async_setup_entry( coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] + + entities.extend( SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) for device_id, device_data in coordinator.data.parsed.items() for sensor_id, sensor_data in device_data["motion_sensors"].items() for description in MOTION_SENSOR_TYPES if device_data["motion_sensors"] ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in DEVICE_SENSOR_TYPES + if device_data[description.key] is not None + ) + async_add_entities(entities) class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): """Representation of a Sensibo Motion Sensor.""" - entity_description: SensiboSensorEntityDescription + entity_description: SensiboMotionSensorEntityDescription def __init__( self, @@ -111,7 +153,7 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): device_id: str, sensor_id: str, sensor_data: MotionSensor, - entity_description: SensiboSensorEntityDescription, + entity_description: SensiboMotionSensorEntityDescription, ) -> None: """Initiate Sensibo Motion Sensor.""" super().__init__( @@ -131,3 +173,29 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): def native_value(self) -> StateType: """Return value of sensor.""" return self.entity_description.value_fn(self.sensor_data) + + +class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): + """Representation of a Sensibo Device Sensor.""" + + entity_description: SensiboDeviceSensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboDeviceSensorEntityDescription, + ) -> None: + """Initiate Sensibo Device Sensor.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data['name']} {entity_description.name}" + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.entity_description.value_fn(self.device_data) From b18096fc54faa442ff6c3f71c689fccf4fbf6840 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Mar 2022 20:44:55 -1000 Subject: [PATCH 0416/1054] Remove unused columns from states/events tables (#68078) --- homeassistant/components/recorder/__init__.py | 3 +-- homeassistant/components/recorder/models.py | 2 -- tests/components/recorder/test_purge.py | 19 ------------------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 410ad165ce1..4f1401aaee7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -907,9 +907,9 @@ class Recorder(threading.Thread): try: if event.event_type == EVENT_STATE_CHANGED: dbevent = Events.from_event(event, event_data="{}") + dbevent.event_data = None else: dbevent = Events.from_event(event) - dbevent.created = event.time_fired self.event_session.add(dbevent) except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) @@ -928,7 +928,6 @@ class Recorder(threading.Thread): if not has_new_state: dbstate.state = None dbstate.event = dbevent - dbstate.created = event.time_fired self.event_session.add(dbstate) if has_new_state: self._old_states[dbstate.entity_id] = dbstate diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 579f47ed4a7..b49189a9c3a 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -92,7 +92,6 @@ class Events(Base): # type: ignore[misc,valid-type] event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) time_fired = Column(DATETIME_TYPE, index=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) @@ -161,7 +160,6 @@ class States(Base): # type: ignore[misc,valid-type] ) last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 8920843e8fe..77b5ad3d191 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -356,7 +356,6 @@ async def test_purge_edge_case( event_type="EVENT_TEST_PURGE", event_data="{}", origin="LOCAL", - created=timestamp, time_fired=timestamp, ) ) @@ -368,7 +367,6 @@ async def test_purge_edge_case( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, event_id=1001, ) ) @@ -416,7 +414,6 @@ async def test_purge_cutoff_date( event_type="KEEP", event_data="{}", origin="LOCAL", - created=timestamp_keep, time_fired=timestamp_keep, ) ) @@ -428,7 +425,6 @@ async def test_purge_cutoff_date( attributes="{}", last_changed=timestamp_keep, last_updated=timestamp_keep, - created=timestamp_keep, event_id=1000, ) ) @@ -439,7 +435,6 @@ async def test_purge_cutoff_date( event_type="PURGE", event_data="{}", origin="LOCAL", - created=timestamp_purge, time_fired=timestamp_purge, ) ) @@ -451,7 +446,6 @@ async def test_purge_cutoff_date( attributes="{}", last_changed=timestamp_purge, last_updated=timestamp_purge, - created=timestamp_purge, event_id=1000 + row, ) ) @@ -519,7 +513,6 @@ async def test_purge_filtered_states( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, ) ) # Add states and state_changed events that should be keeped @@ -541,7 +534,6 @@ async def test_purge_filtered_states( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, old_state_id=1, ) timestamp = dt_util.utcnow() - timedelta(days=4) @@ -552,7 +544,6 @@ async def test_purge_filtered_states( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, old_state_id=2, ) state_3 = States( @@ -562,7 +553,6 @@ async def test_purge_filtered_states( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, old_state_id=62, # keep ) session.add_all((state_1, state_2, state_3)) @@ -573,7 +563,6 @@ async def test_purge_filtered_states( event_type="EVENT_KEEP", event_data="{}", origin="LOCAL", - created=timestamp, time_fired=timestamp, ) ) @@ -652,7 +641,6 @@ async def test_purge_filtered_events( event_type="EVENT_PURGE", event_data="{}", origin="LOCAL", - created=timestamp, time_fired=timestamp, ) ) @@ -745,7 +733,6 @@ async def test_purge_filtered_events_state_changed( event_type="EVENT_KEEP", event_data="{}", origin="LOCAL", - created=timestamp, time_fired=timestamp, ) ) @@ -758,7 +745,6 @@ async def test_purge_filtered_events_state_changed( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, old_state_id=1, ) timestamp = dt_util.utcnow() - timedelta(days=4) @@ -769,7 +755,6 @@ async def test_purge_filtered_events_state_changed( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, old_state_id=2, ) state_3 = States( @@ -779,7 +764,6 @@ async def test_purge_filtered_events_state_changed( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, old_state_id=62, # keep ) session.add_all((state_1, state_2, state_3)) @@ -991,7 +975,6 @@ async def _add_test_events(hass: HomeAssistant, instance: recorder.Recorder): event_type=event_type, event_data=json.dumps(event_data), origin="LOCAL", - created=timestamp, time_fired=timestamp, ) ) @@ -1094,7 +1077,6 @@ def _add_state_and_state_changed_event( attributes="{}", last_changed=timestamp, last_updated=timestamp, - created=timestamp, event_id=event_id, ) ) @@ -1104,7 +1086,6 @@ def _add_state_and_state_changed_event( event_type=EVENT_STATE_CHANGED, event_data="{}", origin="LOCAL", - created=timestamp, time_fired=timestamp, ) ) From 8e76948297b8e3cea4831bdd8ce8b809595f3646 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 14 Mar 2022 07:58:34 +0100 Subject: [PATCH 0417/1054] Add binary_sensor platform for Sensibo (#68088) --- .coveragerc | 1 + .../components/sensibo/binary_sensor.py | 179 ++++++++++++++++++ homeassistant/components/sensibo/const.py | 8 +- .../components/sensibo/coordinator.py | 8 + 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensibo/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index a10f3ca997f..778983e55fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1011,6 +1011,7 @@ omit = homeassistant/components/senseme/light.py homeassistant/components/senseme/switch.py homeassistant/components/sensibo/__init__.py + homeassistant/components/sensibo/binary_sensor.py homeassistant/components/sensibo/climate.py homeassistant/components/sensibo/coordinator.py homeassistant/components/sensibo/diagnostics.py diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py new file mode 100644 index 00000000000..fef81c6d7c1 --- /dev/null +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -0,0 +1,179 @@ +"""Binary Sensor platform for Sensibo integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, LOGGER +from .coordinator import MotionSensor, SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity + + +@dataclass +class MotionBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[MotionSensor], bool | None] + + +@dataclass +class DeviceBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[dict[str, Any]], bool | None] + + +@dataclass +class SensiboMotionBinarySensorEntityDescription( + BinarySensorEntityDescription, MotionBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +@dataclass +class SensiboDeviceBinarySensorEntityDescription( + BinarySensorEntityDescription, DeviceBaseEntityDescriptionMixin +): + """Describes Sensibo Motion sensor entity.""" + + +MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( + SensiboMotionBinarySensorEntityDescription( + key="alive", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + name="Alive", + icon="mdi:wifi", + value_fn=lambda data: data.alive, + ), + SensiboMotionBinarySensorEntityDescription( + key="is_main_sensor", + entity_category=EntityCategory.DIAGNOSTIC, + name="Main Sensor", + icon="mdi:connection", + value_fn=lambda data: data.is_main_sensor, + ), + SensiboMotionBinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + name="Motion", + icon="mdi:motion-sensor", + value_fn=lambda data: data.motion, + ), +) + +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( + SensiboDeviceBinarySensorEntityDescription( + key="room_occupied", + device_class=BinarySensorDeviceClass.MOTION, + name="Room Occupied", + icon="mdi:motion-sensor", + value_fn=lambda data: data["room_occupied"], + ), + SensiboDeviceBinarySensorEntityDescription( + key="update_available", + device_class=BinarySensorDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, + name="Update Available", + icon="mdi:rocket-launch", + value_fn=lambda data: data["update_available"], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo binary sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] + LOGGER.debug("parsed data: %s", coordinator.data.parsed) + entities.extend( + SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) + for device_id, device_data in coordinator.data.parsed.items() + for sensor_id, sensor_data in device_data["motion_sensors"].items() + for description in MOTION_SENSOR_TYPES + if device_data["motion_sensors"] + ) + LOGGER.debug("start device %s", entities) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for description in DEVICE_SENSOR_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if device_data[description.key] is not None + ) + LOGGER.debug("list: %s", entities) + + async_add_entities(entities) + + +class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): + """Representation of a Sensibo Motion Binary Sensor.""" + + entity_description: SensiboMotionBinarySensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + sensor_id: str, + sensor_data: MotionSensor, + entity_description: SensiboMotionBinarySensorEntityDescription, + ) -> None: + """Initiate Sensibo Motion Binary Sensor.""" + super().__init__( + coordinator, + device_id, + sensor_id, + sensor_data, + entity_description.name, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{sensor_id}-{entity_description.key}" + self._attr_name = ( + f"{self.device_data['name']} Motion Sensor {entity_description.name}" + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value_fn(self.sensor_data) + + +class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): + """Representation of a Sensibo Device Binary Sensor.""" + + entity_description: SensiboDeviceBinarySensorEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboDeviceBinarySensorEntityDescription, + ) -> None: + """Initiate Sensibo Device Binary Sensor.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data['name']} {entity_description.name}" + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.entity_description.value_fn(self.device_data) diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index d39f941d63e..59f0d1c1179 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -12,7 +12,13 @@ LOGGER = logging.getLogger(__package__) DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" -PLATFORMS = [Platform.CLIMATE, Platform.NUMBER, Platform.SELECT, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, +] ALL = ["all"] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index b79ae9d0923..6aaf53a0e73 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -173,6 +173,12 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): pure_boost_enabled = pure_conf.get("enabled") if pure_conf else None pm25 = dev["measurements"].get("pm25") + # Binary sensors for main device + room_occupied = dev["roomIsOccupied"] + update_available = bool( + dev["firmwareVersion"] != dev["currentlyAvailableFirmwareVersion"] + ) + device_data[unique_id] = { "id": unique_id, "mac": mac, @@ -209,6 +215,8 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): "pure_sensitivity": pure_sensitivity, "pure_boost_enabled": pure_boost_enabled, "pm25": pm25, + "room_occupied": room_occupied, + "update_available": update_available, } return SensiboData(raw=data, parsed=device_data) From cea21a00b38938da1748cfceb5d291530510d3ee Mon Sep 17 00:00:00 2001 From: Jeef Date: Mon, 14 Mar 2022 00:58:55 -0600 Subject: [PATCH 0418/1054] Add intellifire UDP discovery at configuration start (#67002) Co-authored-by: J. Nick Koston --- .../components/intellifire/config_flow.py | 122 +++++++++--- .../components/intellifire/strings.json | 9 +- .../intellifire/translations/en.json | 33 ++-- tests/components/intellifire/conftest.py | 22 +++ .../intellifire/test_config_flow.py | 180 ++++++++++++++++-- 5 files changed, 310 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index c541c733e08..869504d22a2 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -4,29 +4,31 @@ from __future__ import annotations from typing import Any from aiohttp import ClientConnectionError -from intellifire4py import IntellifireAsync +from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN +from .const import DOMAIN, LOGGER STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) +MANUAL_ENTRY_STRING = "IP Address" # Simplified so it does not have to be translated -async def validate_input(hass: HomeAssistant, host: str) -> str: + +async def validate_host_input(host: str) -> str: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ api = IntellifireAsync(host) await api.poll() - + serial = api.data.serial + LOGGER.debug("Found a fireplace: %s", serial) # Return the serial number which will be used to calculate a unique ID for the device/sensors - return api.data.serial + return serial class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -34,30 +36,94 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self): + """Initialize the Config Flow Handler.""" + self._config_context = {} + self._not_configured_hosts: list[str] = [] + + async def _find_fireplaces(self): + """Perform UDP discovery.""" + fireplace_finder = AsyncUDPFireplaceFinder() + discovered_hosts = await fireplace_finder.search_fireplace(timeout=1) + configured_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + if CONF_HOST in entry.data # CONF_HOST will be missing for ignored entries + } + + self._not_configured_hosts = [ + ip for ip in discovered_hosts if ip not in configured_hosts + ] + LOGGER.debug("Discovered Hosts: %s", discovered_hosts) + LOGGER.debug("Configured Hosts: %s", configured_hosts) + LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts) + + async def _async_validate_and_create_entry(self, host: str) -> FlowResult: + """Validate and create the entry.""" + self._async_abort_entries_match({CONF_HOST: host}) + serial = await validate_host_input(host) + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self.async_create_entry( + title=f"Fireplace {serial}", + data={CONF_HOST: host}, + ) + + async def async_step_manual_device_entry(self, user_input=None): + """Handle manual input of local IP configuration.""" + errors = {} + host = user_input.get(CONF_HOST) if user_input else None + if user_input is not None: + try: + return await self._async_validate_and_create_entry(host) + except (ConnectionError, ClientConnectionError): + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="manual_device_entry", + errors=errors, + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), + ) + + async def async_step_pick_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Pick which device to configure.""" + errors = {} + + if user_input is not None: + if user_input[CONF_HOST] == MANUAL_ENTRY_STRING: + return await self.async_step_manual_device_entry() + + try: + return await self._async_validate_and_create_entry( + user_input[CONF_HOST] + ) + except (ConnectionError, ClientConnectionError): + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="pick_device", + errors=errors, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): vol.In( + self._not_configured_hosts + [MANUAL_ENTRY_STRING] + ) + } + ), + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the initial step.""" - if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) - errors = {} + """Start the user flow.""" - try: - serial = await validate_input(self.hass, user_input[CONF_HOST]) - except (ConnectionError, ClientConnectionError): - errors["base"] = "cannot_connect" - else: - await self.async_set_unique_id(serial) - self._abort_if_unique_id_configured( - updates={CONF_HOST: user_input[CONF_HOST]} - ) + # Launch fireplaces discovery + await self._find_fireplaces() - return self.async_create_entry( - title="Fireplace", - data={CONF_HOST: user_input[CONF_HOST]}, - ) - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors - ) + if self._not_configured_hosts: + LOGGER.debug("Running Step: pick_device") + return await self.async_step_pick_device() + LOGGER.debug("Running Step: manual_device_entry") + return await self.async_step_manual_device_entry() diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index 52d57eda809..d5d3f344c8e 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -1,7 +1,13 @@ { "config": { "step": { - "user": { + "manual_device_entry": { + "description": "Local Configuration", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "pick_device": { "data": { "host": "[%key:common::config_flow::data::host%]" } @@ -15,3 +21,4 @@ } } } + diff --git a/homeassistant/components/intellifire/translations/en.json b/homeassistant/components/intellifire/translations/en.json index 0a4ba36e285..0f7538e7413 100644 --- a/homeassistant/components/intellifire/translations/en.json +++ b/homeassistant/components/intellifire/translations/en.json @@ -1,18 +1,27 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again" + }, + "step": { + "manual_device_entry": { + "title": "IntelliFire - Local Config", + "description": "Enter the IP address of the IntelliFire unit on your local network.", + "data": { + "host": "Host (IP Address)" + } }, - "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "user": { + "description": "Username and password are the same information used in your IntelliFire Android/iOS application.", + "title": "IntelliFire Config" }, - "step": { - "user": { - "data": { - "host": "Host" - } - } + "pick_device": { + "title": "Device Selection", + "description": "The following IntelliFire devices were discovered. Please select which you wish to configure." } + } } -} \ No newline at end of file + } \ No newline at end of file diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index 2bbb3318090..a9de5a260bc 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -14,6 +14,28 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup +@pytest.fixture() +def mock_fireplace_finder_none() -> Generator[None, MagicMock, None]: + """Mock fireplace finder.""" + mock_found_fireplaces = Mock() + mock_found_fireplaces.ips = [] + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" + ): + yield mock_found_fireplaces + + +@pytest.fixture() +def mock_fireplace_finder_single() -> Generator[None, MagicMock, None]: + """Mock fireplace finder.""" + mock_found_fireplaces = Mock() + mock_found_fireplaces.ips = ["192.168.1.69"] + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" + ): + yield mock_found_fireplaces + + @pytest.fixture def mock_intellifire_config_flow() -> Generator[None, MagicMock, None]: """Return a mocked IntelliFire client.""" diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index c9d08318d3c..2e130bfa14e 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -1,41 +1,152 @@ """Test the IntelliFire config flow.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries +from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import DOMAIN +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from tests.common import MockConfigEntry -async def test_form( + +async def test_no_discovery( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_intellifire_config_flow: MagicMock, ) -> None: - """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + """Test we should get the manual discovery form - because no discovered fireplaces.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None + assert result["errors"] == {} + assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", + CONF_HOST: "1.1.1.1", }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Fireplace" - assert result2["data"] == {"host": "1.1.1.1"} - + assert result2["title"] == "Fireplace 12345" + assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect( - hass: HomeAssistant, mock_intellifire_config_flow: MagicMock +async def test_single_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test single fireplace UDP discovery.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69"], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "192.168.1.69"} + ) + await hass.async_block_till_done() + print("Result:", result) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Fireplace 12345" + assert result2["data"] == {CONF_HOST: "192.168.1.69"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_manual_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test for multiple firepalce discovery - involing a pick_device step.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "pick_device" + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MANUAL_ENTRY_STRING} + ) + + await hass.async_block_till_done() + assert result2["step_id"] == "manual_device_entry" + + +async def test_multi_discovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test for multiple fireplace discovery - involving a pick_device step.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "pick_device" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} + ) + await hass.async_block_till_done() + assert result["step_id"] == "pick_device" + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + + +async def test_multi_discovery_cannot_connect( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test for multiple fireplace discovery - involving a pick_device step.""" + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.69", "192.168.1.33", "192.168.169"], + ): + + mock_intellifire_config_flow.poll.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pick_device" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_cannot_connect_manual_entry( + hass: HomeAssistant, + mock_intellifire_config_flow: MagicMock, + mock_fireplace_finder_single: AsyncMock, ) -> None: """Test we handle cannot connect error.""" mock_intellifire_config_flow.poll.side_effect = ConnectionError @@ -43,13 +154,52 @@ async def test_form_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", + CONF_HOST: "1.1.1.1", }, ) assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_picker_already_discovered( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test single fireplace UDP discovery.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "192.168.1.3", + }, + title="Fireplace", + unique_id=44444, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", + return_value=["192.168.1.3"], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "192.168.1.4", + }, + ) + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Fireplace 12345" + assert result2["data"] == {CONF_HOST: "192.168.1.4"} + assert len(mock_setup_entry.mock_calls) == 2 From dfdf5f258368539dd12b2c964b263a135349bd92 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 09:50:55 +0100 Subject: [PATCH 0419/1054] Add fixture for enabling disabled entities in WLED tests (#68087) --- tests/components/conftest.py | 13 ++++- tests/components/wled/test_sensor.py | 71 +++++----------------------- 2 files changed, 23 insertions(+), 61 deletions(-) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 9e029e159a1..f153263cbc6 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,5 +1,6 @@ """Fixtures for component testing.""" -from unittest.mock import patch +from collections.abc import Generator +from unittest.mock import AsyncMock, patch import pytest @@ -22,3 +23,13 @@ def prevent_io(): return_value=[], ): yield + + +@pytest.fixture +def entity_registry_enabled_by_default() -> Generator[AsyncMock, None, None]: + """Test fixture that ensures all entities are enabled in the registry.""" + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + return_value=True, + ) as mock_entity_registry_enabled_by_default: + yield mock_entity_registry_enabled_by_default diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index bf3ef3b060b..8d16407b88d 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the WLED sensor platform.""" from datetime import datetime -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -28,61 +28,12 @@ async def test_sensors( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_wled: MagicMock, + entity_registry_enabled_by_default: AsyncMock, ) -> None: """Test the creation and values of the WLED sensors.""" registry = er.async_get(hass) - - # Pre-create registry entries for disabled by default sensors - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aabbccddeeff_uptime", - suggested_object_id="wled_rgb_light_uptime", - disabled_by=None, - ) - - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aabbccddeeff_free_heap", - suggested_object_id="wled_rgb_light_free_memory", - disabled_by=None, - ) - - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aabbccddeeff_wifi_signal", - suggested_object_id="wled_rgb_light_wifi_signal", - disabled_by=None, - ) - - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aabbccddeeff_wifi_rssi", - suggested_object_id="wled_rgb_light_wifi_rssi", - disabled_by=None, - ) - - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aabbccddeeff_wifi_channel", - suggested_object_id="wled_rgb_light_wifi_channel", - disabled_by=None, - ) - - registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "aabbccddeeff_wifi_bssid", - suggested_object_id="wled_rgb_light_wifi_bssid", - disabled_by=None, - ) - - # Setup mock_config_entry.add_to_hass(hass) + test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -124,19 +75,19 @@ async def test_sensors( assert entry.unique_id == "aabbccddeeff_free_heap" assert entry.entity_category is EntityCategory.DIAGNOSTIC - state = hass.states.get("sensor.wled_rgb_light_wifi_signal") + state = hass.states.get("sensor.wled_rgb_light_wi_fi_signal") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "76" assert entry.entity_category is EntityCategory.DIAGNOSTIC - entry = registry.async_get("sensor.wled_rgb_light_wifi_signal") + entry = registry.async_get("sensor.wled_rgb_light_wi_fi_signal") assert entry assert entry.unique_id == "aabbccddeeff_wifi_signal" assert entry.entity_category is EntityCategory.DIAGNOSTIC - state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") + state = hass.states.get("sensor.wled_rgb_light_wi_fi_rssi") assert state assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert ( @@ -145,29 +96,29 @@ async def test_sensors( ) assert state.state == "-62" - entry = registry.async_get("sensor.wled_rgb_light_wifi_rssi") + entry = registry.async_get("sensor.wled_rgb_light_wi_fi_rssi") assert entry assert entry.unique_id == "aabbccddeeff_wifi_rssi" assert entry.entity_category is EntityCategory.DIAGNOSTIC - state = hass.states.get("sensor.wled_rgb_light_wifi_channel") + state = hass.states.get("sensor.wled_rgb_light_wi_fi_channel") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "11" - entry = registry.async_get("sensor.wled_rgb_light_wifi_channel") + entry = registry.async_get("sensor.wled_rgb_light_wi_fi_channel") assert entry assert entry.unique_id == "aabbccddeeff_wifi_channel" assert entry.entity_category is EntityCategory.DIAGNOSTIC - state = hass.states.get("sensor.wled_rgb_light_wifi_bssid") + state = hass.states.get("sensor.wled_rgb_light_wi_fi_bssid") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "AA:AA:AA:AA:AA:BB" - entry = registry.async_get("sensor.wled_rgb_light_wifi_bssid") + entry = registry.async_get("sensor.wled_rgb_light_wi_fi_bssid") assert entry assert entry.unique_id == "aabbccddeeff_wifi_bssid" assert entry.entity_category is EntityCategory.DIAGNOSTIC From 6b36ada4ec7972ad13deb36295f24ae90c6305b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 11:35:19 +0100 Subject: [PATCH 0420/1054] Update pytest to 7.1.0 (#68108) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 856d1827a84..944b8dbed00 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,7 +24,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==2.1.0 pytest-xdist==2.4.0 -pytest==7.0.1 +pytest==7.1.0 requests_mock==1.9.2 respx==0.19.0 stdlib-list==0.7.0 From fb703ddc5d68727635b5d8058f56e935466051fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 11:36:34 +0100 Subject: [PATCH 0421/1054] Update pyupgrade to v2.31.1 (#68110) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66e794f95fa..ddef6569ae4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index d296c2be72d..802ca899bf6 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.31.0 +pyupgrade==2.31.1 yamllint==1.26.3 From a29afc3731fb48a875144f06e6054a604985cf60 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 11:41:56 +0100 Subject: [PATCH 0422/1054] Clean up Whois tests use entity_registry_enabled_by_default fixture (#68113) --- tests/components/whois/conftest.py | 10 ---------- tests/components/whois/test_sensor.py | 6 +++--- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index ef14750356e..056f6058bd2 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -124,13 +124,3 @@ async def init_integration_missing_some_attrs( await hass.async_block_till_done() return mock_config_entry - - -@pytest.fixture -def enable_all_entities() -> Generator[AsyncMock, None, None]: - """Test fixture that ensures all entities are enabled in the registry.""" - with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - return_value=True, - ) as mock_entity_registry_enabled_by_default: - yield mock_entity_registry_enabled_by_default diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index e824522ed09..89b84dc5849 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -22,7 +22,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.freeze_time("2022-01-01 12:00:00", tz_offset=0) async def test_whois_sensors( hass: HomeAssistant, - enable_all_entities: AsyncMock, + entity_registry_enabled_by_default: AsyncMock, init_integration: MockConfigEntry, ) -> None: """Test the Whois sensors.""" @@ -146,7 +146,7 @@ async def test_whois_sensors( @pytest.mark.freeze_time("2022-01-01 12:00:00", tz_offset=0) async def test_whois_sensors_missing_some_attrs( hass: HomeAssistant, - enable_all_entities: AsyncMock, + entity_registry_enabled_by_default: AsyncMock, init_integration_missing_some_attrs: MockConfigEntry, ) -> None: """Test the Whois sensors with owner and reseller missing.""" @@ -213,7 +213,7 @@ async def test_disabled_by_default_sensors( async def test_no_data( hass: HomeAssistant, mock_whois: MagicMock, - enable_all_entities: AsyncMock, + entity_registry_enabled_by_default: AsyncMock, init_integration: MockConfigEntry, entity_id: str, ) -> None: From 6aacaa744e72150e97516617d1b7a665a1578260 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 11:49:16 +0100 Subject: [PATCH 0423/1054] Only create WLED current sensors when available (#68116) --- homeassistant/components/wled/sensor.py | 8 +++++++- tests/components/wled/test_sensor.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index b37238aef64..720158938c7 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -44,6 +44,8 @@ class WLEDSensorEntityDescription( ): """Describes WLED sensor entity.""" + exists_fn: Callable[[WLEDDevice], bool] = lambda _: True + SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( WLEDSensorEntityDescription( @@ -54,6 +56,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.power, + exists_fn=lambda device: bool(device.info.leds.max_power), ), WLEDSensorEntityDescription( key="info_leds_count", @@ -68,6 +71,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.CURRENT, value_fn=lambda device: device.info.leds.max_power, + exists_fn=lambda device: bool(device.info.leds.max_power), ), WLEDSensorEntityDescription( key="uptime", @@ -132,7 +136,9 @@ async def async_setup_entry( """Set up WLED sensor based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - WLEDSensorEntity(coordinator, description) for description in SENSORS + WLEDSensorEntity(coordinator, description) + for description in SENSORS + if description.exists_fn(coordinator.data) ) diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 8d16407b88d..4d9632ff5b8 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -189,3 +189,20 @@ async def test_no_wifi_support( state = hass.states.get(f"sensor.wled_rgb_light_wifi_{key}") assert state assert state.state == STATE_UNKNOWN + + +async def test_no_current_measurement( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test missing current information when no max power is defined.""" + device = mock_wled.update.return_value + device.info.leds.max_power = 0 + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.wled_rgb_light_max_current") is None + assert hass.states.get("sensor.wled_rgb_light_estimated_current") is None From c70c699af0397b677f84abfb8d0de97cb087aa66 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 11:59:55 +0100 Subject: [PATCH 0424/1054] Add cover platform to Switch as X (#68107) --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/cover.py | 83 ++++++++++++ tests/components/switch_as_x/test_cover.py | 121 ++++++++++++++++++ tests/components/switch_as_x/test_init.py | 95 ++++++++++++++ tests/components/switch_as_x/test_light.py | 98 -------------- 5 files changed, 300 insertions(+), 98 deletions(-) create mode 100644 homeassistant/components/switch_as_x/cover.py create mode 100644 tests/components/switch_as_x/test_cover.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index c81a449eea7..ed11ae25489 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -22,6 +22,7 @@ CONFIG_FLOW = { { "select": { "options": [ + {"value": Platform.COVER, "label": "Cover"}, {"value": Platform.LIGHT, "label": "Light"}, ] } diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py new file mode 100644 index 00000000000..3825953ed63 --- /dev/null +++ b/homeassistant/components/switch_as_x/cover.py @@ -0,0 +1,83 @@ +"""Cover support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Cover Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + CoverSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class CoverSwitch(BaseEntity, CoverEntity): + """Represents a Switch as a Cover.""" + + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + self._attr_is_closed = state.state != STATE_ON diff --git a/tests/components/switch_as_x/test_cover.py b/tests/components/switch_as_x/test_cover.py new file mode 100644 index 00000000000..d8317a51b8c --- /dev/null +++ b/tests/components/switch_as_x/test_cover.py @@ -0,0 +1,121 @@ +"""Tests for the Switch as X Cover platform.""" +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_CLOSED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test cover switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.COVER, + }, + title="Garage Door", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("cover.garage_door") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 3 + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls to cover.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.COVER, + }, + title="garage_door", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("cover.garage_door").state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "cover.garage_door"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.garage_door").state == STATE_CLOSED + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {CONF_ENTITY_ID: "cover.garage_door"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("cover.garage_door").state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {CONF_ENTITY_ID: "cover.garage_door"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.garage_door").state == STATE_CLOSED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("cover.garage_door").state == STATE_OPEN + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.garage_door").state == STATE_CLOSED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("cover.garage_door").state == STATE_OPEN diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index dc4d418d060..ca87673dfba 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -200,3 +200,98 @@ async def test_device_registry_config_entry_2( # Check that the switch_as_x config entry is removed from the device device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +async def test_config_entry_entity_id( + hass: HomeAssistant, target_domain: Platform +) -> None: + """Test light switch setup from config entry with entity id.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.abc", + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert DOMAIN in hass.config.components + + state = hass.states.get(f"{target_domain}.abc") + assert state + assert state.state == "unavailable" + # Name copied from config entry title + assert state.name == "ABC" + + # Check the light is added to the entity registry + registry = er.async_get(hass) + entity_entry = registry.async_get(f"{target_domain}.abc") + assert entity_entry + assert entity_entry.unique_id == config_entry.entry_id + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: + """Test light switch setup from config entry with entity registry id.""" + registry = er.async_get(hass) + registry_entry = registry.async_get_or_create("switch", "test", "unique") + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: registry_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{target_domain}.abc") + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: + """Test the entity is added to the wrapped entity's device.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + test_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=test_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", "test", "unique", device_id=device_entry.id + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry + assert entity_entry.device_id == switch_entity_entry.device_id diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 09fd7808459..75e62008e7b 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -1,6 +1,4 @@ """Tests for the Switch as X Light platform.""" -import pytest - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -25,7 +23,6 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -150,98 +147,3 @@ async def test_switch_service_calls(hass: HomeAssistant) -> None: assert hass.states.get("switch.decorative_lights").state == STATE_ON assert hass.states.get("light.decorative_lights").state == STATE_ON - - -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) -async def test_config_entry_entity_id( - hass: HomeAssistant, target_domain: Platform -) -> None: - """Test light switch setup from config entry with entity id.""" - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: "switch.abc", - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert DOMAIN in hass.config.components - - state = hass.states.get(f"{target_domain}.abc") - assert state - assert state.state == "unavailable" - # Name copied from config entry title - assert state.name == "ABC" - - # Check the light is added to the entity registry - registry = er.async_get(hass) - entity_entry = registry.async_get(f"{target_domain}.abc") - assert entity_entry - assert entity_entry.unique_id == config_entry.entry_id - - -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) -async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: - """Test light switch setup from config entry with entity registry id.""" - registry = er.async_get(hass) - registry_entry = registry.async_get_or_create("switch", "test", "unique") - - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: registry_entry.id, - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert hass.states.get(f"{target_domain}.abc") - - -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) -async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: - """Test the entity is added to the wrapped entity's device.""" - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) - - test_config_entry = MockConfigEntry() - - device_entry = device_registry.async_get_or_create( - config_entry_id=test_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - switch_entity_entry = entity_registry.async_get_or_create( - "switch", "test", "unique", device_id=device_entry.id - ) - - switch_as_x_config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: switch_entity_entry.id, - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - ) - - switch_as_x_config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) - await hass.async_block_till_done() - - entity_entry = entity_registry.async_get(f"{target_domain}.abc") - assert entity_entry - assert entity_entry.device_id == switch_entity_entry.device_id From c8fb86a8ed6c1de310a1cc394780522f4ec8c3ba Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 14 Mar 2022 12:39:44 +0100 Subject: [PATCH 0425/1054] Fix MQTT false positive deprecation warnings (#68117) --- homeassistant/components/mqtt/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 96074acab37..c74417ece37 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -130,6 +130,13 @@ DEFAULT_KEEPALIVE = 60 DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_TLS_PROTOCOL = "auto" +DEFAULT_VALUES = { + CONF_PORT: DEFAULT_PORT, + CONF_WILL_MESSAGE: DEFAULT_WILL, + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, +} + ATTR_TOPIC_TEMPLATE = "topic_template" ATTR_PAYLOAD_TEMPLATE = "payload_template" @@ -186,7 +193,7 @@ CONFIG_SCHEMA_BASE = vol.Schema( vol.Coerce(int), vol.Range(min=15) ), vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), @@ -203,9 +210,9 @@ CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) ), - vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. vol.Optional( @@ -608,6 +615,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _merge_config(entry, conf): """Merge configuration.yaml config with config entry.""" + # Base config on default values + conf = {**DEFAULT_VALUES, **conf} return {**conf, **entry.data} @@ -627,6 +636,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: override, ) + # Merge the configuration values from configuration.yaml conf = _merge_config(entry, conf) hass.data[DATA_MQTT] = MQTT( From ed6c86a28176624fc72d8a4d9b850c4d6755b5a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 13:55:06 +0100 Subject: [PATCH 0426/1054] Add siren platform to Switch as X (#68118) * Add siren platform to Switch as X * Clarify inline test documentation * Clarify inline test documentation --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/siren.py | 43 +++++++ tests/components/switch_as_x/test_init.py | 26 ++-- tests/components/switch_as_x/test_siren.py | 117 ++++++++++++++++++ 4 files changed, 180 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/switch_as_x/siren.py create mode 100644 tests/components/switch_as_x/test_siren.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index ed11ae25489..ff5c60274ef 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -24,6 +24,7 @@ CONFIG_FLOW = { "options": [ {"value": Platform.COVER, "label": "Cover"}, {"value": Platform.LIGHT, "label": "Light"}, + {"value": Platform.SIREN, "label": "Siren"}, ] } } diff --git a/homeassistant/components/switch_as_x/siren.py b/homeassistant/components/switch_as_x/siren.py new file mode 100644 index 00000000000..752f7fd76ad --- /dev/null +++ b/homeassistant/components/switch_as_x/siren.py @@ -0,0 +1,43 @@ +"""Siren support for switch entities.""" +from __future__ import annotations + +from homeassistant.components.siren import SirenEntity +from homeassistant.components.siren.const import SUPPORT_TURN_OFF, SUPPORT_TURN_ON +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseToggleEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Siren Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + SirenSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class SirenSwitch(BaseToggleEntity, SirenEntity): + """Represents a Switch as a Siren.""" + + _attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index ca87673dfba..a8145fe201a 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -11,7 +11,9 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize( + "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) +) async def test_config_entry_unregistered_uuid( hass: HomeAssistant, target_domain: str ) -> None: @@ -36,7 +38,7 @@ async def test_config_entry_unregistered_uuid( assert len(hass.states.async_all()) == 0 -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.SIREN)) async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) -> None: """Test entity registry events are tracked.""" registry = er.async_get(hass) @@ -93,7 +95,9 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - assert len(hass.config_entries.async_entries("switch_as_x")) == 0 -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize( + "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) +) async def test_device_registry_config_entry_1( hass: HomeAssistant, target_domain: str ) -> None: @@ -151,7 +155,9 @@ async def test_device_registry_config_entry_1( assert switch_as_x_config_entry.entry_id not in device_entry.config_entries -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize( + "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) +) async def test_device_registry_config_entry_2( hass: HomeAssistant, target_domain: str ) -> None: @@ -202,7 +208,9 @@ async def test_device_registry_config_entry_2( assert switch_as_x_config_entry.entry_id not in device_entry.config_entries -@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +@pytest.mark.parametrize( + "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) +) async def test_config_entry_entity_id( hass: HomeAssistant, target_domain: Platform ) -> None: @@ -237,7 +245,9 @@ async def test_config_entry_entity_id( assert entity_entry.unique_id == config_entry.entry_id -@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +@pytest.mark.parametrize( + "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) +) async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: """Test light switch setup from config entry with entity registry id.""" registry = er.async_get(hass) @@ -261,7 +271,9 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - assert hass.states.get(f"{target_domain}.abc") -@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +@pytest.mark.parametrize( + "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) +) async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: """Test the entity is added to the wrapped entity's device.""" device_registry = dr.async_get(hass) diff --git a/tests/components/switch_as_x/test_siren.py b/tests/components/switch_as_x/test_siren.py new file mode 100644 index 00000000000..2b3dedf6fb8 --- /dev/null +++ b/tests/components/switch_as_x/test_siren.py @@ -0,0 +1,117 @@ +"""Tests for the Switch as X Siren platform.""" +from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test siren switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.SIREN, + }, + title="Noise Maker", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("siren.noise_maker") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 3 + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as siren entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.SIREN, + }, + title="noise_maker", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("siren.noise_maker").state == STATE_ON + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "siren.noise_maker"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("siren.noise_maker").state == STATE_OFF + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "siren.noise_maker"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("siren.noise_maker").state == STATE_ON + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "siren.noise_maker"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("siren.noise_maker").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("siren.noise_maker").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("siren.noise_maker").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("siren.noise_maker").state == STATE_ON From 344537d0a89dc7258700badec0a9e2bab5a8f684 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 15:07:21 +0100 Subject: [PATCH 0427/1054] Add Fan platform to Switch as X (#68122) --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/fan.py | 64 ++++++++++ tests/components/switch_as_x/test_fan.py | 117 ++++++++++++++++++ tests/components/switch_as_x/test_init.py | 57 +++++++-- 4 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/switch_as_x/fan.py create mode 100644 tests/components/switch_as_x/test_fan.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index ff5c60274ef..6247d55b1fa 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -23,6 +23,7 @@ CONFIG_FLOW = { "select": { "options": [ {"value": Platform.COVER, "label": "Cover"}, + {"value": Platform.FAN, "label": "Fan"}, {"value": Platform.LIGHT, "label": "Light"}, {"value": Platform.SIREN, "label": "Siren"}, ] diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py new file mode 100644 index 00000000000..546e22b3fc1 --- /dev/null +++ b/homeassistant/components/switch_as_x/fan.py @@ -0,0 +1,64 @@ +"""Fan support for switch entities.""" +from __future__ import annotations + +from homeassistant.components.fan import FanEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseToggleEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Fan Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + FanSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class FanSwitch(BaseToggleEntity, FanEntity): + """Represents a Switch as a Fan.""" + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on. + + Fan logic uses speed percentage or preset mode to determine + its it on or off, however, when using a wrapped switch, we + just use the wrapped switch's state. + """ + return self._attr_is_on + + # pylint: disable=arguments-differ + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs, + ) -> None: + """Turn on the fan. + + Arguments of the turn_on methods fan entity differ, + thus we need to override them here. + """ + await super().async_turn_on() diff --git a/tests/components/switch_as_x/test_fan.py b/tests/components/switch_as_x/test_fan.py new file mode 100644 index 00000000000..b7b746344b3 --- /dev/null +++ b/tests/components/switch_as_x/test_fan.py @@ -0,0 +1,117 @@ +"""Tests for the Switch as X Fan platform.""" +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test fan switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.FAN, + }, + title="Wind Machine", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("fan.wind_machine") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 0 + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as fan entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.FAN, + }, + title="wind_machine", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("fan.wind_machine").state == STATE_ON + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "fan.wind_machine"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.wind_machine").state == STATE_OFF + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "fan.wind_machine"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("fan.wind_machine").state == STATE_ON + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "fan.wind_machine"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.wind_machine").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("fan.wind_machine").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.wind_machine").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("fan.wind_machine").state == STATE_ON diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index a8145fe201a..7a7002de094 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -12,7 +12,13 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_config_entry_unregistered_uuid( hass: HomeAssistant, target_domain: str @@ -38,7 +44,14 @@ async def test_config_entry_unregistered_uuid( assert len(hass.states.async_all()) == 0 -@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.SIREN)) +@pytest.mark.parametrize( + "target_domain", + ( + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), +) async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) -> None: """Test entity registry events are tracked.""" registry = er.async_get(hass) @@ -96,7 +109,13 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_device_registry_config_entry_1( hass: HomeAssistant, target_domain: str @@ -156,7 +175,13 @@ async def test_device_registry_config_entry_1( @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_device_registry_config_entry_2( hass: HomeAssistant, target_domain: str @@ -209,7 +234,13 @@ async def test_device_registry_config_entry_2( @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_config_entry_entity_id( hass: HomeAssistant, target_domain: Platform @@ -246,7 +277,13 @@ async def test_config_entry_entity_id( @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: """Test light switch setup from config entry with entity registry id.""" @@ -272,7 +309,13 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: """Test the entity is added to the wrapped entity's device.""" From 57c33a5cf0cd52d55db30cbddc8ddc59523ff94e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 17:33:04 +0100 Subject: [PATCH 0428/1054] Remove deprecated OpenZWave integration (#68054) --- .coveragerc | 3 - CODEOWNERS | 2 - homeassistant/components/ozw/__init__.py | 418 ----------- homeassistant/components/ozw/binary_sensor.py | 396 ---------- homeassistant/components/ozw/climate.py | 379 ---------- homeassistant/components/ozw/config_flow.py | 228 ------ homeassistant/components/ozw/const.py | 71 -- homeassistant/components/ozw/cover.py | 127 ---- homeassistant/components/ozw/discovery.py | 356 --------- homeassistant/components/ozw/entity.py | 304 -------- homeassistant/components/ozw/fan.py | 88 --- homeassistant/components/ozw/light.py | 343 --------- homeassistant/components/ozw/lock.py | 105 --- homeassistant/components/ozw/manifest.json | 11 - homeassistant/components/ozw/sensor.py | 164 ----- homeassistant/components/ozw/services.py | 134 ---- homeassistant/components/ozw/services.yaml | 121 ---- homeassistant/components/ozw/strings.json | 39 - homeassistant/components/ozw/switch.py | 47 -- .../components/ozw/translations/bg.json | 8 - .../components/ozw/translations/ca.json | 41 -- .../components/ozw/translations/cs.json | 38 - .../components/ozw/translations/de.json | 41 -- .../components/ozw/translations/el.json | 41 -- .../components/ozw/translations/en.json | 41 -- .../components/ozw/translations/es.json | 41 -- .../components/ozw/translations/et.json | 41 -- .../components/ozw/translations/fr.json | 41 -- .../components/ozw/translations/he.json | 24 - .../components/ozw/translations/hu.json | 41 -- .../components/ozw/translations/id.json | 41 -- .../components/ozw/translations/it.json | 41 -- .../components/ozw/translations/ja.json | 41 -- .../components/ozw/translations/ka.json | 29 - .../components/ozw/translations/ko.json | 41 -- .../components/ozw/translations/lb.json | 17 - .../components/ozw/translations/nl.json | 41 -- .../components/ozw/translations/no.json | 41 -- .../components/ozw/translations/pl.json | 41 -- .../components/ozw/translations/pt-BR.json | 41 -- .../components/ozw/translations/pt.json | 16 - .../components/ozw/translations/ru.json | 41 -- .../components/ozw/translations/sk.json | 7 - .../components/ozw/translations/sl.json | 23 - .../components/ozw/translations/tr.json | 41 -- .../components/ozw/translations/uk.json | 41 -- .../components/ozw/translations/zh-Hans.json | 7 - .../components/ozw/translations/zh-Hant.json | 41 -- homeassistant/components/ozw/websocket_api.py | 493 ------------- homeassistant/generated/config_flows.py | 1 - mypy.ini | 9 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - script/hassfest/mypy_config.py | 3 - tests/components/ozw/__init__.py | 1 - tests/components/ozw/common.py | 62 -- tests/components/ozw/conftest.py | 280 ------- .../ozw/fixtures/binary_sensor.json | 38 - .../ozw/fixtures/binary_sensor_alt.json | 25 - tests/components/ozw/fixtures/climate.json | 54 -- .../ozw/fixtures/climate_network_dump.csv | 208 ------ tests/components/ozw/fixtures/cover.json | 25 - tests/components/ozw/fixtures/cover_gdo.json | 54 -- .../ozw/fixtures/cover_gdo_network_dump.csv | 45 -- .../ozw/fixtures/cover_network_dump.csv | 134 ---- tests/components/ozw/fixtures/fan.json | 25 - .../ozw/fixtures/fan_network_dump.csv | 51 -- .../ozw/fixtures/generic_network_dump.csv | 284 -------- tests/components/ozw/fixtures/light.json | 25 - .../ozw/fixtures/light_network_dump.csv | 320 -------- .../fixtures/light_new_ozw_network_dump.csv | 55 -- .../ozw/fixtures/light_no_cw_network_dump.csv | 54 -- .../components/ozw/fixtures/light_no_rgb.json | 25 - .../ozw/fixtures/light_no_ww_network_dump.csv | 54 -- .../ozw/fixtures/light_pure_rgb.json | 25 - tests/components/ozw/fixtures/light_rgb.json | 25 - .../ozw/fixtures/light_wc_network_dump.csv | 54 -- tests/components/ozw/fixtures/lock.json | 25 - .../ozw/fixtures/lock_network_dump.csv | 79 -- .../ozw/fixtures/migration_fixture.csv | 9 - tests/components/ozw/fixtures/sensor.json | 38 - .../sensor_string_value_network_dump.csv | 5 - tests/components/ozw/fixtures/switch.json | 25 - tests/components/ozw/test_binary_sensor.py | 67 -- tests/components/ozw/test_climate.py | 327 --------- tests/components/ozw/test_config_flow.py | 502 ------------- tests/components/ozw/test_cover.py | 177 ----- tests/components/ozw/test_fan.py | 120 --- tests/components/ozw/test_init.py | 238 ------ tests/components/ozw/test_light.py | 683 ------------------ tests/components/ozw/test_lock.py | 83 --- tests/components/ozw/test_scenes.py | 89 --- tests/components/ozw/test_sensor.py | 93 --- tests/components/ozw/test_services.py | 115 --- tests/components/ozw/test_switch.py | 41 -- tests/components/ozw/test_websocket_api.py | 387 ---------- 96 files changed, 9863 deletions(-) delete mode 100644 homeassistant/components/ozw/__init__.py delete mode 100644 homeassistant/components/ozw/binary_sensor.py delete mode 100644 homeassistant/components/ozw/climate.py delete mode 100644 homeassistant/components/ozw/config_flow.py delete mode 100644 homeassistant/components/ozw/const.py delete mode 100644 homeassistant/components/ozw/cover.py delete mode 100644 homeassistant/components/ozw/discovery.py delete mode 100644 homeassistant/components/ozw/entity.py delete mode 100644 homeassistant/components/ozw/fan.py delete mode 100644 homeassistant/components/ozw/light.py delete mode 100644 homeassistant/components/ozw/lock.py delete mode 100644 homeassistant/components/ozw/manifest.json delete mode 100644 homeassistant/components/ozw/sensor.py delete mode 100644 homeassistant/components/ozw/services.py delete mode 100644 homeassistant/components/ozw/services.yaml delete mode 100644 homeassistant/components/ozw/strings.json delete mode 100644 homeassistant/components/ozw/switch.py delete mode 100644 homeassistant/components/ozw/translations/bg.json delete mode 100644 homeassistant/components/ozw/translations/ca.json delete mode 100644 homeassistant/components/ozw/translations/cs.json delete mode 100644 homeassistant/components/ozw/translations/de.json delete mode 100644 homeassistant/components/ozw/translations/el.json delete mode 100644 homeassistant/components/ozw/translations/en.json delete mode 100644 homeassistant/components/ozw/translations/es.json delete mode 100644 homeassistant/components/ozw/translations/et.json delete mode 100644 homeassistant/components/ozw/translations/fr.json delete mode 100644 homeassistant/components/ozw/translations/he.json delete mode 100644 homeassistant/components/ozw/translations/hu.json delete mode 100644 homeassistant/components/ozw/translations/id.json delete mode 100644 homeassistant/components/ozw/translations/it.json delete mode 100644 homeassistant/components/ozw/translations/ja.json delete mode 100644 homeassistant/components/ozw/translations/ka.json delete mode 100644 homeassistant/components/ozw/translations/ko.json delete mode 100644 homeassistant/components/ozw/translations/lb.json delete mode 100644 homeassistant/components/ozw/translations/nl.json delete mode 100644 homeassistant/components/ozw/translations/no.json delete mode 100644 homeassistant/components/ozw/translations/pl.json delete mode 100644 homeassistant/components/ozw/translations/pt-BR.json delete mode 100644 homeassistant/components/ozw/translations/pt.json delete mode 100644 homeassistant/components/ozw/translations/ru.json delete mode 100644 homeassistant/components/ozw/translations/sk.json delete mode 100644 homeassistant/components/ozw/translations/sl.json delete mode 100644 homeassistant/components/ozw/translations/tr.json delete mode 100644 homeassistant/components/ozw/translations/uk.json delete mode 100644 homeassistant/components/ozw/translations/zh-Hans.json delete mode 100644 homeassistant/components/ozw/translations/zh-Hant.json delete mode 100644 homeassistant/components/ozw/websocket_api.py delete mode 100644 tests/components/ozw/__init__.py delete mode 100644 tests/components/ozw/common.py delete mode 100644 tests/components/ozw/conftest.py delete mode 100644 tests/components/ozw/fixtures/binary_sensor.json delete mode 100644 tests/components/ozw/fixtures/binary_sensor_alt.json delete mode 100644 tests/components/ozw/fixtures/climate.json delete mode 100644 tests/components/ozw/fixtures/climate_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/cover.json delete mode 100644 tests/components/ozw/fixtures/cover_gdo.json delete mode 100644 tests/components/ozw/fixtures/cover_gdo_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/cover_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/fan.json delete mode 100644 tests/components/ozw/fixtures/fan_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/generic_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/light.json delete mode 100644 tests/components/ozw/fixtures/light_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/light_new_ozw_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/light_no_cw_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/light_no_rgb.json delete mode 100644 tests/components/ozw/fixtures/light_no_ww_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/light_pure_rgb.json delete mode 100644 tests/components/ozw/fixtures/light_rgb.json delete mode 100644 tests/components/ozw/fixtures/light_wc_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/lock.json delete mode 100644 tests/components/ozw/fixtures/lock_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/migration_fixture.csv delete mode 100644 tests/components/ozw/fixtures/sensor.json delete mode 100644 tests/components/ozw/fixtures/sensor_string_value_network_dump.csv delete mode 100644 tests/components/ozw/fixtures/switch.json delete mode 100644 tests/components/ozw/test_binary_sensor.py delete mode 100644 tests/components/ozw/test_climate.py delete mode 100644 tests/components/ozw/test_config_flow.py delete mode 100644 tests/components/ozw/test_cover.py delete mode 100644 tests/components/ozw/test_fan.py delete mode 100644 tests/components/ozw/test_init.py delete mode 100644 tests/components/ozw/test_light.py delete mode 100644 tests/components/ozw/test_lock.py delete mode 100644 tests/components/ozw/test_scenes.py delete mode 100644 tests/components/ozw/test_sensor.py delete mode 100644 tests/components/ozw/test_services.py delete mode 100644 tests/components/ozw/test_switch.py delete mode 100644 tests/components/ozw/test_websocket_api.py diff --git a/.coveragerc b/.coveragerc index 778983e55fa..ed5aff10dd6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -872,9 +872,6 @@ omit = homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py - homeassistant/components/ozw/__init__.py - homeassistant/components/ozw/entity.py - homeassistant/components/ozw/services.py homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 668a6bae19e..eee2d323086 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -735,8 +735,6 @@ homeassistant/components/overkiz/* @imicknl @vlebourl @tetienne tests/components/overkiz/* @imicknl @vlebourl @tetienne homeassistant/components/ovo_energy/* @timmo001 tests/components/ovo_energy/* @timmo001 -homeassistant/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare -tests/components/ozw/* @cgarwood @marcelveldt @MartinHjelmare homeassistant/components/p1_monitor/* @klaasnicolaas tests/components/p1_monitor/* @klaasnicolaas homeassistant/components/panel_custom/* @home-assistant/frontend diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py deleted file mode 100644 index 6aa3bbbcf1c..00000000000 --- a/homeassistant/components/ozw/__init__.py +++ /dev/null @@ -1,418 +0,0 @@ -"""The ozw integration.""" -import asyncio -from contextlib import suppress -import json -import logging - -from openzwavemqtt import OZWManager, OZWOptions -from openzwavemqtt.const import ( - EVENT_INSTANCE_EVENT, - EVENT_NODE_ADDED, - EVENT_NODE_CHANGED, - EVENT_NODE_REMOVED, - EVENT_VALUE_ADDED, - EVENT_VALUE_CHANGED, - EVENT_VALUE_REMOVED, - CommandClass, - ValueType, -) -from openzwavemqtt.models.node import OZWNode -from openzwavemqtt.models.value import OZWValue -from openzwavemqtt.util.mqtt_client import MQTTClient - -from homeassistant.components import hassio, mqtt -from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.dispatcher import async_dispatcher_send - -from . import const -from .const import ( - CONF_INTEGRATION_CREATED_ADDON, - CONF_USE_ADDON, - DATA_UNSUBSCRIBE, - DOMAIN, - MANAGER, - NODES_VALUES, - PLATFORMS, - TOPIC_OPENZWAVE, -) -from .discovery import DISCOVERY_SCHEMAS, check_node_schema, check_value_schema -from .entity import ( - ZWaveDeviceEntityValues, - create_device_id, - create_device_name, - create_value_id, -) -from .services import ZWaveServices -from .websocket_api import async_register_api - -_LOGGER = logging.getLogger(__name__) - -DATA_DEVICES = "zwave-mqtt-devices" -DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" - - -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: - """Set up ozw from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - ozw_data = hass.data[DOMAIN][entry.entry_id] = {} - ozw_data[DATA_UNSUBSCRIBE] = [] - - data_nodes = {} - hass.data[DOMAIN][NODES_VALUES] = data_values = {} - removed_nodes = [] - manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"} - - if entry.unique_id is None: - hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) - - if entry.data.get(CONF_USE_ADDON): - # Do not use MQTT integration. Use own MQTT client. - # Retrieve discovery info from the OpenZWave add-on. - discovery_info = await hassio.async_get_addon_discovery_info(hass, "core_zwave") - - if not discovery_info: - _LOGGER.error("Failed to get add-on discovery info") - raise ConfigEntryNotReady - - discovery_info_config = discovery_info["config"] - - host = discovery_info_config["host"] - port = discovery_info_config["port"] - username = discovery_info_config["username"] - password = discovery_info_config["password"] - mqtt_client = MQTTClient(host, port, username=username, password=password) - manager_options["send_message"] = mqtt_client.send_message - - else: - mqtt_entries = hass.config_entries.async_entries("mqtt") - if not mqtt_entries or mqtt_entries[0].state is not ConfigEntryState.LOADED: - _LOGGER.error("MQTT integration is not set up") - return False - - mqtt_entry = mqtt_entries[0] # MQTT integration only has one entry. - - @callback - def send_message(topic, payload): - if mqtt_entry.state is not ConfigEntryState.LOADED: - _LOGGER.error("MQTT integration is not set up") - return - - hass.async_create_task(mqtt.async_publish(hass, topic, json.dumps(payload))) - - manager_options["send_message"] = send_message - - options = OZWOptions(**manager_options) - manager = OZWManager(options) - - hass.data[DOMAIN][MANAGER] = manager - - @callback - def async_node_added(node): - # Caution: This is also called on (re)start. - _LOGGER.debug("[NODE ADDED] node_id: %s", node.id) - data_nodes[node.id] = node - if node.id not in data_values: - data_values[node.id] = [] - - @callback - def async_node_changed(node): - _LOGGER.debug("[NODE CHANGED] node_id: %s", node.id) - data_nodes[node.id] = node - # notify devices about the node change - if node.id not in removed_nodes: - hass.async_create_task(async_handle_node_update(hass, node)) - - @callback - def async_node_removed(node): - _LOGGER.debug("[NODE REMOVED] node_id: %s", node.id) - data_nodes.pop(node.id) - # node added/removed events also happen on (re)starts of hass/mqtt/ozw - # cleanup device/entity registry if we know this node is permanently deleted - # entities itself are removed by the values logic - if node.id in removed_nodes: - hass.async_create_task(async_handle_remove_node(hass, node)) - removed_nodes.remove(node.id) - - @callback - def async_instance_event(message): - event = message["event"] - event_data = message["data"] - _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data) - # The actual removal action of a Z-Wave node is reported as instance event - # Only when this event is detected we cleanup the device and entities from hass - # Note: Find a more elegant way of doing this, e.g. a notification of this event from OZW - if event in ("removenode", "removefailednode") and "Node" in event_data: - removed_nodes.append(event_data["Node"]) - - @callback - def async_value_added(value): - node = value.node - # Clean up node.node_id and node.id use. They are the same. - node_id = value.node.node_id - - # Filter out CommandClasses we're definitely not interested in. - if value.command_class in (CommandClass.MANUFACTURER_SPECIFIC,): - return - - _LOGGER.debug( - "[VALUE ADDED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", - value.node.id, - value.label, - value.value, - value.value_id_key, - value.command_class, - ) - - node_data_values = data_values[node_id] - - # Check if this value should be tracked by an existing entity - value_unique_id = create_value_id(value) - for values in node_data_values: - values.async_check_value(value) - if values.values_id == value_unique_id: - return # this value already has an entity - - # Run discovery on it and see if any entities need created - for schema in DISCOVERY_SCHEMAS: - if not check_node_schema(node, schema): - continue - if not check_value_schema( - value, schema[const.DISC_VALUES][const.DISC_PRIMARY] - ): - continue - - values = ZWaveDeviceEntityValues(hass, options, schema, value) - values.async_setup() - - # This is legacy and can be cleaned up since we are in the main thread: - # We create a new list and update the reference here so that - # the list can be safely iterated over in the main thread - data_values[node_id] = node_data_values + [values] - - @callback - def async_value_changed(value): - # if an entity belonging to this value needs updating, - # it's handled within the entity logic - _LOGGER.debug( - "[VALUE CHANGED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", - value.node.id, - value.label, - value.value, - value.value_id_key, - value.command_class, - ) - # Handle a scene activation message - if value.command_class in ( - CommandClass.SCENE_ACTIVATION, - CommandClass.CENTRAL_SCENE, - ): - async_handle_scene_activated(hass, value) - return - - @callback - def async_value_removed(value): - _LOGGER.debug( - "[VALUE REMOVED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", - value.node.id, - value.label, - value.value, - value.value_id_key, - value.command_class, - ) - # signal all entities using this value for removal - value_unique_id = create_value_id(value) - async_dispatcher_send(hass, const.SIGNAL_DELETE_ENTITY, value_unique_id) - # remove value from our local list - node_data_values = data_values[value.node.id] - node_data_values[:] = [ - item for item in node_data_values if item.values_id != value_unique_id - ] - - # Listen to events for node and value changes - for event, event_callback in ( - (EVENT_NODE_ADDED, async_node_added), - (EVENT_NODE_CHANGED, async_node_changed), - (EVENT_NODE_REMOVED, async_node_removed), - (EVENT_VALUE_ADDED, async_value_added), - (EVENT_VALUE_CHANGED, async_value_changed), - (EVENT_VALUE_REMOVED, async_value_removed), - (EVENT_INSTANCE_EVENT, async_instance_event), - ): - ozw_data[DATA_UNSUBSCRIBE].append(options.listen(event, event_callback)) - - # Register Services - services = ZWaveServices(hass, manager) - services.async_register() - - # Register WebSocket API - async_register_api(hass) - - @callback - def async_receive_message(msg): - manager.receive_message(msg.topic, msg.payload) - - async def start_platforms(): - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - if entry.data.get(CONF_USE_ADDON): - mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager)) - - async def async_stop_mqtt_client(event=None): - """Stop the mqtt client. - - Do not unsubscribe the manager topic. - """ - mqtt_client_task.cancel() - with suppress(asyncio.CancelledError): - await mqtt_client_task - - ozw_data[DATA_UNSUBSCRIBE].append( - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client - ) - ) - ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client - - else: - ozw_data[DATA_UNSUBSCRIBE].append( - await mqtt.async_subscribe( - hass, f"{manager.options.topic_prefix}#", async_receive_message - ) - ) - - hass.async_create_task(start_platforms()) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - # cleanup platforms - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if not unload_ok: - return False - - # unsubscribe all listeners - for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: - unsubscribe_listener() - - if entry.data.get(CONF_USE_ADDON): - async_stop_mqtt_client = hass.data[DOMAIN][entry.entry_id][ - DATA_STOP_MQTT_CLIENT - ] - await async_stop_mqtt_client() - - hass.data[DOMAIN].pop(entry.entry_id) - - return True - - -async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Remove a config entry.""" - if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): - return - - try: - await hassio.async_stop_addon(hass, "core_zwave") - except HassioAPIError as err: - _LOGGER.error("Failed to stop the OpenZWave add-on: %s", err) - return - try: - await hassio.async_uninstall_addon(hass, "core_zwave") - except HassioAPIError as err: - _LOGGER.error("Failed to uninstall the OpenZWave add-on: %s", err) - - -async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): - """Handle the removal of a Z-Wave node, removing all traces in device/entity registry.""" - dev_registry = await get_dev_reg(hass) - # grab device in device registry attached to this node - dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}) - if not device: - return - devices_to_remove = [device.id] - # also grab slave devices (node instances) - for item in dev_registry.devices.values(): - if item.via_device_id == device.id: - devices_to_remove.append(item.id) - # remove all devices in registry related to this node - # note: removal of entity registry is handled by core - for dev_id in devices_to_remove: - dev_registry.async_remove_device(dev_id) - - -async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): - """ - Handle a node updated event from OZW. - - Meaning some of the basic info like name/model is updated. - We want these changes to be pushed to the device registry. - """ - dev_registry = await get_dev_reg(hass) - # grab device in device registry attached to this node - dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}) - if not device: - return - # update device in device registry with (updated) info - for item in dev_registry.devices.values(): - if device.id not in (item.id, item.via_device_id): - continue - dev_name = create_device_name(node) - dev_registry.async_update_device( - item.id, - manufacturer=node.node_manufacturer_name, - model=node.node_product_name, - name=dev_name, - ) - - -@callback -def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): - """Handle a (central) scene activation message.""" - node_id = scene_value.node.id - ozw_instance_id = scene_value.ozw_instance.id - scene_id = scene_value.index - scene_label = scene_value.label - if scene_value.command_class == CommandClass.SCENE_ACTIVATION: - # legacy/network scene - scene_value_id = scene_value.value - scene_value_label = scene_value.label - else: - # central scene command - if scene_value.type != ValueType.LIST: - return - scene_value_label = scene_value.value["Selected"] - scene_value_id = scene_value.value["Selected_id"] - - _LOGGER.debug( - "[SCENE_ACTIVATED] ozw_instance: %s - node_id: %s - scene_id: %s - scene_value_id: %s", - ozw_instance_id, - node_id, - scene_id, - scene_value_id, - ) - # Simply forward it to the hass event bus - hass.bus.async_fire( - const.EVENT_SCENE_ACTIVATED, - { - const.ATTR_INSTANCE_ID: ozw_instance_id, - const.ATTR_NODE_ID: node_id, - const.ATTR_SCENE_ID: scene_id, - const.ATTR_SCENE_LABEL: scene_label, - const.ATTR_SCENE_VALUE_ID: scene_value_id, - const.ATTR_SCENE_VALUE_LABEL: scene_value_label, - }, - ) diff --git a/homeassistant/components/ozw/binary_sensor.py b/homeassistant/components/ozw/binary_sensor.py deleted file mode 100644 index c234d30c5ca..00000000000 --- a/homeassistant/components/ozw/binary_sensor.py +++ /dev/null @@ -1,396 +0,0 @@ -"""Representation of Z-Wave binary_sensors.""" -from openzwavemqtt.const import CommandClass, ValueIndex, ValueType - -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDeviceClass, - BinarySensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -NOTIFICATION_TYPE = "index" -NOTIFICATION_VALUES = "values" -NOTIFICATION_DEVICE_CLASS = "device_class" -NOTIFICATION_SENSOR_ENABLED = "enabled" -NOTIFICATION_OFF_VALUE = "off_value" - -NOTIFICATION_VALUE_CLEAR = 0 - -# Translation from values in Notification CC to binary sensors -# https://github.com/OpenZWave/open-zwave/blob/master/config/NotificationCCTypes.xml -NOTIFICATION_SENSORS = [ - { - # Index 1: Smoke Alarm - Value Id's 1 and 2 - # Assuming here that Value 1 and 2 are not present at the same time - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE, - }, - { - # Index 1: Smoke Alarm - All other Value Id's - # Create as disabled sensors - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SMOKE_ALARM, - NOTIFICATION_VALUES: [3, 4, 5, 6, 7, 8], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 2: Carbon Monoxide - Value Id's 1 and 2 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - }, - { - # Index 2: Carbon Monoxide - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_MONOOXIDE, - NOTIFICATION_VALUES: [4, 5, 7], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 3: Carbon Dioxide - Value Id's 1 and 2 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - }, - { - # Index 3: Carbon Dioxide - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CARBON_DIOXIDE, - NOTIFICATION_VALUES: [4, 5, 7], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 4: Heat - Value Id's 1, 2, 5, 6 (heat/underheat) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, - NOTIFICATION_VALUES: [1, 2, 5, 6], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT, - }, - { - # Index 4: Heat - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HEAT, - NOTIFICATION_VALUES: [3, 4, 8, 10, 11], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.HEAT, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 5: Water - Value Id's 1, 2, 3, 4 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, - NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE, - }, - { - # Index 5: Water - All other Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER, - NOTIFICATION_VALUES: [5], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOISTURE, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 6: Access Control - Value Id's 1, 2, 3, 4 (Lock) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, - NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.LOCK, - }, - { - # Index 6: Access Control - Value Id 22 (door/window open) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_ACCESS_CONTROL, - NOTIFICATION_VALUES: [22], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.DOOR, - NOTIFICATION_OFF_VALUE: 23, - }, - { - # Index 7: Home Security - Value Id's 1, 2 (intrusion) - # Assuming that value 1 and 2 are not present at the same time - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, - }, - { - # Index 7: Home Security - Value Id's 3, 4, 9 (tampering) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [3, 4, 9], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, - }, - { - # Index 7: Home Security - Value Id's 5, 6 (glass breakage) - # Assuming that value 5 and 6 are not present at the same time - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [5, 6], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SAFETY, - }, - { - # Index 7: Home Security - Value Id's 7, 8 (motion) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_HOME_SECURITY, - NOTIFICATION_VALUES: [7, 8], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.MOTION, - }, - { - # Index 8: Power management - Values 1...9 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, - NOTIFICATION_VALUES: [1, 2, 3, 4, 5, 6, 7, 8, 9], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 8: Power management - Values 10...15 - # Battery values (mutually exclusive) - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_POWER_MANAGEMENT, - NOTIFICATION_VALUES: [10, 11, 12, 13, 14, 15], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.POWER, - NOTIFICATION_SENSOR_ENABLED: False, - NOTIFICATION_OFF_VALUE: None, - }, - { - # Index 9: System - Value Id's 1, 2, 6, 7 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SYSTEM, - NOTIFICATION_VALUES: [1, 2, 6, 7], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 10: Emergency - Value Id's 1, 2, 3 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_EMERGENCY, - NOTIFICATION_VALUES: [1, 2, 3], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, - { - # Index 11: Clock - Value Id's 1, 2 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_CLOCK, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: None, - NOTIFICATION_SENSOR_ENABLED: False, - }, - { - # Index 12: Appliance - All Value Id's - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE, - NOTIFICATION_VALUES: [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - ], - NOTIFICATION_DEVICE_CLASS: None, - }, - { - # Index 13: Home Health - Value Id's 1,2,3,4,5 - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_APPLIANCE, - NOTIFICATION_VALUES: [1, 2, 3, 4, 5], - NOTIFICATION_DEVICE_CLASS: None, - }, - { - # Index 14: Siren - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_SIREN, - NOTIFICATION_VALUES: [1], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.SOUND, - }, - { - # Index 15: Water valve - # ignore non-boolean values - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WATER_VALVE, - NOTIFICATION_VALUES: [3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, - { - # Index 16: Weather - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_WEATHER, - NOTIFICATION_VALUES: [1, 2], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, - { - # Index 17: Irrigation - # ignore non-boolean values - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_IRRIGATION, - NOTIFICATION_VALUES: [1, 2, 3, 4, 5], - NOTIFICATION_DEVICE_CLASS: None, - }, - { - # Index 18: Gas - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, - NOTIFICATION_VALUES: [1, 2, 3, 4], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.GAS, - }, - { - # Index 18: Gas - NOTIFICATION_TYPE: ValueIndex.NOTIFICATION_GAS, - NOTIFICATION_VALUES: [6], - NOTIFICATION_DEVICE_CLASS: BinarySensorDeviceClass.PROBLEM, - }, -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave binary_sensor from config entry.""" - - @callback - def async_add_binary_sensor(values): - """Add Z-Wave Binary Sensor(s).""" - async_add_entities(VALUE_TYPE_SENSORS[values.primary.type](values)) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{BINARY_SENSOR_DOMAIN}", async_add_binary_sensor - ) - ) - - -@callback -def async_get_legacy_binary_sensors(values): - """Add Legacy/classic Z-Wave Binary Sensor.""" - return [ZWaveBinarySensor(values)] - - -@callback -def async_get_notification_sensors(values): - """Convert Notification values into binary sensors.""" - sensors_to_add = [] - for list_value in values.primary.value["List"]: - # check if we have a mapping for this value - for item in NOTIFICATION_SENSORS: - if item[NOTIFICATION_TYPE] != values.primary.index: - continue - if list_value["Value"] not in item[NOTIFICATION_VALUES]: - continue - sensors_to_add.append( - ZWaveListValueSensor( - # required values - values, - list_value["Value"], - item[NOTIFICATION_DEVICE_CLASS], - # optional values - item.get(NOTIFICATION_SENSOR_ENABLED, True), - item.get(NOTIFICATION_OFF_VALUE, NOTIFICATION_VALUE_CLEAR), - ) - ) - return sensors_to_add - - -VALUE_TYPE_SENSORS = { - ValueType.BOOL: async_get_legacy_binary_sensors, - ValueType.LIST: async_get_notification_sensors, -} - - -class ZWaveBinarySensor(ZWaveDeviceEntity, BinarySensorEntity): - """Representation of a Z-Wave binary_sensor.""" - - @property - def is_on(self): - """Return if the sensor is on or off.""" - return self.values.primary.value - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # Legacy binary sensors are phased out (replaced by notification sensors) - # Disable by default to not confuse users - for item in self.values.primary.node.values(): - if item.command_class == CommandClass.NOTIFICATION: - # This device properly implements the Notification CC, legacy sensor can be disabled - return False - return True - - -class ZWaveListValueSensor(ZWaveDeviceEntity, BinarySensorEntity): - """Representation of a binary_sensor from values in the Z-Wave Notification CommandClass.""" - - def __init__( - self, - values, - on_value, - device_class=None, - default_enabled=True, - off_value=NOTIFICATION_VALUE_CLEAR, - ): - """Initialize a ZWaveListValueSensor entity.""" - super().__init__(values) - self._on_value = on_value - self._device_class = device_class - self._default_enabled = default_enabled - self._off_value = off_value - # make sure the correct value is selected at startup - self._state = False - self.on_value_update() - - @callback - def on_value_update(self): - """Call when a value is added/updated in the underlying EntityValues Collection.""" - if self.values.primary.value["Selected_id"] == self._on_value: - # Only when the active ID exactly matches our watched ON value, set sensor state to ON - self._state = True - elif self.values.primary.value["Selected_id"] == self._off_value: - # Only when the active ID exactly matches our watched OFF value, set sensor state to OFF - self._state = False - elif ( - self._off_value is None - and self.values.primary.value["Selected_id"] != self._on_value - ): - # Off value not explicitly specified - # Some values are reset by the simple fact they're overruled by another value coming in - # For example the battery charging values in Power Management Index - self._state = False - - @property - def name(self): - """Return the name of the entity.""" - # Append value label to base name - base_name = super().name - value_label = "" - for item in self.values.primary.value["List"]: - if item["Value"] == self._on_value: - value_label = item["Label"] - break - # Strip "on location" / "at location" from name - # Note: We're assuming that we don't retrieve 2 values with different location - value_label = value_label.split(" on ")[0] - value_label = value_label.split(" at ")[0] - return f"{base_name}: {value_label}" - - @property - def unique_id(self): - """Return the unique_id of the entity.""" - unique_id = super().unique_id - return f"{unique_id}.{self._on_value}" - - @property - def is_on(self): - """Return if the sensor is on or off.""" - return self._state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # We hide the more advanced sensors by default to not overwhelm users - return self._default_enabled diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py deleted file mode 100644 index 334f851d6b0..00000000000 --- a/homeassistant/components/ozw/climate.py +++ /dev/null @@ -1,379 +0,0 @@ -"""Support for Z-Wave climate devices.""" -from __future__ import annotations - -from enum import IntEnum -import logging - -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ( - ATTR_HVAC_MODE, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_FAN, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - PRESET_NONE, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -VALUE_LIST = "List" -VALUE_ID = "Value" -VALUE_LABEL = "Label" -VALUE_SELECTED_ID = "Selected_id" -VALUE_SELECTED_LABEL = "Selected" - -ATTR_FAN_ACTION = "fan_action" -ATTR_VALVE_POSITION = "valve_position" -_LOGGER = logging.getLogger(__name__) - - -class ThermostatMode(IntEnum): - """Enum with all (known/used) Z-Wave ThermostatModes.""" - - # https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatMode.cpp - OFF = 0 - HEAT = 1 - COOL = 2 - AUTO = 3 - AUXILIARY = 4 - RESUME_ON = 5 - FAN = 6 - FURNANCE = 7 - DRY = 8 - MOIST = 9 - AUTO_CHANGE_OVER = 10 - HEATING_ECON = 11 - COOLING_ECON = 12 - AWAY = 13 - FULL_POWER = 15 - MANUFACTURER_SPECIFIC = 31 - - -# In Z-Wave the modes and presets are both in ThermostatMode. -# This list contains thermostatmodes we should consider a mode only -MODES_LIST = [ - ThermostatMode.OFF, - ThermostatMode.HEAT, - ThermostatMode.COOL, - ThermostatMode.AUTO, - ThermostatMode.AUTO_CHANGE_OVER, -] - -MODE_SETPOINT_MAPPINGS = { - ThermostatMode.OFF: (), - ThermostatMode.HEAT: ("setpoint_heating",), - ThermostatMode.COOL: ("setpoint_cooling",), - ThermostatMode.AUTO: ("setpoint_heating", "setpoint_cooling"), - ThermostatMode.AUXILIARY: ("setpoint_heating",), - ThermostatMode.FURNANCE: ("setpoint_furnace",), - ThermostatMode.DRY: ("setpoint_dry_air",), - ThermostatMode.MOIST: ("setpoint_moist_air",), - ThermostatMode.AUTO_CHANGE_OVER: ("setpoint_auto_changeover",), - ThermostatMode.HEATING_ECON: ("setpoint_eco_heating",), - ThermostatMode.COOLING_ECON: ("setpoint_eco_cooling",), - ThermostatMode.AWAY: ("setpoint_away_heating", "setpoint_away_cooling"), - ThermostatMode.FULL_POWER: ("setpoint_full_power",), -} - - -# strings, OZW and/or qt-ozw does not send numeric values -# https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatOperatingState.cpp -HVAC_CURRENT_MAPPINGS = { - "idle": CURRENT_HVAC_IDLE, - "heat": CURRENT_HVAC_HEAT, - "pending heat": CURRENT_HVAC_IDLE, - "heating": CURRENT_HVAC_HEAT, - "cool": CURRENT_HVAC_COOL, - "pending cool": CURRENT_HVAC_IDLE, - "cooling": CURRENT_HVAC_COOL, - "fan only": CURRENT_HVAC_FAN, - "vent / economiser": CURRENT_HVAC_FAN, - "off": CURRENT_HVAC_OFF, -} - - -# Map Z-Wave HVAC Mode to Home Assistant value -# Note: We treat "auto" as "heat_cool" as most Z-Wave devices -# report auto_changeover as auto without schedule support. -ZW_HVAC_MODE_MAPPINGS = { - ThermostatMode.OFF: HVAC_MODE_OFF, - ThermostatMode.HEAT: HVAC_MODE_HEAT, - ThermostatMode.COOL: HVAC_MODE_COOL, - # Z-Wave auto mode is actually heat/cool in the hass world - ThermostatMode.AUTO: HVAC_MODE_HEAT_COOL, - ThermostatMode.AUXILIARY: HVAC_MODE_HEAT, - ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, - ThermostatMode.FURNANCE: HVAC_MODE_HEAT, - ThermostatMode.DRY: HVAC_MODE_DRY, - ThermostatMode.AUTO_CHANGE_OVER: HVAC_MODE_HEAT_COOL, - ThermostatMode.HEATING_ECON: HVAC_MODE_HEAT, - ThermostatMode.COOLING_ECON: HVAC_MODE_COOL, - ThermostatMode.AWAY: HVAC_MODE_HEAT_COOL, - ThermostatMode.FULL_POWER: HVAC_MODE_HEAT, -} - -# Map Home Assistant HVAC Mode to Z-Wave value -HVAC_MODE_ZW_MAPPINGS = { - HVAC_MODE_OFF: ThermostatMode.OFF, - HVAC_MODE_HEAT: ThermostatMode.HEAT, - HVAC_MODE_COOL: ThermostatMode.COOL, - HVAC_MODE_FAN_ONLY: ThermostatMode.FAN, - HVAC_MODE_DRY: ThermostatMode.DRY, - HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER, -} - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Climate from Config Entry.""" - - @callback - def async_add_climate(values): - """Add Z-Wave Climate.""" - async_add_entities([ZWaveClimateEntity(values)]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{CLIMATE_DOMAIN}", async_add_climate - ) - ) - - -class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): - """Representation of a Z-Wave Climate device.""" - - def __init__(self, values): - """Initialize the entity.""" - super().__init__(values) - self._hvac_modes = {} - self._hvac_presets = {} - self.on_value_update() - - @callback - def on_value_update(self): - """Call when the underlying values object changes.""" - self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() - if not self._hvac_modes: - self._set_modes_and_presets() - - @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode.""" - if not self.values.mode: - # Thermostat(valve) with no support for setting a mode is considered heating-only - return HVAC_MODE_HEAT - return ZW_HVAC_MODE_MAPPINGS.get( - self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_HEAT_COOL - ) - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes.""" - return list(self._hvac_modes) - - @property - def fan_mode(self): - """Return the fan speed set.""" - return self.values.fan_mode.value[VALUE_SELECTED_LABEL] - - @property - def fan_modes(self): - """Return a list of available fan modes.""" - return [entry[VALUE_LABEL] for entry in self.values.fan_mode.value[VALUE_LIST]] - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - if self.values.temperature is not None and self.values.temperature.units == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if not self.values.temperature: - return None - return self.values.temperature.value - - @property - def hvac_action(self): - """Return the current running hvac operation if supported.""" - if not self.values.operating_state: - return None - cur_state = self.values.operating_state.value.lower() - return HVAC_CURRENT_MAPPINGS.get(cur_state) - - @property - def preset_mode(self): - """Return preset operation ie. eco, away.""" - # A Zwave mode that can not be translated to a hass mode is considered a preset - if not self.values.mode: - return None - if self.values.mode.value[VALUE_SELECTED_ID] not in MODES_LIST: - return self.values.mode.value[VALUE_SELECTED_LABEL] - return PRESET_NONE - - @property - def preset_modes(self): - """Return the list of available preset operation modes.""" - return list(self._hvac_presets) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value - - @property - def target_temperature_low(self) -> float | None: - """Return the lowbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[0].value - - @property - def target_temperature_high(self) -> float | None: - """Return the highbound target temperature we try to reach.""" - return self._current_mode_setpoint_values[1].value - - async def async_set_temperature(self, **kwargs): - """Set new target temperature. - - Must know if single or double setpoint. - """ - if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: - await self.async_set_hvac_mode(hvac_mode) - - if len(self._current_mode_setpoint_values) == 1: - setpoint = self._current_mode_setpoint_values[0] - target_temp = kwargs.get(ATTR_TEMPERATURE) - if setpoint is not None and target_temp is not None: - setpoint.send_value(target_temp) - elif len(self._current_mode_setpoint_values) == 2: - (setpoint_low, setpoint_high) = self._current_mode_setpoint_values - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if setpoint_low is not None and target_temp_low is not None: - setpoint_low.send_value(target_temp_low) - if setpoint_high is not None and target_temp_high is not None: - setpoint_high.send_value(target_temp_high) - - async def async_set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - # get id for this fan_mode - fan_mode_value = _get_list_id(self.values.fan_mode.value[VALUE_LIST], fan_mode) - if fan_mode_value is None: - _LOGGER.warning("Received an invalid fan mode: %s", fan_mode) - return - self.values.fan_mode.send_value(fan_mode_value) - - async def async_set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - if not self.values.mode: - # Thermostat(valve) with no support for setting a mode - _LOGGER.warning( - "Thermostat %s does not support setting a mode", self.entity_id - ) - return - if (hvac_mode_value := self._hvac_modes.get(hvac_mode)) is None: - _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) - return - self.values.mode.send_value(hvac_mode_value) - - async def async_set_preset_mode(self, preset_mode): - """Set new target preset mode.""" - if preset_mode == PRESET_NONE: - # try to restore to the (translated) main hvac mode - await self.async_set_hvac_mode(self.hvac_mode) - return - preset_mode_value = self._hvac_presets.get(preset_mode) - if preset_mode_value is None: - _LOGGER.warning("Received an invalid preset mode: %s", preset_mode) - return - self.values.mode.send_value(preset_mode_value) - - @property - def extra_state_attributes(self): - """Return the optional state attributes.""" - data = super().extra_state_attributes - if self.values.fan_action: - data[ATTR_FAN_ACTION] = self.values.fan_action.value - if self.values.valve_position: - data[ - ATTR_VALVE_POSITION - ] = f"{self.values.valve_position.value} {self.values.valve_position.units}" - return data - - @property - def supported_features(self): - """Return the list of supported features.""" - support = 0 - if len(self._current_mode_setpoint_values) == 1: - support |= SUPPORT_TARGET_TEMPERATURE - if len(self._current_mode_setpoint_values) > 1: - support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self.values.fan_mode: - support |= SUPPORT_FAN_MODE - if self.values.mode: - support |= SUPPORT_PRESET_MODE - return support - - def _get_current_mode_setpoint_values(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - if not self.values.mode: - setpoint_names = ("setpoint_heating",) - else: - current_mode = self.values.mode.value[VALUE_SELECTED_ID] - setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - # we do not want None values in our tuple so check if the value exists - return tuple( - getattr(self.values, value_name) - for value_name in setpoint_names - if getattr(self.values, value_name, None) - ) - - def _set_modes_and_presets(self): - """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" - all_modes = {} - all_presets = {PRESET_NONE: None} - if self.values.mode: - # Z-Wave uses one list for both modes and presets. - # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. - for val in self.values.mode.value[VALUE_LIST]: - if val[VALUE_ID] in MODES_LIST: - # treat value as hvac mode - hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) - all_modes[hass_mode] = val[VALUE_ID] - else: - # treat value as hvac preset - all_presets[val[VALUE_LABEL]] = val[VALUE_ID] - else: - all_modes[HVAC_MODE_HEAT] = None - self._hvac_modes = all_modes - self._hvac_presets = all_presets - - -def _get_list_id(value_lst, value_lbl): - """Return the id for the value in the list.""" - return next( - (val[VALUE_ID] for val in value_lst if val[VALUE_LABEL] == value_lbl), None - ) diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py deleted file mode 100644 index 5e745a123f4..00000000000 --- a/homeassistant/components/ozw/config_flow.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Config flow for ozw integration.""" -import logging - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.components import hassio -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow, FlowResult - -from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -CONF_ADDON_DEVICE = "device" -CONF_ADDON_NETWORK_KEY = "network_key" -CONF_NETWORK_KEY = "network_key" -CONF_USB_PATH = "usb_path" -TITLE = "OpenZWave" - -ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool}) - - -class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for ozw.""" - - VERSION = 1 - - def __init__(self): - """Set up flow instance.""" - self.addon_config = None - self.network_key = None - self.usb_path = None - self.use_addon = False - # If we install the add-on we should uninstall it on entry remove. - self.integration_created_addon = False - self.install_task = None - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - # Set a unique_id to make sure discovery flow is aborted on progress. - await self.async_set_unique_id(DOMAIN, raise_on_progress=False) - - if not hassio.is_hassio(self.hass): - return self._async_use_mqtt_integration() - - return await self.async_step_on_supervisor() - - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: - """Receive configuration from add-on discovery info. - - This flow is triggered by the OpenZWave add-on. - """ - await self.async_set_unique_id(DOMAIN) - self._abort_if_unique_id_configured() - - return await self.async_step_hassio_confirm() - - async def async_step_hassio_confirm(self, user_input=None): - """Confirm the add-on discovery.""" - if user_input is not None: - return await self.async_step_on_supervisor( - user_input={CONF_USE_ADDON: True} - ) - - return self.async_show_form(step_id="hassio_confirm") - - def _async_create_entry_from_vars(self): - """Return a config entry for the flow.""" - return self.async_create_entry( - title=TITLE, - data={ - CONF_USB_PATH: self.usb_path, - CONF_NETWORK_KEY: self.network_key, - CONF_USE_ADDON: self.use_addon, - CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, - }, - ) - - @callback - def _async_use_mqtt_integration(self): - """Handle logic when using the MQTT integration. - - This is the entry point for the logic that is needed - when this integration will depend on the MQTT integration. - """ - mqtt_entries = self.hass.config_entries.async_entries("mqtt") - if ( - not mqtt_entries - or mqtt_entries[0].state is not config_entries.ConfigEntryState.LOADED - ): - return self.async_abort(reason="mqtt_required") - return self._async_create_entry_from_vars() - - async def async_step_on_supervisor(self, user_input=None): - """Handle logic when on Supervisor host.""" - if user_input is None: - return self.async_show_form( - step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA - ) - if not user_input[CONF_USE_ADDON]: - return self._async_create_entry_from_vars() - - self.use_addon = True - - if await self._async_is_addon_running(): - addon_config = await self._async_get_addon_config() - self.usb_path = addon_config[CONF_ADDON_DEVICE] - self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") - return self._async_create_entry_from_vars() - - if await self._async_is_addon_installed(): - return await self.async_step_start_addon() - - return await self.async_step_install_addon() - - async def async_step_install_addon(self, user_input=None): - """Install OpenZWave add-on.""" - if not self.install_task: - self.install_task = self.hass.async_create_task(self._async_install_addon()) - return self.async_show_progress( - step_id="install_addon", progress_action="install_addon" - ) - - try: - await self.install_task - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to install OpenZWave add-on: %s", err) - return self.async_show_progress_done(next_step_id="install_failed") - - self.integration_created_addon = True - - return self.async_show_progress_done(next_step_id="start_addon") - - async def async_step_install_failed(self, user_input=None): - """Add-on installation failed.""" - return self.async_abort(reason="addon_install_failed") - - async def async_step_start_addon(self, user_input=None): - """Ask for config and start OpenZWave add-on.""" - if self.addon_config is None: - self.addon_config = await self._async_get_addon_config() - - errors = {} - - if user_input is not None: - self.network_key = user_input[CONF_NETWORK_KEY] - self.usb_path = user_input[CONF_USB_PATH] - - new_addon_config = { - CONF_ADDON_DEVICE: self.usb_path, - CONF_ADDON_NETWORK_KEY: self.network_key, - } - - if new_addon_config != self.addon_config: - await self._async_set_addon_config(new_addon_config) - - try: - await hassio.async_start_addon(self.hass, "core_zwave") - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to start OpenZWave add-on: %s", err) - errors["base"] = "addon_start_failed" - else: - return self._async_create_entry_from_vars() - - usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") - network_key = self.addon_config.get( - CONF_ADDON_NETWORK_KEY, self.network_key or "" - ) - - data_schema = vol.Schema( - { - vol.Required(CONF_USB_PATH, default=usb_path): str, - vol.Optional(CONF_NETWORK_KEY, default=network_key): str, - } - ) - - return self.async_show_form( - step_id="start_addon", data_schema=data_schema, errors=errors - ) - - async def _async_get_addon_info(self): - """Return and cache OpenZWave add-on info.""" - try: - addon_info = await hassio.async_get_addon_info(self.hass, "core_zwave") - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to get OpenZWave add-on info: %s", err) - raise AbortFlow("addon_info_failed") from err - - return addon_info - - async def _async_is_addon_running(self): - """Return True if OpenZWave add-on is running.""" - addon_info = await self._async_get_addon_info() - return addon_info["state"] == "started" - - async def _async_is_addon_installed(self): - """Return True if OpenZWave add-on is installed.""" - addon_info = await self._async_get_addon_info() - return addon_info["version"] is not None - - async def _async_get_addon_config(self): - """Get OpenZWave add-on config.""" - addon_info = await self._async_get_addon_info() - return addon_info["options"] - - async def _async_set_addon_config(self, config): - """Set OpenZWave add-on config.""" - options = {"options": config} - try: - await hassio.async_set_addon_options(self.hass, "core_zwave", options) - except hassio.HassioAPIError as err: - _LOGGER.error("Failed to set OpenZWave add-on config: %s", err) - raise AbortFlow("addon_set_config_failed") from err - - async def _async_install_addon(self): - """Install the OpenZWave add-on.""" - try: - await hassio.async_install_addon(self.hass, "core_zwave") - finally: - # Continue the flow after show progress when the task is done. - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py deleted file mode 100644 index 68eaf9f7c8a..00000000000 --- a/homeassistant/components/ozw/const.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Constants for the ozw integration.""" -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.fan import DOMAIN as FAN_DOMAIN -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN - -DOMAIN = "ozw" -DATA_UNSUBSCRIBE = "unsubscribe" - -CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" -CONF_USE_ADDON = "use_addon" - -PLATFORMS = [ - BINARY_SENSOR_DOMAIN, - COVER_DOMAIN, - CLIMATE_DOMAIN, - FAN_DOMAIN, - LIGHT_DOMAIN, - LOCK_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, -] -MANAGER = "manager" -NODES_VALUES = "nodes_values" - -# MQTT Topics -TOPIC_OPENZWAVE = "OpenZWave" - -# Common Attributes -ATTR_CONFIG_PARAMETER = "parameter" -ATTR_CONFIG_VALUE = "value" -ATTR_INSTANCE_ID = "instance_id" -ATTR_SECURE = "secure" -ATTR_NODE_ID = "node_id" -ATTR_SCENE_ID = "scene_id" -ATTR_SCENE_LABEL = "scene_label" -ATTR_SCENE_VALUE_ID = "scene_value_id" -ATTR_SCENE_VALUE_LABEL = "scene_value_label" - -# Config entry data and options -MIGRATED = "migrated" - -# Service specific -SERVICE_ADD_NODE = "add_node" -SERVICE_REMOVE_NODE = "remove_node" -SERVICE_CANCEL_COMMAND = "cancel_command" -SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" - -# Home Assistant Events -EVENT_SCENE_ACTIVATED = f"{DOMAIN}.scene_activated" - -# Signals -SIGNAL_DELETE_ENTITY = f"{DOMAIN}_delete_entity" - -# Discovery Information -DISC_COMMAND_CLASS = "command_class" -DISC_COMPONENT = "component" -DISC_GENERIC_DEVICE_CLASS = "generic_device_class" -DISC_GENRE = "genre" -DISC_INDEX = "index" -DISC_INSTANCE = "instance" -DISC_NODE_ID = "node_id" -DISC_OPTIONAL = "optional" -DISC_PRIMARY = "primary" -DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" -DISC_TYPE = "type" -DISC_VALUES = "values" diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py deleted file mode 100644 index 780e19c2ccd..00000000000 --- a/homeassistant/components/ozw/cover.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Support for Z-Wave cover devices.""" -from openzwavemqtt.const import CommandClass - -from homeassistant.components.cover import ( - ATTR_POSITION, - DOMAIN as COVER_DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, - CoverDeviceClass, - CoverEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -VALUE_SELECTED_ID = "Selected_id" -PRESS_BUTTON = True -RELEASE_BUTTON = False - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Cover from Config Entry.""" - - @callback - def async_add_cover(values): - """Add Z-Wave Cover.""" - if values.primary.command_class == CommandClass.BARRIER_OPERATOR: - cover = ZwaveGarageDoorBarrier(values) - else: - cover = ZWaveCoverEntity(values) - - async_add_entities([cover]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{COVER_DOMAIN}", async_add_cover) - ) - - -def percent_to_zwave_position(value): - """Convert position in 0-100 scale to 0-99 scale. - - `value` -- (int) Position byte value from 0-100. - """ - if value > 0: - return max(1, round((value / 100) * 99)) - return 0 - - -class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): - """Representation of a Z-Wave Cover device.""" - - @property - def is_closed(self): - """Return true if cover is closed.""" - return self.values.primary.value == 0 - - @property - def current_cover_position(self): - """Return the current position of cover where 0 means closed and 100 is fully open.""" - return round((self.values.primary.value / 99) * 100) - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - self.values.primary.send_value(percent_to_zwave_position(kwargs[ATTR_POSITION])) - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - self.values.open.send_value(PRESS_BUTTON) - - async def async_close_cover(self, **kwargs): - """Close cover.""" - self.values.close.send_value(PRESS_BUTTON) - - async def async_stop_cover(self, **kwargs): - """Stop cover.""" - # Need to issue both buttons release since qt-openzwave implements idempotency - # keeping internal state of model to trigger actual updates. We could also keep - # another state in Home Assistant to know which button to release, - # but this implementation is simpler. - self.values.open.send_value(RELEASE_BUTTON) - self.values.close.send_value(RELEASE_BUTTON) - - -class ZwaveGarageDoorBarrier(ZWaveDeviceEntity, CoverEntity): - """Representation of a barrier operator Zwave garage door device.""" - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_GARAGE - - @property - def device_class(self): - """Return the class of this device, from CoverDeviceClass.""" - return CoverDeviceClass.GARAGE - - @property - def is_opening(self): - """Return true if cover is in an opening state.""" - return self.values.primary.value[VALUE_SELECTED_ID] == 3 - - @property - def is_closing(self): - """Return true if cover is in a closing state.""" - return self.values.primary.value[VALUE_SELECTED_ID] == 1 - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return self.values.primary.value[VALUE_SELECTED_ID] == 0 - - async def async_close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.send_value(0) - - async def async_open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.send_value(4) diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py deleted file mode 100644 index 67e3442cf5f..00000000000 --- a/homeassistant/components/ozw/discovery.py +++ /dev/null @@ -1,356 +0,0 @@ -"""Map Z-Wave nodes and values to Home Assistant entities.""" -import openzwavemqtt.const as const_ozw -from openzwavemqtt.const import CommandClass, ValueGenre, ValueIndex, ValueType - -from . import const - -DISCOVERY_SCHEMAS = ( - { # Binary sensors - const.DISC_COMPONENT: "binary_sensor", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.SENSOR_BINARY, - const.DISC_TYPE: ValueType.BOOL, - const.DISC_GENRE: ValueGenre.USER, - }, - "off_delay": { - const.DISC_COMMAND_CLASS: CommandClass.CONFIGURATION, - const.DISC_INDEX: 9, - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Notification CommandClass translates to binary_sensor - const.DISC_COMPONENT: "binary_sensor", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.NOTIFICATION, - const.DISC_GENRE: ValueGenre.USER, - const.DISC_TYPE: (ValueType.BOOL, ValueType.LIST), - } - }, - }, - { # Z-Wave Thermostat device translates to Climate entity - const.DISC_COMPONENT: "climate", - const.DISC_GENERIC_DEVICE_CLASS: ( - const_ozw.GENERIC_TYPE_THERMOSTAT, - const_ozw.GENERIC_TYPE_SENSOR_MULTILEVEL, - ), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL, - const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, - const_ozw.SPECIFIC_TYPE_SETBACK_THERMOSTAT, - const_ozw.SPECIFIC_TYPE_THERMOSTAT_HEATING, - const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - const_ozw.SPECIFIC_TYPE_NOT_USED, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,) - }, - "mode": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,), - const.DISC_OPTIONAL: True, - }, - "temperature": { - const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - "fan_mode": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_MODE,), - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), - const.DISC_OPTIONAL: True, - }, - "fan_action": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_STATE,), - const.DISC_OPTIONAL: True, - }, - "valve_position": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: (0,), - const.DISC_OPTIONAL: True, - }, - "setpoint_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - "setpoint_cooling": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (2,), - const.DISC_OPTIONAL: True, - }, - "setpoint_furnace": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (7,), - const.DISC_OPTIONAL: True, - }, - "setpoint_dry_air": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (8,), - const.DISC_OPTIONAL: True, - }, - "setpoint_moist_air": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (9,), - const.DISC_OPTIONAL: True, - }, - "setpoint_auto_changeover": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (10,), - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (11,), - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_cooling": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (12,), - const.DISC_OPTIONAL: True, - }, - "setpoint_away_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (13,), - const.DISC_OPTIONAL: True, - }, - "setpoint_away_cooling": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (14,), - const.DISC_OPTIONAL: True, - }, - "setpoint_full_power": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (15,), - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Z-Wave Thermostat device without mode support - const.DISC_COMPONENT: "climate", - const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_THERMOSTAT,), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - const_ozw.SPECIFIC_TYPE_NOT_USED, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,) - }, - "temperature": { - const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), - const.DISC_OPTIONAL: True, - }, - "valve_position": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: (0,), - const.DISC_OPTIONAL: True, - }, - "setpoint_heating": { - const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), - const.DISC_INDEX: (1,), - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Rollershutter - const.DISC_COMPONENT: "cover", - const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const_ozw.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const_ozw.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const_ozw.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const_ozw.SPECIFIC_TYPE_SECURE_DOOR, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, - const.DISC_GENRE: ValueGenre.USER, - }, - "open": { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_BRIGHT, - const.DISC_OPTIONAL: True, - }, - "close": { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DIM, - const.DISC_OPTIONAL: True, - }, - }, - }, - { # Garage Door Barrier - const.DISC_COMPONENT: "cover", - const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_ENTRY_CONTROL,), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.BARRIER_OPERATOR, - const.DISC_INDEX: ValueIndex.BARRIER_OPERATOR_LABEL, - }, - }, - }, - { # Fan - const.DISC_COMPONENT: "fan", - const.DISC_GENERIC_DEVICE_CLASS: const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.DISC_SPECIFIC_DEVICE_CLASS: const_ozw.SPECIFIC_TYPE_FAN_SWITCH, - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, - const.DISC_TYPE: ValueType.BYTE, - }, - }, - }, - { # Light - const.DISC_COMPONENT: "light", - const.DISC_GENERIC_DEVICE_CLASS: ( - const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, - const_ozw.GENERIC_TYPE_SWITCH_REMOTE, - ), - const.DISC_SPECIFIC_DEVICE_CLASS: ( - const_ozw.SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL, - const_ozw.SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL, - const_ozw.SPECIFIC_TYPE_COLOR_TUNABLE_BINARY, - const_ozw.SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL, - const_ozw.SPECIFIC_TYPE_NOT_USED, - ), - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, - const.DISC_TYPE: ValueType.BYTE, - }, - "dimming_duration": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), - const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DURATION, - const.DISC_OPTIONAL: True, - }, - "color": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,), - const.DISC_INDEX: ValueIndex.SWITCH_COLOR_COLOR, - const.DISC_OPTIONAL: True, - }, - "color_channels": { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_COLOR,), - const.DISC_INDEX: ValueIndex.SWITCH_COLOR_CHANNELS, - const.DISC_OPTIONAL: True, - }, - "min_kelvin": { - const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,), - const.DISC_INDEX: 81, # PR for upstream to add SWITCH_COLOR_CT_WARM - const.DISC_TYPE: ValueType.INT, - const.DISC_OPTIONAL: True, - }, - "max_kelvin": { - const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,), - const.DISC_INDEX: 82, # PR for upstream to add SWITCH_COLOR_CT_COLD - const.DISC_TYPE: ValueType.INT, - const.DISC_OPTIONAL: True, - }, - }, - }, - { # All other text/numeric sensors - const.DISC_COMPONENT: "sensor", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: ( - CommandClass.SENSOR_MULTILEVEL, - CommandClass.METER, - CommandClass.ALARM, - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - CommandClass.BATTERY, - CommandClass.NOTIFICATION, - CommandClass.BASIC, - ), - const.DISC_TYPE: ( - ValueType.DECIMAL, - ValueType.INT, - ValueType.STRING, - ValueType.BYTE, - ValueType.LIST, - ), - } - }, - }, - { # Switch platform - const.DISC_COMPONENT: "switch", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_BINARY,), - const.DISC_TYPE: ValueType.BOOL, - const.DISC_GENRE: ValueGenre.USER, - } - }, - }, - { # Lock platform - const.DISC_COMPONENT: "lock", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: (CommandClass.DOOR_LOCK,), - const.DISC_TYPE: ValueType.BOOL, - const.DISC_GENRE: ValueGenre.USER, - } - }, - }, -) - - -def check_node_schema(node, schema): - """Check if node matches the passed node schema.""" - if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]: - return False - if const.DISC_GENERIC_DEVICE_CLASS in schema and not eq_or_in( - node.node_generic, schema[const.DISC_GENERIC_DEVICE_CLASS] - ): - return False - if const.DISC_SPECIFIC_DEVICE_CLASS in schema and not eq_or_in( - node.node_specific, schema[const.DISC_SPECIFIC_DEVICE_CLASS] - ): - return False - return True - - -def check_value_schema(value, schema): - """Check if the value matches the passed value schema.""" - if const.DISC_COMMAND_CLASS in schema and not eq_or_in( - value.parent.command_class_id, schema[const.DISC_COMMAND_CLASS] - ): - return False - if const.DISC_TYPE in schema and not eq_or_in(value.type, schema[const.DISC_TYPE]): - return False - if const.DISC_GENRE in schema and not eq_or_in( - value.genre, schema[const.DISC_GENRE] - ): - return False - if const.DISC_INDEX in schema and not eq_or_in( - value.index, schema[const.DISC_INDEX] - ): - return False - if const.DISC_INSTANCE in schema and not eq_or_in( - value.instance, schema[const.DISC_INSTANCE] - ): - return False - - return True - - -def eq_or_in(val, options): - """Return True if options contains value or if value is equal to options.""" - return val in options if isinstance(options, tuple) else val == options diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py deleted file mode 100644 index 790b60aed69..00000000000 --- a/homeassistant/components/ozw/entity.py +++ /dev/null @@ -1,304 +0,0 @@ -"""Generic Z-Wave Entity Classes.""" - -import copy -import logging - -from openzwavemqtt.const import ( - EVENT_INSTANCE_STATUS_CHANGED, - EVENT_VALUE_CHANGED, - OZW_READY_STATES, - CommandClass, - ValueIndex, -) -from openzwavemqtt.models.node import OZWNode -from openzwavemqtt.models.value import OZWValue - -from homeassistant.const import ATTR_NAME, ATTR_SW_VERSION, ATTR_VIA_DEVICE -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import DeviceInfo, Entity - -from . import const -from .const import DOMAIN, PLATFORMS -from .discovery import check_node_schema, check_value_schema - -_LOGGER = logging.getLogger(__name__) -OZW_READY_STATES_VALUES = {st.value for st in OZW_READY_STATES} - - -class ZWaveDeviceEntityValues: - """Manages entity access to the underlying Z-Wave value objects.""" - - def __init__(self, hass, options, schema, primary_value): - """Initialize the values object with the passed entity schema.""" - self._hass = hass - self._entity_created = False - self._schema = copy.deepcopy(schema) - self._values = {} - self.options = options - - # Go through values listed in the discovery schema, initialize them, - # and add a check to the schema to make sure the Instance matches. - for name, disc_settings in self._schema[const.DISC_VALUES].items(): - self._values[name] = None - disc_settings[const.DISC_INSTANCE] = (primary_value.instance,) - - self._values[const.DISC_PRIMARY] = primary_value - self._node = primary_value.node - self._schema[const.DISC_NODE_ID] = [self._node.node_id] - - def async_setup(self): - """Set up values instance.""" - # Check values that have already been discovered for node - # and see if they match the schema and need added to the entity. - for value in self._node.values(): - self.async_check_value(value) - - # Check if all the _required_ values in the schema are present and - # create the entity. - self._async_check_entity_ready() - - def __getattr__(self, name): - """Get the specified value for this entity.""" - return self._values.get(name, None) - - def __iter__(self): - """Allow iteration over all values.""" - return iter(self._values.values()) - - def __contains__(self, name): - """Check if the specified name/key exists in the values.""" - return name in self._values - - @callback - def async_check_value(self, value): - """Check if the new value matches a missing value for this entity. - - If a match is found, it is added to the values mapping. - """ - # Make sure the node matches the schema for this entity. - if not check_node_schema(value.node, self._schema): - return - - # Go through the possible values for this entity defined by the schema. - for name, name_value in self._values.items(): - # Skip if it's already been added. - if name_value is not None: - continue - # Skip if the value doesn't match the schema. - if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): - continue - - # Add value to mapping. - self._values[name] = value - - # If the entity has already been created, notify it of the new value. - if self._entity_created: - async_dispatcher_send( - self._hass, f"{DOMAIN}_{self.values_id}_value_added" - ) - - # Check if entity has all required values and create the entity if needed. - self._async_check_entity_ready() - - @callback - def _async_check_entity_ready(self): - """Check if all required values are discovered and create entity.""" - # Abort if the entity has already been created - if self._entity_created: - return - - # Go through values defined in the schema and abort if a required value is missing. - for name, disc_settings in self._schema[const.DISC_VALUES].items(): - if self._values[name] is None and not disc_settings.get( - const.DISC_OPTIONAL - ): - return - - # We have all the required values, so create the entity. - component = self._schema[const.DISC_COMPONENT] - - _LOGGER.debug( - "Adding Node_id=%s Generic_command_class=%s, " - "Specific_command_class=%s, " - "Command_class=%s, Index=%s, Value type=%s, " - "Genre=%s as %s", - self._node.node_id, - self._node.node_generic, - self._node.node_specific, - self.primary.command_class, - self.primary.index, - self.primary.type, - self.primary.genre, - component, - ) - self._entity_created = True - - if component in PLATFORMS: - async_dispatcher_send(self._hass, f"{DOMAIN}_new_{component}", self) - - @property - def values_id(self): - """Identification for this values collection.""" - return create_value_id(self.primary) - - -class ZWaveDeviceEntity(Entity): - """Generic Entity Class for a Z-Wave Device.""" - - def __init__(self, values): - """Initialize a generic Z-Wave device entity.""" - self.values = values - self.options = values.options - - @callback - def on_value_update(self): - """Call when a value is added/updated in the entity EntityValues Collection. - - To be overridden by platforms needing this event. - """ - - async def async_added_to_hass(self): - """Call when entity is added.""" - # Add dispatcher and OZW listeners callbacks. - # Add to on_remove so they will be cleaned up on entity removal. - self.async_on_remove( - self.options.listen(EVENT_VALUE_CHANGED, self._value_changed) - ) - self.async_on_remove( - self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{DOMAIN}_{self.values.values_id}_value_added", - self._value_added, - ) - ) - - @property - def device_info(self) -> DeviceInfo: - """Return device information for the device registry.""" - node = self.values.primary.node - node_instance = self.values.primary.instance - dev_id = create_device_id(node, self.values.primary.instance) - node_firmware = node.get_value( - CommandClass.VERSION, ValueIndex.VERSION_APPLICATION - ) - device_info = DeviceInfo( - identifiers={(DOMAIN, dev_id)}, - name=create_device_name(node), - manufacturer=node.node_manufacturer_name, - model=node.node_product_name, - ) - if node_firmware is not None: - device_info[ATTR_SW_VERSION] = node_firmware.value - - # device with multiple instances is split up into virtual devices for each instance - if node_instance > 1: - parent_dev_id = create_device_id(node) - device_info[ATTR_NAME] += f" - Instance {node_instance}" - device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_dev_id) - return device_info - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - return {const.ATTR_NODE_ID: self.values.primary.node.node_id} - - @property - def name(self): - """Return the name of the entity.""" - node = self.values.primary.node - return f"{create_device_name(node)}: {self.values.primary.label}" - - @property - def unique_id(self): - """Return the unique_id of the entity.""" - return self.values.values_id - - @property - def available(self) -> bool: - """Return entity availability.""" - # Use OZW Daemon status for availability. - instance_status = self.values.primary.ozw_instance.get_status() - return instance_status and instance_status.status in OZW_READY_STATES_VALUES - - @callback - def _value_changed(self, value): - """Call when a value from ZWaveDeviceEntityValues is changed. - - Should not be overridden by subclasses. - """ - if value.value_id_key in (v.value_id_key for v in self.values if v): - self.on_value_update() - self.async_write_ha_state() - - @callback - def _value_added(self): - """Call when a value from ZWaveDeviceEntityValues is added. - - Should not be overridden by subclasses. - """ - self.on_value_update() - - @callback - def _instance_updated(self, new_status): - """Call when the instance status changes. - - Should not be overridden by subclasses. - """ - self.on_value_update() - self.async_write_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - async def _delete_callback(self, values_id): - """Remove this entity.""" - if not self.values: - return # race condition: delete already requested - if values_id == self.values.values_id: - await self.async_remove(force_remove=True) - - -def create_device_name(node: OZWNode): - """Generate sensible (short) default device name from a OZWNode.""" - # Prefer custom name set by OZWAdmin if present - if node.node_name: - return node.node_name - # Prefer short devicename from metadata if present - if node.meta_data and node.meta_data.get("Name"): - return node.meta_data["Name"] - # Fallback to productname or devicetype strings - if node.node_product_name: - return node.node_product_name - if node.node_device_type_string: - return node.node_device_type_string - if node.node_specific_string: - return node.node_specific_string - # Last resort: use Node id (should never happen, but just in case) - return f"Node {node.id}" - - -def create_device_id(node: OZWNode, node_instance: int = 1): - """Generate unique device_id from a OZWNode.""" - ozw_instance = node.parent.id - dev_id = f"{ozw_instance}.{node.node_id}.{node_instance}" - return dev_id - - -def create_value_id(value: OZWValue): - """Generate unique value_id from an OZWValue.""" - # [OZW_INSTANCE_ID]-[NODE_ID]-[VALUE_ID_KEY] - return f"{value.node.parent.id}-{value.node.id}-{value.value_id_key}" diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py deleted file mode 100644 index fdfbdadc2ee..00000000000 --- a/homeassistant/components/ozw/fan.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for Z-Wave fans.""" -import math - -from homeassistant.components.fan import ( - DOMAIN as FAN_DOMAIN, - SUPPORT_SET_SPEED, - FanEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.percentage import ( - int_states_in_range, - percentage_to_ranged_value, - ranged_value_to_percentage, -) - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED -SPEED_RANGE = (1, 99) # off is not included - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Fan from Config Entry.""" - - @callback - def async_add_fan(values): - """Add Z-Wave Fan.""" - fan = ZwaveFan(values) - async_add_entities([fan]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{FAN_DOMAIN}", async_add_fan) - ) - - -class ZwaveFan(ZWaveDeviceEntity, FanEntity): - """Representation of a Z-Wave fan.""" - - async def async_set_percentage(self, percentage): - """Set the speed percentage of the fan.""" - if percentage is None: - # Value 255 tells device to return to previous value - zwave_speed = 255 - elif percentage == 0: - zwave_speed = 0 - else: - zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - self.values.primary.send_value(zwave_speed) - - async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs): - """Turn the device on.""" - await self.async_set_percentage(percentage) - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - self.values.primary.send_value(0) - - @property - def is_on(self): - """Return true if device is on (speed above 0).""" - return self.values.primary.value > 0 - - @property - def percentage(self): - """Return the current speed. - - The Z-Wave speed value is a byte 0-255. 255 means previous value. - The normal range of the speed is 0-99. 0 means off. - """ - return ranged_value_to_percentage(SPEED_RANGE, self.values.primary.value) - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py deleted file mode 100644 index a3c6798f40f..00000000000 --- a/homeassistant/components/ozw/light.py +++ /dev/null @@ -1,343 +0,0 @@ -"""Support for Z-Wave lights.""" -import logging - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - ATTR_RGBW_COLOR, - ATTR_TRANSITION, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_RGBW, - DOMAIN as LIGHT_DOMAIN, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -import homeassistant.util.color as color_util - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -_LOGGER = logging.getLogger(__name__) - -ATTR_VALUE = "Value" -COLOR_CHANNEL_WARM_WHITE = 0x01 -COLOR_CHANNEL_COLD_WHITE = 0x02 -COLOR_CHANNEL_RED = 0x04 -COLOR_CHANNEL_GREEN = 0x08 -COLOR_CHANNEL_BLUE = 0x10 - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Light from Config Entry.""" - - @callback - def async_add_light(values): - """Add Z-Wave Light.""" - light = ZwaveLight(values) - - async_add_entities([light]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{LIGHT_DOMAIN}", async_add_light) - ) - - -def byte_to_zwave_brightness(value): - """Convert brightness in 0-255 scale to 0-99 scale. - - `value` -- (int) Brightness byte value from 0-255. - """ - if value > 0: - return max(1, round((value / 255) * 99)) - return 0 - - -class ZwaveLight(ZWaveDeviceEntity, LightEntity): - """Representation of a Z-Wave light.""" - - def __init__(self, values): - """Initialize the light.""" - super().__init__(values) - self._color_channels = None - self._hs = None - self._rgbw_color = None - self._ct = None - self._attr_color_mode = None - self._attr_supported_features = 0 - self._attr_supported_color_modes = set() - self._min_mireds = 153 # 6500K as a safe default - self._max_mireds = 370 # 2700K as a safe default - - # make sure that supported features is correctly set - self.on_value_update() - - @callback - def on_value_update(self): - """Call when the underlying value(s) is added or updated.""" - if self.values.dimming_duration is not None: - self._attr_supported_features |= SUPPORT_TRANSITION - - if self.values.color_channels is not None: - # Support Color Temp if both white channels - if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and ( - self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE - ): - self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) - self._attr_supported_color_modes.add(COLOR_MODE_HS) - - # Support White value if only a single white channel - if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ ( - (self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0 - ): - self._attr_supported_color_modes.add(COLOR_MODE_RGBW) - - if not self._attr_supported_color_modes and self.values.color is not None: - self._attr_supported_color_modes.add(COLOR_MODE_HS) - - if not self._attr_supported_color_modes: - self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) - # Default: Brightness (no color) - self._attr_color_mode = COLOR_MODE_BRIGHTNESS - - if self.values.color is not None: - self._calculate_color_values() - - @property - def brightness(self): - """Return the brightness of this light between 0..255. - - Zwave multilevel switches use a range of [0, 99] to control brightness. - """ - if "target" in self.values: - return round((self.values.target.value / 99) * 255) - return round((self.values.primary.value / 99) * 255) - - @property - def is_on(self): - """Return true if device is on (brightness above 0).""" - if "target" in self.values: - return self.values.target.value > 0 - return self.values.primary.value > 0 - - @property - def hs_color(self): - """Return the hs color.""" - return self._hs - - @property - def rgbw_color(self): - """Return the rgbw color.""" - return self._rgbw_color - - @property - def color_temp(self): - """Return the color temperature.""" - return self._ct - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._max_mireds - - @callback - def async_set_duration(self, **kwargs): - """Set the transition time for the brightness value. - - Zwave Dimming Duration values now use seconds as an - integer (max: 7620 seconds or 127 mins) - Build 1205 https://github.com/OpenZWave/open-zwave/commit/f81bc04 - """ - if self.values.dimming_duration is None: - return - - ozw_version = tuple( - int(x) - for x in self.values.primary.ozw_instance.get_status().openzwave_version.split( - "." - ) - ) - - if ATTR_TRANSITION not in kwargs: - # no transition specified by user, use defaults - new_value = 7621 # anything over 7620 uses the factory default - if ozw_version < (1, 6, 1205): - new_value = 255 # default for older version - - else: - # transition specified by user - new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION]))) - if ozw_version < (1, 6, 1205): - if (transition := kwargs[ATTR_TRANSITION]) <= 127: - new_value = int(transition) - else: - minutes = int(transition / 60) - _LOGGER.debug( - "Transition rounded to %d minutes for %s", - minutes, - self.entity_id, - ) - new_value = minutes + 128 - - # only send value if it differs from current - # this prevents a command for nothing - if self.values.dimming_duration.value != new_value: - self.values.dimming_duration.send_value(new_value) - - async def async_turn_on(self, **kwargs): - """Turn the device on.""" - self.async_set_duration(**kwargs) - - rgbw = None - hs_color = kwargs.get(ATTR_HS_COLOR) - rgbw_color = kwargs.get(ATTR_RGBW_COLOR) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - - if hs_color is not None: - rgbw = "#" - for colorval in color_util.color_hs_to_RGB(*hs_color): - rgbw += f"{colorval:02x}" - if self._color_channels and self._color_channels & COLOR_CHANNEL_COLD_WHITE: - rgbw += "0000" - else: - # trim the CW value or it will not work correctly - rgbw += "00" - # white LED must be off in order for color to work - - elif rgbw_color is not None: - red = rgbw_color[0] - green = rgbw_color[1] - blue = rgbw_color[2] - white = rgbw_color[3] - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - # trim the CW value or it will not work correctly - rgbw = f"#{red:02x}{green:02x}{blue:02x}{white:02x}" - else: - rgbw = f"#{red:02x}{green:02x}{blue:02x}00{white:02x}" - - elif color_temp is not None: - # Limit color temp to min/max values - cold = max( - 0, - min( - 255, - round( - (self._max_mireds - color_temp) - / (self._max_mireds - self._min_mireds) - * 255 - ), - ), - ) - warm = 255 - cold - rgbw = f"#000000{warm:02x}{cold:02x}" - - if rgbw and self.values.color: - self.values.color.send_value(rgbw) - - # Zwave multilevel switches use a range of [0, 99] to control - # brightness. Level 255 means to set it to previous value. - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs[ATTR_BRIGHTNESS] - brightness = byte_to_zwave_brightness(brightness) - else: - brightness = 255 - - self.values.primary.send_value(brightness) - - async def async_turn_off(self, **kwargs): - """Turn the device off.""" - self.async_set_duration(**kwargs) - - self.values.primary.send_value(0) - - def _calculate_color_values(self): - """Parse color rgb and color temperature data.""" - # Color Data String - data = self.values.color.data[ATTR_VALUE] - - # RGB is always present in the OpenZWave color data string. - rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] - self._hs = color_util.color_RGB_to_hs(*rgb) - - # Light supports color, set color mode to hs - self._attr_color_mode = COLOR_MODE_HS - - if self.values.color_channels is None: - return - - # Color Channels - self._color_channels = self.values.color_channels.data[ATTR_VALUE] - - # Parse remaining color channels. OpenZWave appends white channels - # that are present. - index = 7 - temp_warm = 0 - temp_cold = 0 - - # Update color temp limits. - if self.values.min_kelvin: - self._max_mireds = color_util.color_temperature_kelvin_to_mired( - self.values.min_kelvin.data[ATTR_VALUE] - ) - if self.values.max_kelvin: - self._min_mireds = color_util.color_temperature_kelvin_to_mired( - self.values.max_kelvin.data[ATTR_VALUE] - ) - - # Warm white - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - white = int(data[index : index + 2], 16) - self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] - temp_warm = white - # Light supports rgbw, set color mode to rgbw - self._attr_color_mode = COLOR_MODE_RGBW - - index += 2 - - # Cold white - if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - white = int(data[index : index + 2], 16) - self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] - temp_cold = white - # Light supports rgbw, set color mode to rgbw - self._attr_color_mode = COLOR_MODE_RGBW - - # Calculate color temps based on white LED status - if temp_cold or temp_warm: - self._ct = round( - self._max_mireds - - ((temp_cold / 255) * (self._max_mireds - self._min_mireds)) - ) - - if ( - self._color_channels & COLOR_CHANNEL_WARM_WHITE - and self._color_channels & COLOR_CHANNEL_COLD_WHITE - ): - # Light supports 5 channels, set color_mode to color_temp or hs - if rgb[0] == 0 and rgb[1] == 0 and rgb[2] == 0: - # Color channels turned off, set color mode to color_temp - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - else: - self._attr_color_mode = COLOR_MODE_HS - - if not ( - self._color_channels & COLOR_CHANNEL_RED - or self._color_channels & COLOR_CHANNEL_GREEN - or self._color_channels & COLOR_CHANNEL_BLUE - ): - self._hs = None diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py deleted file mode 100644 index ea6782c5c79..00000000000 --- a/homeassistant/components/ozw/lock.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Representation of Z-Wave locks.""" -import logging - -from openzwavemqtt.const import ATTR_CODE_SLOT -from openzwavemqtt.exceptions import BaseOZWError -from openzwavemqtt.util.lock import clear_usercode, set_usercode -import voluptuous as vol - -from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -ATTR_USERCODE = "usercode" - -SERVICE_SET_USERCODE = "set_usercode" -SERVICE_GET_USERCODE = "get_usercode" -SERVICE_CLEAR_USERCODE = "clear_usercode" - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave lock from config entry.""" - - @callback - def async_add_lock(value): - """Add Z-Wave Lock.""" - lock = ZWaveLock(value) - - async_add_entities([lock]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect(hass, f"{DOMAIN}_new_{LOCK_DOMAIN}", async_add_lock) - ) - - platform = entity_platform.async_get_current_platform() - - platform.async_register_entity_service( - SERVICE_SET_USERCODE, - { - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - }, - "async_set_usercode", - ) - - platform.async_register_entity_service( - SERVICE_CLEAR_USERCODE, - {vol.Required(ATTR_CODE_SLOT): vol.Coerce(int)}, - "async_clear_usercode", - ) - - -def _call_util_lock_function(function, *args): - """Call an openzwavemqtt.util.lock function and return success of call.""" - try: - function(*args) - except BaseOZWError as err: - _LOGGER.error("%s: %s", type(err), err.args[0]) - return False - - return True - - -class ZWaveLock(ZWaveDeviceEntity, LockEntity): - """Representation of a Z-Wave lock.""" - - @property - def is_locked(self): - """Return a boolean for the state of the lock.""" - return bool(self.values.primary.value) - - async def async_lock(self, **kwargs): - """Lock the lock.""" - self.values.primary.send_value(True) - - async def async_unlock(self, **kwargs): - """Unlock the lock.""" - self.values.primary.send_value(False) - - @callback - def async_set_usercode(self, code_slot, usercode): - """Set the usercode to index X on the lock.""" - if _call_util_lock_function( - set_usercode, self.values.primary.node, code_slot, usercode - ): - _LOGGER.debug("User code at slot %s set", code_slot) - - @callback - def async_clear_usercode(self, code_slot): - """Clear usercode in slot X on the lock.""" - if _call_util_lock_function( - clear_usercode, self.values.primary.node, code_slot - ): - _LOGGER.info("Usercode at slot %s is cleared", code_slot) diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json deleted file mode 100644 index 997fbbc5a70..00000000000 --- a/homeassistant/components/ozw/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "ozw", - "name": "OpenZWave (deprecated)", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ozw", - "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], - "after_dependencies": ["mqtt"], - "codeowners": ["@cgarwood", "@marcelveldt", "@MartinHjelmare"], - "iot_class": "local_push", - "loggers": ["openzwavemqtt"] -} diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py deleted file mode 100644 index 0a109118153..00000000000 --- a/homeassistant/components/ozw/sensor.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Representation of Z-Wave sensors.""" -import logging - -from openzwavemqtt.const import CommandClass, ValueType - -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave sensor from config entry.""" - - @callback - def async_add_sensor(value): - """Add Z-Wave Sensor.""" - # Basic Sensor types - if value.primary.type in ( - ValueType.BYTE, - ValueType.INT, - ValueType.SHORT, - ValueType.DECIMAL, - ): - sensor = ZWaveNumericSensor(value) - - elif value.primary.type == ValueType.LIST: - sensor = ZWaveListSensor(value) - - elif value.primary.type == ValueType.STRING: - sensor = ZWaveStringSensor(value) - - else: - _LOGGER.warning("Sensor not implemented for value %s", value.primary.label) - return - - async_add_entities([sensor]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{SENSOR_DOMAIN}", async_add_sensor - ) - ) - - -class ZwaveSensorBase(ZWaveDeviceEntity, SensorEntity): - """Basic Representation of a Z-Wave sensor.""" - - @property - def device_class(self): - """Return the device class of the sensor.""" - if self.values.primary.command_class == CommandClass.BATTERY: - return SensorDeviceClass.BATTERY - if self.values.primary.command_class == CommandClass.METER: - return SensorDeviceClass.POWER - if "Temperature" in self.values.primary.label: - return SensorDeviceClass.TEMPERATURE - if "Illuminance" in self.values.primary.label: - return SensorDeviceClass.ILLUMINANCE - if "Humidity" in self.values.primary.label: - return SensorDeviceClass.HUMIDITY - if "Power" in self.values.primary.label: - return SensorDeviceClass.POWER - if "Energy" in self.values.primary.label: - return SensorDeviceClass.POWER - if "Electric" in self.values.primary.label: - return SensorDeviceClass.POWER - if "Pressure" in self.values.primary.label: - return SensorDeviceClass.PRESSURE - return None - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # We hide some of the more advanced sensors by default to not overwhelm users - if self.values.primary.command_class in ( - CommandClass.BASIC, - CommandClass.INDICATOR, - CommandClass.NOTIFICATION, - ): - return False - return True - - @property - def force_update(self) -> bool: - """Force updates.""" - return True - - -class ZWaveStringSensor(ZwaveSensorBase): - """Representation of a Z-Wave sensor.""" - - @property - def native_value(self): - """Return state of the sensor.""" - return self.values.primary.value - - @property - def native_unit_of_measurement(self): - """Return unit of measurement the value is expressed in.""" - return self.values.primary.units - - @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - return False - - -class ZWaveNumericSensor(ZwaveSensorBase): - """Representation of a Z-Wave sensor.""" - - @property - def native_value(self): - """Return state of the sensor.""" - return round(self.values.primary.value, 2) - - @property - def native_unit_of_measurement(self): - """Return unit of measurement the value is expressed in.""" - if self.values.primary.units == "C": - return TEMP_CELSIUS - if self.values.primary.units == "F": - return TEMP_FAHRENHEIT - - return self.values.primary.units - - -class ZWaveListSensor(ZwaveSensorBase): - """Representation of a Z-Wave list sensor.""" - - @property - def native_value(self): - """Return the state of the sensor.""" - # We use the id as value for backwards compatibility - return self.values.primary.value["Selected_id"] - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attributes = super().extra_state_attributes - # add the value's label as property - attributes["label"] = self.values.primary.value["Selected"] - return attributes - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - # these sensors are only here for backwards compatibility, disable them by default - return False diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py deleted file mode 100644 index b6e54b4baa1..00000000000 --- a/homeassistant/components/ozw/services.py +++ /dev/null @@ -1,134 +0,0 @@ -"""Methods and classes related to executing Z-Wave commands and publishing these to hass.""" -import logging - -from openzwavemqtt.const import ATTR_LABEL, ATTR_POSITION, ATTR_VALUE -from openzwavemqtt.util.node import get_node_from_manager, set_config_parameter -import voluptuous as vol - -from homeassistant.core import ServiceCall, callback -import homeassistant.helpers.config_validation as cv - -from . import const - -_LOGGER = logging.getLogger(__name__) - - -class ZWaveServices: - """Class that holds our services ( Zwave Commands) that should be published to hass.""" - - def __init__(self, hass, manager): - """Initialize with both hass and ozwmanager objects.""" - self._hass = hass - self._manager = manager - - @callback - def async_register(self): - """Register all our services.""" - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_ADD_NODE, - self.async_add_node, - schema=vol.Schema( - { - vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), - vol.Optional(const.ATTR_SECURE, default=False): vol.Coerce(bool), - } - ), - ) - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_REMOVE_NODE, - self.async_remove_node, - schema=vol.Schema( - {vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)} - ), - ) - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_CANCEL_COMMAND, - self.async_cancel_command, - schema=vol.Schema( - {vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)} - ), - ) - - self._hass.services.async_register( - const.DOMAIN, - const.SERVICE_SET_CONFIG_PARAMETER, - self.async_set_config_parameter, - schema=vol.Schema( - { - vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.All( - cv.ensure_list, - [ - vol.All( - { - vol.Exclusive(ATTR_LABEL, "bit"): cv.string, - vol.Exclusive(ATTR_POSITION, "bit"): vol.Coerce( - int - ), - vol.Required(ATTR_VALUE): bool, - }, - cv.has_at_least_one_key(ATTR_LABEL, ATTR_POSITION), - ) - ], - ), - vol.Coerce(int), - bool, - cv.string, - ), - } - ), - ) - - @callback - def async_set_config_parameter(self, service: ServiceCall) -> None: - """Set a config parameter to a node.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - node_id = service.data[const.ATTR_NODE_ID] - param = service.data[const.ATTR_CONFIG_PARAMETER] - selection = service.data[const.ATTR_CONFIG_VALUE] - - # These function calls may raise an exception but that's ok because - # the exception will show in the UI to the user - node = get_node_from_manager(self._manager, instance_id, node_id) - payload = set_config_parameter(node, param, selection) - - _LOGGER.info( - "Setting configuration parameter %s on Node %s with value %s", - param, - node_id, - payload, - ) - - @callback - def async_add_node(self, service: ServiceCall) -> None: - """Enter inclusion mode on the controller.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - secure = service.data[const.ATTR_SECURE] - instance = self._manager.get_instance(instance_id) - if instance is None: - raise ValueError(f"No OpenZWave Instance with ID {instance_id}") - instance.add_node(secure) - - @callback - def async_remove_node(self, service: ServiceCall) -> None: - """Enter exclusion mode on the controller.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - instance = self._manager.get_instance(instance_id) - if instance is None: - raise ValueError(f"No OpenZWave Instance with ID {instance_id}") - instance.remove_node() - - @callback - def async_cancel_command(self, service: ServiceCall) -> None: - """Tell the controller to cancel an add or remove command.""" - instance_id = service.data[const.ATTR_INSTANCE_ID] - instance = self._manager.get_instance(instance_id) - if instance is None: - raise ValueError(f"No OpenZWave Instance with ID {instance_id}") - instance.cancel_controller_command() diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml deleted file mode 100644 index c9c23023134..00000000000 --- a/homeassistant/components/ozw/services.yaml +++ /dev/null @@ -1,121 +0,0 @@ -# Describes the format for available Z-Wave services -add_node: - name: Add node - description: Add a new node to the Z-Wave network. - fields: - secure: - name: Secure - description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. - default: false - selector: - boolean: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - selector: - number: - min: 1 - max: 255 - -remove_node: - name: Remove node - description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode. - fields: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - default: 1 - selector: - number: - min: 1 - max: 255 - -cancel_command: - name: Cancel command - description: Cancel a pending add or remove node command. - fields: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - default: 1 - selector: - number: - min: 1 - max: 255 - -set_config_parameter: - name: Set config parameter - description: Set a config parameter to a node on the Z-Wave network. - fields: - node_id: - name: Node ID - description: Node id of the device to set config parameter to. - required: true - selector: - number: - min: 1 - max: 255 - parameter: - name: Parameter - description: Parameter number to set. - required: true - selector: - number: - min: 1 - max: 255 - value: - name: Value - description: Value to set for parameter. (String value for list and bool parameters, integer for others). - required: true - example: 50268673 - selector: - text: - instance_id: - name: Instance ID - description: The OZW Instance/Controller to use. - default: 1 - selector: - number: - min: 1 - max: 255 - -clear_usercode: - name: Clear usercode - description: Clear a usercode from lock. - target: - entity: - integration: ozw - domain: lock - fields: - code_slot: - name: Code slot - description: Code slot to clear code from. - required: true - selector: - number: - min: 1 - max: 255 - -set_usercode: - name: Set usercode - description: Set a usercode to lock. - target: - entity: - integration: ozw - domain: lock - fields: - code_slot: - name: Code slot - description: Code slot to set the code. - required: true - selector: - number: - min: 1 - max: 255 - usercode: - name: Usercode - description: Code to set. - required: true - example: 1234 - selector: - text: diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json deleted file mode 100644 index ed9816c57f2..00000000000 --- a/homeassistant/components/ozw/strings.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "config": { - "step": { - "on_supervisor": { - "title": "Select connection method", - "description": "Do you want to use the OpenZWave Supervisor add-on?", - "data": { "use_addon": "Use the OpenZWave Supervisor add-on" } - }, - "install_addon": { - "title": "The OpenZWave add-on installation has started" - }, - "start_addon": { - "title": "Enter the OpenZWave add-on configuration", - "data": { - "usb_path": "[%key:common::config_flow::data::usb_path%]", - "network_key": "Network Key" - } - }, - "hassio_confirm": { - "title": "Set up OpenZWave integration with the OpenZWave add-on" - } - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "addon_info_failed": "Failed to get OpenZWave add-on info.", - "addon_install_failed": "Failed to install the OpenZWave add-on.", - "addon_set_config_failed": "Failed to set OpenZWave configuration.", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "mqtt_required": "The MQTT integration is not set up" - }, - "error": { - "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." - }, - "progress": { - "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." - } - } -} diff --git a/homeassistant/components/ozw/switch.py b/homeassistant/components/ozw/switch.py deleted file mode 100644 index 69ae1dfb379..00000000000 --- a/homeassistant/components/ozw/switch.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Representation of Z-Wave switches.""" -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_UNSUBSCRIBE, DOMAIN -from .entity import ZWaveDeviceEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave switch from config entry.""" - - @callback - def async_add_switch(value): - """Add Z-Wave Switch.""" - switch = ZWaveSwitch(value) - - async_add_entities([switch]) - - hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( - async_dispatcher_connect( - hass, f"{DOMAIN}_new_{SWITCH_DOMAIN}", async_add_switch - ) - ) - - -class ZWaveSwitch(ZWaveDeviceEntity, SwitchEntity): - """Representation of a Z-Wave switch.""" - - @property - def is_on(self): - """Return a boolean for the state of the switch.""" - return bool(self.values.primary.value) - - async def async_turn_on(self, **kwargs): - """Turn the switch on.""" - self.values.primary.send_value(True) - - async def async_turn_off(self, **kwargs): - """Turn the switch off.""" - self.values.primary.send_value(False) diff --git a/homeassistant/components/ozw/translations/bg.json b/homeassistant/components/ozw/translations/bg.json deleted file mode 100644 index 6f032fa973a..00000000000 --- a/homeassistant/components/ozw/translations/bg.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json deleted file mode 100644 index 9c9fc17e58e..00000000000 --- a/homeassistant/components/ozw/translations/ca.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement OpenZWave.", - "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement OpenZWave.", - "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 d'OpenZWave.", - "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "error": { - "addon_start_failed": "No s'ha pogut iniciar el complement OpenZWave. Comprova la configuraci\u00f3." - }, - "progress": { - "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement OpenZWave. Pot tardar uns quants minuts." - }, - "step": { - "hassio_confirm": { - "title": "Configuraci\u00f3 de la integraci\u00f3 d'OpenZWave amb el complement OpenZWave" - }, - "install_addon": { - "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement OpenZWave" - }, - "on_supervisor": { - "data": { - "use_addon": "Utilitza el complement OpenZWave Supervisor" - }, - "description": "Vols utilitzar el complement Supervisor d'OpenZWave?", - "title": "Selecciona el m\u00e8tode de connexi\u00f3" - }, - "start_addon": { - "data": { - "network_key": "Clau de xarxa", - "usb_path": "Ruta del dispositiu USB" - }, - "title": "Introdueix la configuraci\u00f3 del complement OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/cs.json b/homeassistant/components/ozw/translations/cs.json deleted file mode 100644 index d479efdf95f..00000000000 --- a/homeassistant/components/ozw/translations/cs.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Nepoda\u0159ilo se z\u00edskat informace o dopl\u0148ku OpenZWave.", - "addon_install_failed": "Instalace dopl\u0148ku OpenZWave se nezda\u0159ila.", - "addon_set_config_failed": "Nepoda\u0159ilo se nastavit OpenZWave.", - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "mqtt_required": "Integrace MQTT nen\u00ed nastavena", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "error": { - "addon_start_failed": "Spu\u0161t\u011bn\u00ed dopl\u0148ku OpenZWave se nezda\u0159ilo. Zkontrolujte konfiguraci." - }, - "step": { - "hassio_confirm": { - "title": "Nastaven\u00ed integrace OpenZWave s dopl\u0148kem OpenZWave" - }, - "install_addon": { - "title": "Instalace dopl\u0148ku OpenZWave byla zah\u00e1jena." - }, - "on_supervisor": { - "data": { - "use_addon": "Pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor" - }, - "description": "Chcete pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor?", - "title": "Vyberte metodu p\u0159ipojen\u00ed" - }, - "start_addon": { - "data": { - "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d", - "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" - }, - "title": "Zadejte konfiguraci dopl\u0148ku OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json deleted file mode 100644 index c58c55c49ad..00000000000 --- a/homeassistant/components/ozw/translations/de.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Fehler beim Abrufen von OpenZWave Add-on Informationen.", - "addon_install_failed": "Installation des OpenZWave Add-ons fehlgeschlagen.", - "addon_set_config_failed": "Setzen der OpenZWave Konfiguration fehlgeschlagen.", - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "error": { - "addon_start_failed": "Fehler beim Starten des OpenZWave Add-ons. \u00dcberpr\u00fcfe die Konfiguration." - }, - "progress": { - "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." - }, - "step": { - "hassio_confirm": { - "title": "Richte die OpenZWave Integration mit dem OpenZWave Add-On ein" - }, - "install_addon": { - "title": "Die Installation des OpenZWave-Add-On wurde gestartet" - }, - "on_supervisor": { - "data": { - "use_addon": "Verwende das OpenZWave Supervisor Add-on" - }, - "description": "M\u00f6chtest du das OpenZWave Supervisor Add-on verwenden?", - "title": "Verbindungstyp ausw\u00e4hlen" - }, - "start_addon": { - "data": { - "network_key": "Netzwerk-Schl\u00fcssel", - "usb_path": "USB-Ger\u00e4te-Pfad" - }, - "title": "Gib die Konfiguration des OpenZWave Add-ons ein" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/el.json b/homeassistant/components/ozw/translations/el.json deleted file mode 100644 index 6708cec1358..00000000000 --- a/homeassistant/components/ozw/translations/el.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03bb\u03ae\u03c8\u03b7\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave.", - "addon_install_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave.", - "addon_set_config_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd OpenZWave.", - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "mqtt_required": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 MQTT \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." - }, - "error": { - "addon_start_failed": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." - }, - "progress": { - "install_addon": "\u03a0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03b1\u03c1\u03ba\u03b5\u03c4\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac." - }, - "step": { - "hassio_confirm": { - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenZWave \u03bc\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave" - }, - "install_addon": { - "title": "\u0397 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave \u03ad\u03c7\u03b5\u03b9 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03b9" - }, - "on_supervisor": { - "data": { - "use_addon": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave Supervisor" - }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf OpenZWave Supervisor;", - "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" - }, - "start_addon": { - "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", - "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" - }, - "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json deleted file mode 100644 index 1c837e0bde5..00000000000 --- a/homeassistant/components/ozw/translations/en.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Failed to get OpenZWave add-on info.", - "addon_install_failed": "Failed to install the OpenZWave add-on.", - "addon_set_config_failed": "Failed to set OpenZWave configuration.", - "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress", - "mqtt_required": "The MQTT integration is not set up", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "error": { - "addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration." - }, - "progress": { - "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes." - }, - "step": { - "hassio_confirm": { - "title": "Set up OpenZWave integration with the OpenZWave add-on" - }, - "install_addon": { - "title": "The OpenZWave add-on installation has started" - }, - "on_supervisor": { - "data": { - "use_addon": "Use the OpenZWave Supervisor add-on" - }, - "description": "Do you want to use the OpenZWave Supervisor add-on?", - "title": "Select connection method" - }, - "start_addon": { - "data": { - "network_key": "Network Key", - "usb_path": "USB Device Path" - }, - "title": "Enter the OpenZWave add-on configuration" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json deleted file mode 100644 index f06c2896bc8..00000000000 --- a/homeassistant/components/ozw/translations/es.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento de OpenZWave.", - "addon_install_failed": "No se pudo instalar el complemento de OpenZWave.", - "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de OpenZWave.", - "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "mqtt_required": "La integraci\u00f3n de MQTT no est\u00e1 configurada", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "error": { - "addon_start_failed": "No se pudo iniciar el complemento OpenZWave. Verifica la configuraci\u00f3n." - }, - "progress": { - "install_addon": "Espera mientras finaliza la instalaci\u00f3n del complemento OpenZWave. Esto puede tardar varios minutos." - }, - "step": { - "hassio_confirm": { - "title": "Configurar la integraci\u00f3n de OpenZWave con el complemento OpenZWave" - }, - "install_addon": { - "title": "La instalaci\u00f3n del complemento OpenZWave se ha iniciado" - }, - "on_supervisor": { - "data": { - "use_addon": "Usar el complemento de supervisor de OpenZWave" - }, - "description": "\u00bfQuiere utilizar el complemento de Supervisor de OpenZWave?", - "title": "Selecciona el m\u00e9todo de conexi\u00f3n" - }, - "start_addon": { - "data": { - "network_key": "Clave de red", - "usb_path": "Ruta del dispositivo USB" - }, - "title": "Introduce la configuraci\u00f3n del complemento OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json deleted file mode 100644 index 6ddd2e7ab96..00000000000 --- a/homeassistant/components/ozw/translations/et.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave'i lisandmooduli teabe hankimine nurjus.", - "addon_install_failed": "OpenZWave'i lisandmooduli paigaldamine nurjus.", - "addon_set_config_failed": "OpenZWave'i konfiguratsiooni seadistamine eba\u00f5nnestus.", - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "already_in_progress": "Seadistamine on juba k\u00e4imas", - "mqtt_required": "MQTT sidumine pole seadistatud", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "error": { - "addon_start_failed": "OpenZWave'i lisandmooduli k\u00e4ivitamine nurjus. Kontrolli s\u00e4tteid." - }, - "progress": { - "install_addon": "Palun oota kuni OpenZWave lisandmooduli paigaldus l\u00f5peb. See v\u00f5ib v\u00f5tta mitu minutit." - }, - "step": { - "hassio_confirm": { - "title": "Seadista OpenZWave'i sidumine OpenZWave lisandmooduli abil" - }, - "install_addon": { - "title": "OpenZWave lisandmooduli paigaldamine on alanud" - }, - "on_supervisor": { - "data": { - "use_addon": "Kasuta OpenZWave Supervisori lisandmoodulit" - }, - "description": "Kas soovid kasutada OpenZWave'i halduri lisandmoodulit?", - "title": "Vali \u00fchendusviis" - }, - "start_addon": { - "data": { - "network_key": "V\u00f5rgu v\u00f5ti", - "usb_path": "USB seadme rada" - }, - "title": "Sisesta OpenZWave'i lisandmooduli seaded" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json deleted file mode 100644 index 0a0232dd91d..00000000000 --- a/homeassistant/components/ozw/translations/fr.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire OpenZWave.", - "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire OpenZWave.", - "addon_set_config_failed": "\u00c9chec de la configuration OpenZWave.", - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours\u00e0", - "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "error": { - "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire OpenZWave. V\u00e9rifiez la configuration." - }, - "progress": { - "install_addon": "Veuillez patienter pendant que l'installation du module OpenZWave se termine. Cela peut prendre plusieurs minutes." - }, - "step": { - "hassio_confirm": { - "title": "Configurer l'int\u00e9gration d'OpenZWave avec le module compl\u00e9mentaire OpenZWave" - }, - "install_addon": { - "title": "L'installation du module compl\u00e9mentaire OpenZWave a commenc\u00e9" - }, - "on_supervisor": { - "data": { - "use_addon": "Utilisez le module compl\u00e9mentaire OpenZWave du Supervisor" - }, - "description": "Voulez-vous utiliser le module compl\u00e9mentaire OpenZWave du Supervisor?", - "title": "S\u00e9lectionner la m\u00e9thode de connexion" - }, - "start_addon": { - "data": { - "network_key": "Cl\u00e9 r\u00e9seau", - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "title": "Entrez dans la configuration du module compl\u00e9mentaire OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/he.json b/homeassistant/components/ozw/translations/he.json deleted file mode 100644 index 021d34db25a..00000000000 --- a/homeassistant/components/ozw/translations/he.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "step": { - "on_supervisor": { - "data": { - "use_addon": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 '\u05de\u05e4\u05e7\u05d7 OpenZWave'" - }, - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05e9\u05dc \u05de\u05e4\u05e7\u05d7 OpenZWave?", - "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" - }, - "start_addon": { - "data": { - "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json deleted file mode 100644 index 1c1d5a9f87d..00000000000 --- a/homeassistant/components/ozw/translations/hu.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Nem siker\u00fclt leh\u00edvni az OpenZWave b\u0151v\u00edtm\u00e9ny inform\u00e1ci\u00f3kat.", - "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", - "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", - "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "error": { - "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." - }, - "progress": { - "install_addon": "V\u00e1rjon, am\u00edg az OpenZWave b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat." - }, - "step": { - "hassio_confirm": { - "title": "\u00c1ll\u00edtsa be az OpenZWave integr\u00e1ci\u00f3t az OpenZWave b\u0151v\u00edtm\u00e9nnyel" - }, - "install_addon": { - "title": "Elindult az OpenZWave b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor b\u0151v\u00edtm\u00e9ny haszn\u00e1lata" - }, - "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?", - "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" - }, - "start_addon": { - "data": { - "network_key": "H\u00e1l\u00f3zati kulcs", - "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - }, - "title": "Adja meg az OpenZWave b\u0151v\u00edtm\u00e9ny konfigur\u00e1ci\u00f3j\u00e1t" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/id.json b/homeassistant/components/ozw/translations/id.json deleted file mode 100644 index ef47e12f12d..00000000000 --- a/homeassistant/components/ozw/translations/id.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Gagal mendapatkan info add-on OpenZWave.", - "addon_install_failed": "Gagal menginstal add-on OpenZWave.", - "addon_set_config_failed": "Gagal menyetel konfigurasi OpenZWave.", - "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung", - "mqtt_required": "Integrasi MQTT belum disiapkan", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "error": { - "addon_start_failed": "Gagal memulai add-on OpenZWave. Periksa konfigurasi." - }, - "progress": { - "install_addon": "Harap tunggu hingga penginstalan add-on OpenZWave selesai. Ini bisa memakan waktu beberapa saat." - }, - "step": { - "hassio_confirm": { - "title": "Siapkan integrasi OpenZWave dengan add-on OpenZWave" - }, - "install_addon": { - "title": "Instalasi add-on OpenZWave telah dimulai" - }, - "on_supervisor": { - "data": { - "use_addon": "Gunakan add-on Supervisor OpenZWave" - }, - "description": "Ingin menggunakan add-on Supervisor OpenZWave?", - "title": "Pilih metode koneksi" - }, - "start_addon": { - "data": { - "network_key": "Kunci Jaringan", - "usb_path": "Jalur Perangkat USB" - }, - "title": "Masukkan konfigurasi add-on OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/it.json b/homeassistant/components/ozw/translations/it.json deleted file mode 100644 index ff3e0a711c5..00000000000 --- a/homeassistant/components/ozw/translations/it.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo OpenZWave.", - "addon_install_failed": "Impossibile installare il componente aggiuntivo OpenZWave.", - "addon_set_config_failed": "Impossibile impostare la configurazione di OpenZWave.", - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "mqtt_required": "L'integrazione MQTT non \u00e8 impostata", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "error": { - "addon_start_failed": "Impossibile avviare il componente aggiuntivo OpenZWave. Controlla la configurazione." - }, - "progress": { - "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo OpenZWave. Questa operazione pu\u00f2 richiedere diversi minuti." - }, - "step": { - "hassio_confirm": { - "title": "Configura l'integrazione di OpenZWave con il componente aggiuntivo OpenZWave" - }, - "install_addon": { - "title": "L'installazione del componente aggiuntivo OpenZWave \u00e8 iniziata" - }, - "on_supervisor": { - "data": { - "use_addon": "Usa il componente aggiuntivo OpenZWave Supervisor" - }, - "description": "Vuoi usare il componente aggiuntivo OpenZWave Supervisor?", - "title": "Seleziona il metodo di connessione" - }, - "start_addon": { - "data": { - "network_key": "Chiave di rete", - "usb_path": "Percorso del dispositivo USB" - }, - "title": "Accedi alla configurazione dell'add-on OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ja.json b/homeassistant/components/ozw/translations/ja.json deleted file mode 100644 index d3ef9f7d17f..00000000000 --- a/homeassistant/components/ozw/translations/ja.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave\u306e\u30a2\u30c9\u30aa\u30f3\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", - "addon_install_failed": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", - "addon_set_config_failed": "OpenZWave\u306e\u8a2d\u5b9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" - }, - "error": { - "addon_start_failed": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u8d77\u52d5\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - }, - "progress": { - "install_addon": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u5b8c\u4e86\u3059\u308b\u307e\u3067\u304a\u5f85\u3061\u304f\u3060\u3055\u3044\u3002\u3053\u308c\u306b\u306f\u6570\u5206\u304b\u304b\u308b\u5834\u5408\u304c\u3042\u308a\u307e\u3059\u3002" - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u3068OpenZWave\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" - }, - "install_addon": { - "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3059\u308b" - }, - "description": "OpenZWave Supervisor\u30a2\u30c9\u30aa\u30f3\u3092\u4f7f\u7528\u3057\u307e\u3059\u304b\uff1f", - "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" - }, - "start_addon": { - "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc", - "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "title": "OpenZWave\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3059\u308b" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ka.json b/homeassistant/components/ozw/translations/ka.json deleted file mode 100644 index da587087c92..00000000000 --- a/homeassistant/components/ozw/translations/ka.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e4\u10dd\u10e1 \u10db\u10d8\u10e6\u10d4\u10d1\u10d0.", - "addon_install_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10d0.", - "addon_set_config_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0.", - "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." - }, - "error": { - "addon_start_failed": "OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8 \u10d0\u10e0 \u10d3\u10d0\u10d8\u10e1\u10e2\u10d0\u10e0\u10e2\u10d0. \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0." - }, - "step": { - "on_supervisor": { - "data": { - "use_addon": "\u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d7 OpenZWave Supervisor \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8" - }, - "description": "\u10d2\u10e1\u10e3\u10e0\u10d7 \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10dd\u10d7 OpenZWave Supervisor-\u10d8\u10e1 \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8?", - "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8" - }, - "start_addon": { - "data": { - "network_key": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d0\u10e6\u10d4\u10d1\u10d8", - "usb_path": "USB \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d6\u10d0" - }, - "title": "\u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10d4\u10d7 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ko.json b/homeassistant/components/ozw/translations/ko.json deleted file mode 100644 index f6dddf5c96a..00000000000 --- a/homeassistant/components/ozw/translations/ko.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "addon_install_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "addon_set_config_failed": "OpenZWave \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", - "mqtt_required": "MQTT \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "error": { - "addon_start_failed": "OpenZWave \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694." - }, - "progress": { - "install_addon": "Openzwave \uc560\ub4dc\uc628\uc758 \uc124\uce58\uac00 \uc644\ub8cc\ub418\ub294 \ub3d9\uc548 \uc7a0\uc2dc \uae30\ub2e4\ub824\uc8fc\uc138\uc694. \uba87 \ubd84 \uc815\ub3c4 \uac78\ub9b4 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave \uc560\ub4dc\uc628\uc73c\ub85c OpenZWave \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30" - }, - "install_addon": { - "title": "Openzwave \uc560\ub4dc\uc628 \uc124\uce58\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uae30" - }, - "description": "OpenZWave Supervisor \uc560\ub4dc\uc628\uc744 \uc0ac\uc6a9\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uc5f0\uacb0 \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" - }, - "start_addon": { - "data": { - "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4", - "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - }, - "title": "OpenZWave \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json deleted file mode 100644 index 33de9a44953..00000000000 --- a/homeassistant/components/ozw/translations/lb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", - "mqtt_required": "MQTT Integratioun ass net ageriicht", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." - }, - "step": { - "start_addon": { - "data": { - "network_key": "Netzwierk Schl\u00ebssel" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/nl.json b/homeassistant/components/ozw/translations/nl.json deleted file mode 100644 index 7392f9b63eb..00000000000 --- a/homeassistant/components/ozw/translations/nl.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Mislukt om OpenZWave add-on info te krijgen.", - "addon_install_failed": "De installatie van de OpenZWave add-on is mislukt.", - "addon_set_config_failed": "Mislukt om OpenZWave configuratie in te stellen.", - "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "mqtt_required": "De MQTT-integratie is niet ingesteld", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." - }, - "error": { - "addon_start_failed": "Het starten van de OpenZWave-add-on is mislukt. Controleer de configuratie." - }, - "progress": { - "install_addon": "Wacht even terwijl de installatie van de OpenZWave add-on wordt voltooid. Dit kan enkele minuten duren." - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave integratie instellen met de OpenZWave add-on" - }, - "install_addon": { - "title": "De OpenZWave add-on installatie is gestart" - }, - "on_supervisor": { - "data": { - "use_addon": "Gebruik de OpenZWave Supervisor add-on" - }, - "description": "Wilt u de OpenZWave Supervisor add-on gebruiken?", - "title": "Selecteer een verbindingsmethode" - }, - "start_addon": { - "data": { - "network_key": "Netwerksleutel", - "usb_path": "USB-apparaatpad" - }, - "title": "Voer de OpenZWave add-on configuratie in" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json deleted file mode 100644 index 652e28fe3fc..00000000000 --- a/homeassistant/components/ozw/translations/no.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Kunne ikke hente informasjon om OpenZWave-tillegg", - "addon_install_failed": "Kunne ikke installere OpenZWave-tillegg", - "addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon", - "already_configured": "Enheten er allerede konfigurert", - "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "mqtt_required": "MQTT-integrasjonen er ikke satt opp", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "error": { - "addon_start_failed": "Kunne ikke starte OpenZWave-tillegg. Sjekk konfigurasjonen." - }, - "progress": { - "install_addon": "Vent mens installasjonen av OpenZWave-tillegg er ferdig. Dette kan ta flere minutter." - }, - "step": { - "hassio_confirm": { - "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegg" - }, - "install_addon": { - "title": "Installasjonen av OpenZWave-tillegg har startet" - }, - "on_supervisor": { - "data": { - "use_addon": "Bruk OpenZWave Supervisor-tillegg" - }, - "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegg?", - "title": "Velg tilkoblingsmetode" - }, - "start_addon": { - "data": { - "network_key": "Nettverksn\u00f8kkel", - "usb_path": "USB enhetsbane" - }, - "title": "Angi konfigurasjon for OpenZWave-tillegg" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json deleted file mode 100644 index c9fd17c59bd..00000000000 --- a/homeassistant/components/ozw/translations/pl.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Nie uda\u0142o si\u0119 pobra\u0107 informacji o dodatku OpenZWave", - "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku OpenZWave", - "addon_set_config_failed": "Nie uda\u0142o si\u0119 ustawi\u0107 konfiguracji OpenZWave", - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "mqtt_required": "Integracja MQTT nie jest skonfigurowana", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "error": { - "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku OpenZWave. Sprawd\u017a konfiguracj\u0119." - }, - "progress": { - "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku OpenZWave. Mo\u017ce to potrwa\u0107 kilka minut." - }, - "step": { - "hassio_confirm": { - "title": "Konfiguracja integracji OpenZWave z dodatkiem OpenZWave" - }, - "install_addon": { - "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku OpenZWave" - }, - "on_supervisor": { - "data": { - "use_addon": "U\u017cyj dodatku OpenZWave Supervisor" - }, - "description": "Czy chcesz u\u017cy\u0107 dodatku OpenZWave Supervisor?", - "title": "Wybierz metod\u0119 po\u0142\u0105czenia" - }, - "start_addon": { - "data": { - "network_key": "Klucz sieci", - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "title": "Wprowad\u017a konfiguracj\u0119 dodatku OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pt-BR.json b/homeassistant/components/ozw/translations/pt-BR.json deleted file mode 100644 index 8ec256d1d75..00000000000 --- a/homeassistant/components/ozw/translations/pt-BR.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "Falha ao obter informa\u00e7\u00f5es do add-on OpenZWave.", - "addon_install_failed": "Falha ao instalar o add-on OpenZWave.", - "addon_set_config_failed": "Falha ao definir a configura\u00e7\u00e3o do OpenZWave.", - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "addon_start_failed": "Falha ao iniciar o add-on OpenZWave. Verifique a configura\u00e7\u00e3o." - }, - "progress": { - "install_addon": "Aguarde enquanto a instala\u00e7\u00e3o do add-on OpenZWave termina. Isso pode levar v\u00e1rios minutos." - }, - "step": { - "hassio_confirm": { - "title": "Configure a integra\u00e7\u00e3o do OpenZWave com o add-on OpenZWave" - }, - "install_addon": { - "title": "A instala\u00e7\u00e3o do add-on OpenZWave foi iniciada" - }, - "on_supervisor": { - "data": { - "use_addon": "Use o add-on OpenZWave Supervisor" - }, - "description": "Deseja usar o add-on OpenZWave Supervisor?", - "title": "Selecione o m\u00e9todo de conex\u00e3o" - }, - "start_addon": { - "data": { - "network_key": "Chave de rede", - "usb_path": "Caminho do Dispositivo USB" - }, - "title": "Digite a configura\u00e7\u00e3o do add-on OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/pt.json b/homeassistant/components/ozw/translations/pt.json deleted file mode 100644 index 75d85097874..00000000000 --- a/homeassistant/components/ozw/translations/pt.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "step": { - "start_addon": { - "data": { - "usb_path": "Caminho do Dispositivo USB" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json deleted file mode 100644 index 07dc84eae07..00000000000 --- a/homeassistant/components/ozw/translations/ru.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 OpenZWave.", - "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c OpenZWave.", - "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e OpenZWave.", - "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.", - "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", - "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." - }, - "error": { - "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenZWave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." - }, - "progress": { - "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." - }, - "step": { - "hassio_confirm": { - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" - }, - "install_addon": { - "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" - }, - "on_supervisor": { - "data": { - "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor OpenZWave" - }, - "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor OpenZWave?", - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" - }, - "start_addon": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", - "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sk.json b/homeassistant/components/ozw/translations/sk.json deleted file mode 100644 index bee0999420f..00000000000 --- a/homeassistant/components/ozw/translations/sk.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json deleted file mode 100644 index 8da77910c38..00000000000 --- a/homeassistant/components/ozw/translations/sl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Naprava je \u017ee name\u0161\u010dena", - "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", - "mqtt_required": "Integracija MQTT ni nastavljena" - }, - "progress": { - "install_addon": "Po\u010dakajte, da se namestitev dodatka OpenZWave zaklju\u010di. To lahko traja ve\u010d minut." - }, - "step": { - "hassio_confirm": { - "title": "Namestite OpenZWave integracijo z OpenZWave dodatkom." - }, - "install_addon": { - "title": "Namestitev dodatka OpenZWave se je za\u010dela" - }, - "on_supervisor": { - "title": "Izberite na\u010din povezave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json deleted file mode 100644 index e9e8643fa94..00000000000 --- a/homeassistant/components/ozw/translations/tr.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.", - "addon_install_failed": "OpenZWave eklentisi y\u00fcklenemedi.", - "addon_set_config_failed": "OpenZWave yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "error": { - "addon_start_failed": "OpenZWave eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin." - }, - "progress": { - "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." - }, - "step": { - "hassio_confirm": { - "title": "OpenZWave eklentisi ile OpenZWave entegrasyonunu kurun" - }, - "install_addon": { - "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" - }, - "on_supervisor": { - "data": { - "use_addon": "OpenZWave Supervisor eklentisini kullan\u0131n" - }, - "description": "OpenZWave Supervisor eklentisini kullanmak istiyor musunuz?", - "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" - }, - "start_addon": { - "data": { - "network_key": "A\u011f Anahtar\u0131", - "usb_path": "USB Cihaz Yolu" - }, - "title": "OpenZWave eklenti yap\u0131land\u0131rmas\u0131n\u0131 girin" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/uk.json b/homeassistant/components/ozw/translations/uk.json deleted file mode 100644 index f662bc978ae..00000000000 --- a/homeassistant/components/ozw/translations/uk.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave.", - "addon_install_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 OpenZWave.", - "addon_set_config_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e OpenZWave.", - "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", - "mqtt_required": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MQTT \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0430.", - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "error": { - "addon_start_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 OpenZWave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." - }, - "progress": { - "install_addon": "\u0417\u0430\u0447\u0435\u043a\u0430\u0439\u0442\u0435, \u043f\u043e\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0437\u0430\u0439\u043d\u044f\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 \u0445\u0432\u0438\u043b\u0438\u043d." - }, - "step": { - "hassio_confirm": { - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave" - }, - "install_addon": { - "title": "\u0420\u043e\u0437\u043f\u043e\u0447\u0430\u0442\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f Open'Wave" - }, - "on_supervisor": { - "data": { - "use_addon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave" - }, - "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave?", - "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" - }, - "start_addon": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456", - "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" - }, - "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 Open'Wave" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hans.json b/homeassistant/components/ozw/translations/zh-Hans.json deleted file mode 100644 index cf0c8771863..00000000000 --- a/homeassistant/components/ozw/translations/zh-Hans.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "mqtt_required": "\u672a\u8bbe\u7f6e MQTT \u96c6\u6210" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json deleted file mode 100644 index 2208ce32c8f..00000000000 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "config": { - "abort": { - "addon_info_failed": "\u53d6\u5f97 OpenZWave \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", - "addon_install_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", - "addon_set_config_failed": "OpenZWave a\u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "error": { - "addon_start_failed": "OpenZWave \u9644\u52a0\u5143\u4ef6\u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" - }, - "progress": { - "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" - }, - "step": { - "hassio_confirm": { - "title": "\u4ee5 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a OpenZwave \u6574\u5408" - }, - "install_addon": { - "title": "OpenZWave \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5df2\u555f\u52d5" - }, - "on_supervisor": { - "data": { - "use_addon": "\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6" - }, - "description": "\u662f\u5426\u8981\u4f7f\u7528 OpenZWave Supervisor \u9644\u52a0\u5143\u4ef6\uff1f", - "title": "\u9078\u64c7\u9023\u7dda\u985e\u5225" - }, - "start_addon": { - "data": { - "network_key": "\u7db2\u8def\u91d1\u9470", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" - }, - "title": "\u8acb\u8f38\u5165 OpenZWave \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u3002" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py deleted file mode 100644 index 45c0a113841..00000000000 --- a/homeassistant/components/ozw/websocket_api.py +++ /dev/null @@ -1,493 +0,0 @@ -"""Web socket API for OpenZWave.""" -from openzwavemqtt.const import ( - ATTR_CODE_SLOT, - ATTR_LABEL, - ATTR_POSITION, - ATTR_VALUE, - EVENT_NODE_ADDED, - EVENT_NODE_CHANGED, -) -from openzwavemqtt.exceptions import NotFoundError, NotSupportedError -from openzwavemqtt.util.lock import clear_usercode, get_code_slots, set_usercode -from openzwavemqtt.util.node import ( - get_config_parameters, - get_node_from_manager, - set_config_parameter, -) -import voluptuous as vol -import voluptuous_serialize - -from homeassistant.components import websocket_api -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv - -from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER -from .lock import ATTR_USERCODE - -DRY_RUN = "dry_run" -TYPE = "type" -ID = "id" -OZW_INSTANCE = "ozw_instance" -NODE_ID = "node_id" -PARAMETER = ATTR_CONFIG_PARAMETER -VALUE = ATTR_CONFIG_VALUE -SCHEMA = "schema" - -ATTR_NODE_QUERY_STAGE = "node_query_stage" -ATTR_IS_ZWAVE_PLUS = "is_zwave_plus" -ATTR_IS_AWAKE = "is_awake" -ATTR_IS_FAILED = "is_failed" -ATTR_NODE_BAUD_RATE = "node_baud_rate" -ATTR_IS_BEAMING = "is_beaming" -ATTR_IS_FLIRS = "is_flirs" -ATTR_IS_ROUTING = "is_routing" -ATTR_IS_SECURITYV1 = "is_securityv1" -ATTR_NODE_BASIC_STRING = "node_basic_string" -ATTR_NODE_GENERIC_STRING = "node_generic_string" -ATTR_NODE_SPECIFIC_STRING = "node_specific_string" -ATTR_NODE_MANUFACTURER_NAME = "node_manufacturer_name" -ATTR_NODE_PRODUCT_NAME = "node_product_name" -ATTR_NEIGHBORS = "neighbors" - - -@callback -def async_register_api(hass): - """Register all of our api endpoints.""" - websocket_api.async_register_command(hass, websocket_get_instances) - websocket_api.async_register_command(hass, websocket_get_nodes) - websocket_api.async_register_command(hass, websocket_network_status) - websocket_api.async_register_command(hass, websocket_network_statistics) - websocket_api.async_register_command(hass, websocket_node_metadata) - websocket_api.async_register_command(hass, websocket_node_status) - websocket_api.async_register_command(hass, websocket_node_statistics) - websocket_api.async_register_command(hass, websocket_refresh_node_info) - websocket_api.async_register_command(hass, websocket_get_config_parameters) - websocket_api.async_register_command(hass, websocket_set_config_parameter) - websocket_api.async_register_command(hass, websocket_set_usercode) - websocket_api.async_register_command(hass, websocket_clear_usercode) - websocket_api.async_register_command(hass, websocket_get_code_slots) - - -def _call_util_function(hass, connection, msg, send_result, function, *args): - """Call an openzwavemqtt.util function.""" - try: - node = get_node_from_manager( - hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID] - ) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - - try: - payload = function(node, *args) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - except NotSupportedError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_SUPPORTED, - err.args[0], - ) - return - - if send_result: - connection.send_result( - msg[ID], - payload, - ) - return - - connection.send_result(msg[ID]) - - -def _get_config_params(node, *args): - raw_values = get_config_parameters(node) - config_params = [] - - for param in raw_values: - schema = {} - - if param["type"] in ("Byte", "Int", "Short"): - schema = vol.Schema( - { - vol.Required(param["label"], default=param["value"]): vol.All( - vol.Coerce(int), vol.Range(min=param["min"], max=param["max"]) - ) - } - ) - data = {param["label"]: param["value"]} - - if param["type"] == "List": - - for options in param["options"]: - if options["Label"] == param["value"]: - selected = options - break - - schema = vol.Schema( - { - vol.Required(param["label"],): vol.In( - { - option["Value"]: option["Label"] - for option in param["options"] - } - ) - } - ) - data = {param["label"]: selected["Value"]} - - config_params.append( - { - "type": param["type"], - "label": param["label"], - "parameter": param["parameter"], - "help": param["help"], - "value": param["value"], - "schema": voluptuous_serialize.convert( - schema, custom_serializer=cv.custom_serializer - ), - "data": data, - } - ) - - return config_params - - -@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"}) -def websocket_get_instances(hass, connection, msg): - """Get a list of OZW instances.""" - manager = hass.data[DOMAIN][MANAGER] - instances = [] - - for instance in manager.collections["instance"]: - instances.append(dict(instance.get_status().data, ozw_instance=instance.id)) - - connection.send_result( - msg[ID], - instances, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/get_nodes", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_get_nodes(hass, connection, msg): - """Get a list of nodes for an OZW instance.""" - manager = hass.data[DOMAIN][MANAGER] - nodes = [] - - for node in manager.get_instance(msg[OZW_INSTANCE]).collections["node"]: - nodes.append( - { - ATTR_NODE_QUERY_STAGE: node.node_query_stage, - NODE_ID: node.node_id, - ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus, - ATTR_IS_AWAKE: node.is_awake, - ATTR_IS_FAILED: node.is_failed, - ATTR_NODE_BAUD_RATE: node.node_baud_rate, - ATTR_IS_BEAMING: node.is_beaming, - ATTR_IS_FLIRS: node.is_flirs, - ATTR_IS_ROUTING: node.is_routing, - ATTR_IS_SECURITYV1: node.is_securityv1, - ATTR_NODE_BASIC_STRING: node.node_basic_string, - ATTR_NODE_GENERIC_STRING: node.node_generic_string, - ATTR_NODE_SPECIFIC_STRING: node.node_specific_string, - ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name, - ATTR_NODE_PRODUCT_NAME: node.node_product_name, - ATTR_NEIGHBORS: node.neighbors, - OZW_INSTANCE: msg[OZW_INSTANCE], - } - ) - - connection.send_result( - msg[ID], - nodes, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/set_usercode", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - } -) -def websocket_set_usercode(hass, connection, msg): - """Set a usercode to a node code slot.""" - _call_util_function( - hass, connection, msg, False, set_usercode, msg[ATTR_CODE_SLOT], ATTR_USERCODE - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/clear_usercode", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - } -) -def websocket_clear_usercode(hass, connection, msg): - """Clear a node code slot.""" - _call_util_function( - hass, connection, msg, False, clear_usercode, msg[ATTR_CODE_SLOT] - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/get_code_slots", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_get_code_slots(hass, connection, msg): - """Get status of node's code slots.""" - _call_util_function(hass, connection, msg, True, get_code_slots) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/get_config_parameters", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_get_config_parameters(hass, connection, msg): - """Get a list of configuration parameters for an OZW node instance.""" - _call_util_function(hass, connection, msg, True, _get_config_params) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/set_config_parameter", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(PARAMETER): vol.Coerce(int), - vol.Required(VALUE): vol.Any( - vol.All( - cv.ensure_list, - [ - vol.All( - { - vol.Exclusive(ATTR_LABEL, "bit"): cv.string, - vol.Exclusive(ATTR_POSITION, "bit"): vol.Coerce(int), - vol.Required(ATTR_VALUE): bool, - }, - cv.has_at_least_one_key(ATTR_LABEL, ATTR_POSITION), - ) - ], - ), - vol.Coerce(int), - bool, - cv.string, - ), - } -) -def websocket_set_config_parameter(hass, connection, msg): - """Set a config parameter to a node.""" - _call_util_function( - hass, connection, msg, True, set_config_parameter, msg[PARAMETER], msg[VALUE] - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/network_status", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_network_status(hass, connection, msg): - """Get Z-Wave network status.""" - - manager = hass.data[DOMAIN][MANAGER] - status = manager.get_instance(msg[OZW_INSTANCE]).get_status().data - connection.send_result( - msg[ID], - dict(status, ozw_instance=msg[OZW_INSTANCE]), - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/network_statistics", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_network_statistics(hass, connection, msg): - """Get Z-Wave network statistics.""" - - manager = hass.data[DOMAIN][MANAGER] - statistics = manager.get_instance(msg[OZW_INSTANCE]).get_statistics().data - node_count = len( - manager.get_instance(msg[OZW_INSTANCE]).collections["node"].collection - ) - connection.send_result( - msg[ID], - dict(statistics, ozw_instance=msg[OZW_INSTANCE], node_count=node_count), - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_status", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_node_status(hass, connection, msg): - """Get the status for a Z-Wave node.""" - try: - node = get_node_from_manager( - hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID] - ) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - - connection.send_result( - msg[ID], - { - ATTR_NODE_QUERY_STAGE: node.node_query_stage, - NODE_ID: node.node_id, - ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus, - ATTR_IS_AWAKE: node.is_awake, - ATTR_IS_FAILED: node.is_failed, - ATTR_NODE_BAUD_RATE: node.node_baud_rate, - ATTR_IS_BEAMING: node.is_beaming, - ATTR_IS_FLIRS: node.is_flirs, - ATTR_IS_ROUTING: node.is_routing, - ATTR_IS_SECURITYV1: node.is_securityv1, - ATTR_NODE_BASIC_STRING: node.node_basic_string, - ATTR_NODE_GENERIC_STRING: node.node_generic_string, - ATTR_NODE_SPECIFIC_STRING: node.node_specific_string, - ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name, - ATTR_NODE_PRODUCT_NAME: node.node_product_name, - ATTR_NEIGHBORS: node.neighbors, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_metadata", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_node_metadata(hass, connection, msg): - """Get the metadata for a Z-Wave node.""" - try: - node = get_node_from_manager( - hass.data[DOMAIN][MANAGER], msg[OZW_INSTANCE], msg[NODE_ID] - ) - except NotFoundError as err: - connection.send_error( - msg[ID], - websocket_api.const.ERR_NOT_FOUND, - err.args[0], - ) - return - - connection.send_result( - msg[ID], - { - "metadata": node.meta_data, - NODE_ID: node.node_id, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_statistics", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } -) -def websocket_node_statistics(hass, connection, msg): - """Get the statistics for a Z-Wave node.""" - manager = hass.data[DOMAIN][MANAGER] - stats = ( - manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]).get_statistics() - ) - connection.send_result( - msg[ID], - { - NODE_ID: msg[NODE_ID], - "send_count": stats.send_count, - "sent_failed": stats.sent_failed, - "retries": stats.retries, - "last_request_rtt": stats.last_request_rtt, - "last_response_rtt": stats.last_response_rtt, - "average_request_rtt": stats.average_request_rtt, - "average_response_rtt": stats.average_response_rtt, - "received_packets": stats.received_packets, - "received_dup_packets": stats.received_dup_packets, - "received_unsolicited": stats.received_unsolicited, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/refresh_node_info", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - vol.Required(NODE_ID): vol.Coerce(int), - } -) -def websocket_refresh_node_info(hass, connection, msg): - """Tell OpenZWave to re-interview a node.""" - - manager = hass.data[DOMAIN][MANAGER] - options = manager.options - - @callback - def forward_node(node): - """Forward node events to websocket.""" - if node.node_id != msg[NODE_ID]: - return - - forward_data = { - "type": "node_updated", - ATTR_NODE_QUERY_STAGE: node.node_query_stage, - } - connection.send_message(websocket_api.event_message(msg["id"], forward_data)) - - @callback - def async_cleanup() -> None: - """Remove signal listeners.""" - for unsub in unsubs: - unsub() - - connection.subscriptions[msg["id"]] = async_cleanup - unsubs = [ - options.listen(EVENT_NODE_CHANGED, forward_node), - options.listen(EVENT_NODE_ADDED, forward_node), - ] - - instance = manager.get_instance(msg[OZW_INSTANCE]) - instance.refresh_node(msg[NODE_ID]) - connection.send_result(msg["id"]) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b48df75c189..99e5cf7caf3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -245,7 +245,6 @@ FLOWS = [ "overkiz", "ovo_energy", "owntracks", - "ozw", "p1_monitor", "panasonic_viera", "philips_js", diff --git a/mypy.ini b/mypy.ini index 8beb6183d01..bf98c7dbcc6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2612,15 +2612,6 @@ ignore_errors = true [mypy-homeassistant.components.onvif.sensor] ignore_errors = true -[mypy-homeassistant.components.ozw] -ignore_errors = true - -[mypy-homeassistant.components.ozw.climate] -ignore_errors = true - -[mypy-homeassistant.components.ozw.entity] -ignore_errors = true - [mypy-homeassistant.components.philips_js] ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index 58eb464e382..17c4452ff00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1905,9 +1905,6 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.2.0 -# homeassistant.components.ozw -python-openzwave-mqtt[mqtt-client]==1.4.0 - # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cbb9bccff8..8f39d58d637 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1226,9 +1226,6 @@ python-miio==0.5.11 # homeassistant.components.nest python-nest==4.2.0 -# homeassistant.components.ozw -python-openzwave-mqtt[mqtt-client]==1.4.0 - # homeassistant.components.picnic python-picnic-api==1.1.0 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9882c016597..7e739cfb7d2 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -112,9 +112,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.models", "homeassistant.components.onvif.parsers", "homeassistant.components.onvif.sensor", - "homeassistant.components.ozw", - "homeassistant.components.ozw.climate", - "homeassistant.components.ozw.entity", "homeassistant.components.philips_js", "homeassistant.components.philips_js.config_flow", "homeassistant.components.philips_js.device_trigger", diff --git a/tests/components/ozw/__init__.py b/tests/components/ozw/__init__.py deleted file mode 100644 index ce419b9f55b..00000000000 --- a/tests/components/ozw/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the OZW integration.""" diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py deleted file mode 100644 index 450066c5aed..00000000000 --- a/tests/components/ozw/common.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Helpers for tests.""" -import json -from unittest.mock import Mock, patch - -from homeassistant import config_entries -from homeassistant.components.ozw.const import DOMAIN - -from tests.common import MockConfigEntry - - -async def setup_ozw(hass, entry=None, fixture=None): - """Set up OZW and load a dump.""" - mqtt_entry = MockConfigEntry( - domain="mqtt", state=config_entries.ConfigEntryState.LOADED - ) - mqtt_entry.add_to_hass(hass) - - if entry is None: - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - ) - - entry.add_to_hass(hass) - - with patch("homeassistant.components.mqtt.async_subscribe") as mock_subscribe: - mock_subscribe.return_value = Mock() - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert "ozw" in hass.config.components - assert len(mock_subscribe.mock_calls) == 1 - receive_message = mock_subscribe.mock_calls[0][1][2] - - if fixture is not None: - for line in fixture.split("\n"): - line = line.strip() - if not line: - continue - topic, payload = line.split(",", 1) - receive_message(Mock(topic=topic, payload=payload)) - - await hass.async_block_till_done() - - return receive_message - - -class MQTTMessage: - """Represent a mock MQTT message.""" - - def __init__(self, topic, payload): - """Set up message.""" - self.topic = topic - self.payload = payload - - def decode(self): - """Decode message payload from a string to a json dict.""" - self.payload = json.loads(self.payload) - - def encode(self): - """Encode message payload into a string.""" - self.payload = json.dumps(self.payload) diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py deleted file mode 100644 index d09259654de..00000000000 --- a/tests/components/ozw/conftest.py +++ /dev/null @@ -1,280 +0,0 @@ -"""Helpers for tests.""" -import json -from unittest.mock import patch - -import pytest - -from homeassistant.config_entries import ConfigEntryState - -from .common import MQTTMessage - -from tests.common import MockConfigEntry, load_fixture -from tests.components.light.conftest import mock_light_profiles # noqa: F401 - - -@pytest.fixture(name="generic_data", scope="session") -def generic_data_fixture(): - """Load generic MQTT data and return it.""" - return load_fixture("ozw/generic_network_dump.csv") - - -@pytest.fixture(name="migration_data", scope="session") -def migration_data_fixture(): - """Load migration MQTT data and return it.""" - return load_fixture("ozw/migration_fixture.csv") - - -@pytest.fixture(name="fan_data", scope="session") -def fan_data_fixture(): - """Load fan MQTT data and return it.""" - return load_fixture("ozw/fan_network_dump.csv") - - -@pytest.fixture(name="light_data", scope="session") -def light_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_network_dump.csv") - - -@pytest.fixture(name="light_new_ozw_data", scope="session") -def light_new_ozw_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_new_ozw_network_dump.csv") - - -@pytest.fixture(name="light_no_ww_data", scope="session") -def light_no_ww_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_no_ww_network_dump.csv") - - -@pytest.fixture(name="light_no_cw_data", scope="session") -def light_no_cw_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_no_cw_network_dump.csv") - - -@pytest.fixture(name="light_wc_data", scope="session") -def light_wc_only_data_fixture(): - """Load light dimmer MQTT data and return it.""" - return load_fixture("ozw/light_wc_network_dump.csv") - - -@pytest.fixture(name="cover_data", scope="session") -def cover_data_fixture(): - """Load cover MQTT data and return it.""" - return load_fixture("ozw/cover_network_dump.csv") - - -@pytest.fixture(name="cover_gdo_data", scope="session") -def cover_gdo_data_fixture(): - """Load cover_gdo MQTT data and return it.""" - return load_fixture("ozw/cover_gdo_network_dump.csv") - - -@pytest.fixture(name="climate_data", scope="session") -def climate_data_fixture(): - """Load climate MQTT data and return it.""" - return load_fixture("ozw/climate_network_dump.csv") - - -@pytest.fixture(name="lock_data", scope="session") -def lock_data_fixture(): - """Load lock MQTT data and return it.""" - return load_fixture("ozw/lock_network_dump.csv") - - -@pytest.fixture(name="string_sensor_data", scope="session") -def string_sensor_fixture(): - """Load string sensor MQTT data and return it.""" - return load_fixture("ozw/sensor_string_value_network_dump.csv") - - -@pytest.fixture(name="sent_messages") -def sent_messages_fixture(): - """Fixture to capture sent messages.""" - sent_messages = [] - - with patch( - "homeassistant.components.mqtt.async_publish", - side_effect=lambda hass, topic, payload: sent_messages.append( - {"topic": topic, "payload": json.loads(payload)} - ), - ): - yield sent_messages - - -@pytest.fixture(name="fan_msg") -async def fan_msg_fixture(hass): - """Return a mock MQTT msg with a fan actuator message.""" - fan_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/fan.json") - ) - message = MQTTMessage(topic=fan_json["topic"], payload=fan_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_msg") -async def light_msg_fixture(hass): - """Return a mock MQTT msg with a light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_no_rgb_msg") -async def light_no_rgb_msg_fixture(hass): - """Return a mock MQTT msg with a light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light_no_rgb.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_rgb_msg") -async def light_rgb_msg_fixture(hass): - """Return a mock MQTT msg with a light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light_rgb.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="light_pure_rgb_msg") -async def light_pure_rgb_msg_fixture(hass): - """Return a mock MQTT msg with a pure rgb light actuator message.""" - light_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/light_pure_rgb.json") - ) - message = MQTTMessage(topic=light_json["topic"], payload=light_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="switch_msg") -async def switch_msg_fixture(hass): - """Return a mock MQTT msg with a switch actuator message.""" - switch_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/switch.json") - ) - message = MQTTMessage(topic=switch_json["topic"], payload=switch_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="sensor_msg") -async def sensor_msg_fixture(hass): - """Return a mock MQTT msg with a sensor change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/sensor.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="binary_sensor_msg") -async def binary_sensor_msg_fixture(hass): - """Return a mock MQTT msg with a binary_sensor change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/binary_sensor.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="binary_sensor_alt_msg") -async def binary_sensor_alt_msg_fixture(hass): - """Return a mock MQTT msg with a binary_sensor change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/binary_sensor_alt.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="cover_msg") -async def cover_msg_fixture(hass): - """Return a mock MQTT msg with a cover level change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/cover.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="cover_gdo_msg") -async def cover_gdo_msg_fixture(hass): - """Return a mock MQTT msg with a cover barrier state change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/cover_gdo.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="climate_msg") -async def climate_msg_fixture(hass): - """Return a mock MQTT msg with a climate mode change message.""" - sensor_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/climate.json") - ) - message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="lock_msg") -async def lock_msg_fixture(hass): - """Return a mock MQTT msg with a lock actuator message.""" - lock_json = json.loads( - await hass.async_add_executor_job(load_fixture, "ozw/lock.json") - ) - message = MQTTMessage(topic=lock_json["topic"], payload=lock_json["payload"]) - message.encode() - return message - - -@pytest.fixture(name="stop_addon") -def mock_install_addon(): - """Mock stop add-on.""" - with patch("homeassistant.components.hassio.async_stop_addon") as stop_addon: - yield stop_addon - - -@pytest.fixture(name="uninstall_addon") -def mock_uninstall_addon(): - """Mock uninstall add-on.""" - with patch( - "homeassistant.components.hassio.async_uninstall_addon" - ) as uninstall_addon: - yield uninstall_addon - - -@pytest.fixture(name="get_addon_discovery_info") -def mock_get_addon_discovery_info(): - """Mock get add-on discovery info.""" - with patch( - "homeassistant.components.hassio.async_get_addon_discovery_info" - ) as get_addon_discovery_info: - yield get_addon_discovery_info - - -@pytest.fixture(name="mqtt") -async def mock_mqtt_fixture(hass): - """Mock the MQTT integration.""" - mqtt_entry = MockConfigEntry(domain="mqtt", state=ConfigEntryState.LOADED) - mqtt_entry.add_to_hass(hass) - return mqtt_entry diff --git a/tests/components/ozw/fixtures/binary_sensor.json b/tests/components/ozw/fixtures/binary_sensor.json deleted file mode 100644 index 4d6317827d1..00000000000 --- a/tests/components/ozw/fixtures/binary_sensor.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "topic": "OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/", - "payload": { - "Label": "Home Security", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Clear" - }, - { - "Value": 8, - "Label": "Motion Detected at Unknown Location" - } - ], - "Selected": "Motion Detected at Unknown Location", - "Selected_id": 8 - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_NOTIFICATION", - "Index": 7, - "Node": 37, - "Genre": "User", - "Help": "Home Security Alerts", - "ValueIDKey": 1970325463777300, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/binary_sensor_alt.json b/tests/components/ozw/fixtures/binary_sensor_alt.json deleted file mode 100644 index 187028843ff..00000000000 --- a/tests/components/ozw/fixtures/binary_sensor_alt.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/", - "payload": { - "Label": "Sensor", - "Value": true, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "Bool", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", - "Index": 0, - "Node": 37, - "Genre": "User", - "Help": "Binary Sensor State", - "ValueIDKey": 625737744, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/climate.json b/tests/components/ozw/fixtures/climate.json deleted file mode 100644 index 652dc9aef26..00000000000 --- a/tests/components/ozw/fixtures/climate.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "topic": "OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/", - "payload": { - "Label": "Mode", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Off" - }, - { - "Value": 1, - "Label": "Heat" - }, - { - "Value": 2, - "Label": "Cool" - }, - { - "Value": 3, - "Label": "Auto" - }, - { - "Value": 11, - "Label": "Heat Econ" - }, - { - "Value": 12, - "Label": "Cool Econ" - } - ], - "Selected": "Auto", - "Selected_id": 3 - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", - "Index": 0, - "Node": 7, - "Genre": "User", - "Help": "Set the Thermostat Mode", - "ValueIDKey": 122683412, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1588264894 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/climate_network_dump.csv b/tests/components/ozw/fixtures/climate_network_dump.csv deleted file mode 100644 index 99cef9091c5..00000000000 --- a/tests/components/ozw/fixtures/climate_network_dump.csv +++ /dev/null @@ -1,208 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": false, "isRouting": false, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "", "ZWAProductURL": "", "ProductPic": "", "Description": "", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "", "ProductPicBase64": "" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588264908, "NodeManufacturerName": "2GIG Technologies", "NodeProductName": "CT32 Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0098", "NodeProductType": "0x2002", "NodeProductID": "0x0100", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 2 ]} -OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Temperature Reporting Threshold", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "0.5F" }, { "Value": 2, "Label": "1.0F" }, { "Value": 3, "Label": "1.5F" }, { "Value": 4, "Label": "2.0F" } ], "Selected": "1.0F", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 4, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "The Temperature Reporting Threshold Configuration Set Command sets the reporting threshold for changes in the ambient temperature as detected by the thermostat.", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/562950081085460/,{ "Label": "HVAC Settings", "Value": { "List": [ { "Value": 17891585, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 34668801, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 18940161, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 35717377, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 17957121, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 34734337, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 19005697, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 35782913, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 17891841, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 34669057, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 18940417, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 35717633, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 17957377, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 34734593, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 19005953, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 35783169, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 17891586, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 34668802, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 18940162, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 35717378, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 17957122, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 34734338, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 19005698, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 35782914, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 17891842, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 34669058, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 18940418, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 35717634, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 17957378, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 34734594, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 19005954, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 35783170, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" } ], "Selected": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1", "Selected_id": 17891585 }, "Units": "", "Min": 0, "Max": 2147483647, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 7, "Genre": "Config", "Help": "Bits 0 - 7 -> HVAC Setup: Normal (0x01) or Heat Pump (0x02) Bits 8 - 11 -> Number of Auxiliary Stages (Heat Pump) / Number of Heat Stages (Normal) Bits 12 - 15 -> Aux Setup: Gas (0x01) or Electric (0x02) Bits 16 - 23 -> Number of Heat Pump Stages Bits 24 - 31 -> Number of Cool Stages", "ValueIDKey": 562950081085460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/844425057796116/,{ "Label": "Utility Lock", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 7, "Genre": "Config", "Help": "The Utility Lock Configuration Set command enables or disables the utility lock. If the utility lock is enabled, the setpoint cannot be modified directly via the thermostat screen.", "ValueIDKey": 844425057796116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1125900034506772/,{ "Label": "C-Wire/Battery Status", "Value": { "List": [ { "Value": 1, "Label": "C-Wire" }, { "Value": 2, "Label": "Battery" } ], "Selected": "C-Wire", "Selected_id": 1 }, "Units": "", "Min": 1, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 7, "Genre": "Config", "Help": "1 -> C-Wire 2 -> Battery", "ValueIDKey": 1125900034506772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1407375011217428/,{ "Label": "Humidity Reporting Threshold", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "3% RH" }, { "Value": 2, "Label": "5% RH" }, { "Value": 3, "Label": "10% RH" } ], "Selected": "5% RH", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 7, "Genre": "Config", "Help": "The Humidity Reporting Threshold Configuration Set Command sets the reporting threshold for changes in the ambient humidity as detected by the thermostat.", "ValueIDKey": 1407375011217428, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Auxiliary/Emergency heat", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "The Auxiliary/Emergency configuration command enables or disables auxiliary / emergency heating in the thermostat. Auxiliary / emergency heating is only available if the thermostat is configured in heat pump mode and with at least one stage of auxiliary heating. This command enables auxiliary / emergency heating when the thermostat is in Auto mode. The Thermostat Set Mode command with mode Auxiliary/Emergency Heat will enable emergency heating but only if the thermostat is in Heat mode. This command should only be used on thermsotats that support Auxiliary/Emergency Heat thermostat mode.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1970324964638740/,{ "Label": "Thermostat Swing Temperature", "Value": { "List": [ { "Value": 1, "Label": "0.5F" }, { "Value": 2, "Label": "1.0F" }, { "Value": 3, "Label": "1.5F" }, { "Value": 4, "Label": "2.0F" }, { "Value": 5, "Label": "2.5F" }, { "Value": 6, "Label": "3.0F" }, { "Value": 7, "Label": "3.5F" }, { "Value": 8, "Label": "4.0F" } ], "Selected": "1.0F", "Selected_id": 2 }, "Units": "", "Min": 1, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 7, "Genre": "Config", "Help": "The Temperate Swing (HVAC cycling rate) is the desired variance in temperature between the thermostat setting and the room temperature required before the heating or cooling system will turn on.", "ValueIDKey": 1970324964638740, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Thermostat Differential Temperature", "Value": { "List": [ { "Value": 4, "Label": "2.0F Heat" }, { "Value": 6, "Label": "3.0F Heat" }, { "Value": 8, "Label": "4.0F Heat" }, { "Value": 10, "Label": "5.0F Heat" }, { "Value": 12, "Label": "6.0F Heat" }, { "Value": 260, "Label": "2.0F Cool" }, { "Value": 262, "Label": "3.0F Cool" }, { "Value": 264, "Label": "4.0F Cool" }, { "Value": 266, "Label": "5.0F Cool" }, { "Value": 268, "Label": "6.0F Cool" } ], "Selected": "2.0F Heat", "Selected_id": 4 }, "Units": "F", "Min": 2, "Max": 32767, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "(Set Only) The Thermostat Differential Temperature configuration command sets the differential temperature for multi-stage HVAC systems. The differential temperature delta defines when the thermostat will turn on additional stages. There are two differential temperatures, one for multistage cool systems and one for multistage heat systems. If the thermostat is not configured for multistage HVAC systems then these parameters have no effect.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060052/,{ "Label": "Thermostat Recovery Mode", "Value": { "List": [ { "Value": 1, "Label": "Fast" }, { "Value": 2, "Label": "Economy" } ], "Selected": "Economy", "Selected_id": 2 }, "Units": "", "Min": 1, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "The Thermostat Recovery Mode configuration command sets the HVAC recovery mode type. The recovery mode determines when additional HVAC stages are turned off as the ambient temperature returns to the target temperature. If the recovery mode is set to economy, the thermostat will turn off additional HVAC stages when the ambient temperature reaches the target temperature plus/minus the differential temperature. If the recovery mode is set to fast, the thermostat will leave all stages on (assuming they were already on) until the ambient temperature reaches the target temperature.", "ValueIDKey": 2533274918060052, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Temperature Reporting Filter", "Value": 124, "Units": "F", "Min": 0, "Max": 124, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "The Temperature Reporting Filter configuration command sets upper and lower bounds of the ambient temperature reporting. The thermostat won't report ambient temperature changes if the ambient temperature falls between these bounds. For example, if the upper bound is 80F and the lower bound is 60F, the thermostat will not send SENSOR_MULTI_LEVEL_REPORTS for ambient temperature values between 60F and 80F. The thermostat will only send ambient temperature changes if the thermostat has been added to an association group (see Command Class Association) and the temperature reporting threshold is non-zero (see Temperature Reporting Threshold). Input in hexadecimal only like so: 0x09 0x05 0x09 0x0A. It must always have four 1 byte sized numbers. The first two bytes control the lower temperature bound for the Temperature Reporting Filter the last two control the upper temperature bound. The first byte in the byte pair always refers to temperature scale (Celsius 0x01 or Fahrenheit 0x09). While the second byte in each byte pair is the bound temperature. The max/min temp you can use is 127 degrees. To convert decimal to hex goto: https://www.binaryhexconverter.com/decimal-to-hex-converter or you can use the built in Windows calculator program in Programmer mode. If you mess up your thermostat copy and paste 0x09 0x00 0x09 0x00 (for a F Thermostat) or 0x01 0x00 0x01 0x00 (for a C Thermostat). This will remove any bounds.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481364/,{ "Label": "Simple UI Mode", "Value": { "List": [ { "Value": 0, "Label": "Enable" }, { "Value": 1, "Label": "Disable" } ], "Selected": "Disable", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "If the value is set to Disable then Normal Mode is enabled. If the value is set to Enable then Simple Mode is enabled.", "ValueIDKey": 3096224871481364, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Multicast", "Value": 0, "Units": "", "Min": 0, "Max": 1, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "If set to 0, multicast is disabled, if set to 1, will enable the multicast.", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/66/,{ "Instance": 1, "CommandClassId": 66, "CommandClass": "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/66/value/122716183/,{ "Label": "Operating State", "Value": "Idle", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Operating State", "ValueIDKey": 122716183, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/69/,{ "Instance": 1, "CommandClassId": 69, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_STATE", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/69/value/122765335/,{ "Label": "Fan State", "Value": "Idle", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_STATE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Fan State", "ValueIDKey": 122765335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/94/value/131563537/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 7, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 131563537, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/94/value/281475108274198/,{ "Label": "InstallerIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 7, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475108274198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/94/value/562950084984854/,{ "Label": "UserIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 7, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950084984854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/844425062023191/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 7, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425062023191, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/114/value/1125900038733847/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 7, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900038733847, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/131907604/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 131907604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/281475108618257/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 7, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475108618257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/562950085328920/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 7, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950085328920, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/844425062039569/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425062039569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1125900038750228/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900038750228, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1407375015460886/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 7, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375015460886, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1688849992171544/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 7, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849992171544, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/1970324968882200/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 7, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324968882200, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/2251799945592852/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 7, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799945592852, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/115/value/2533274922303510/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 7, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274922303510, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/128/value/123731985/,{ "Label": "Battery Level", "Value": 65, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 7, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 123731985, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/129/value/123748372/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Thursday", "Selected_id": 4 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 7, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 123748372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/129/value/281475100459025/,{ "Label": "Hour", "Value": 2, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 7, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475100459025, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/129/value/562950077169681/,{ "Label": "Minute", "Value": 17, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 7, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950077169681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} -OpenZWave/1/node/7/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/134/value/132218903/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 7, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 132218903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} -OpenZWave/1/node/7/instance/1/commandclass/134/value/281475108929559/,{ "Label": "Protocol Version", "Value": "3.83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 7, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475108929559, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} -OpenZWave/1/node/7/instance/1/commandclass/134/value/562950085640215/,{ "Label": "Application Version", "Value": "10.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 7, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950085640215, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} -OpenZWave/1/node/7/instance/1/commandclass/135/,{ "Instance": 1, "CommandClassId": 135, "CommandClass": "COMMAND_CLASS_INDICATOR", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/135/value/123846673/,{ "Label": "Indicator", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_INDICATOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Current Indicator State", "ValueIDKey": 123846673, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/49/value/281475099148306/,{ "Label": "Instance 1: Air Temperature", "Value": 73.5, "Units": "F", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475099148306, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588266231} -OpenZWave/1/node/7/instance/1/commandclass/49/value/72057594168754196/,{ "Label": "Instance 1: Air Temperature Units", "Value": { "List": [ { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Fahrenheit", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 7, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594168754196, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/49/value/1407375005990930/,{ "Label": "Instance 1: Humidity", "Value": 55.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990930, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588266231} -OpenZWave/1/node/7/instance/1/commandclass/49/value/73183494075596820/,{ "Label": "Instance 1: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} -OpenZWave/1/node/7/instance/1/commandclass/64/,{ "Instance": 1, "CommandClassId": 64, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/,{ "Label": "Mode", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Heat" }, { "Value": 2, "Label": "Cool" }, { "Value": 3, "Label": "Auto" }, { "Value": 11, "Label": "Heat Econ" }, { "Value": 12, "Label": "Cool Econ" } ], "Selected": "Heat", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Mode", "ValueIDKey": 122683412, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/67/value/281475099443218/,{ "Label": "Heating 1", "Value": 70.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475099443218, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/67/value/562950076153874/,{ "Label": "Cooling 1", "Value": 78.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 2, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Cooling 1", "ValueIDKey": 562950076153874, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/67/value/3096224866549778/,{ "Label": "Heating Econ", "Value": 62.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 11, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating Econ", "ValueIDKey": 3096224866549778, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/67/value/3377699843260434/,{ "Label": "Cooling Econ", "Value": 85.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 12, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Cooling Econ", "ValueIDKey": 3377699843260434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/1/commandclass/68/,{ "Instance": 1, "CommandClassId": 68, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_MODE", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/1/commandclass/68/value/122748948/,{ "Label": "Fan Mode", "Value": { "List": [ { "Value": 0, "Label": "Auto Low" }, { "Value": 1, "Label": "On Low" } ], "Selected": "Auto Low", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_MODE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Fan Mode", "ValueIDKey": 122748948, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/,{ "Instance": 2, "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/commandclass/49/,{ "Instance": 2, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/commandclass/49/value/281475099148322/,{ "Label": "Instance 2: Air Temperature", "Value": 72.5, "Units": "F", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475099148322, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} -OpenZWave/1/node/7/instance/2/commandclass/49/value/72057594168754212/,{ "Label": "Instance 2: Air Temperature Units", "Value": { "List": [ { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Fahrenheit", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 7, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594168754212, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/instance/2/commandclass/49/value/1407375005990946/,{ "Label": "Instance 2: Humidity", "Value": 56.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990946, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264907} -OpenZWave/1/node/7/instance/2/commandclass/49/value/73183494075596836/,{ "Label": "Instance 2: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596836, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} -OpenZWave/1/node/7/association/1/,{ "Name": "Reporting", "Help": "", "MaxAssociations": 2, "Members": [ "1.0" ], "TimeStamp": 1588264906} -OpenZWave/1/node/8/,{ "NodeID": 8, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0002:0003:0005", "ZWAProductURL": "https://products.z-wavealliance.org/products/1507/", "ProductPic": "images/danfoss/z.png", "Description": "Electronic radiator thermostat", "ProductManualURL": "", "ProductPageURL": "http://heating.consumers.danfoss.com/xxTypex/585379.html", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "CEPT (Europe)", "Name": "Danfoss Living Connect Z v1.06 014G0013", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1594159718, "NodeManufacturerName": "Danfoss", "NodeProductName": "Z Thermostat 014G0013", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "Setpoint Thermostat", "NodeSpecific": 4, "NodeManufacturerID": "0x0002", "NodeProductType": "0x0005", "NodeProductID": "0x0004", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1 ], "Neighbors": [ 1 ]} -OpenZWave/1/node/8/instance/1/commandclass/70/value/2251799953244180/,{ "Label": "Override State", "Value": { "List": [ { "Value": 0, "Label": "None" }, { "Value": 1, "Label": "Temporary" }, { "Value": 2, "Label": "Permanent" } ], "Selected": "None", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 8, "Node": 8, "Genre": "User", "Help": "Override Schedule", "ValueIDKey": 2251799953244180, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/70/value/2533274929954833/,{ "Label": "Override Setback", "Value": 127, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 9, "Node": 8, "Genre": "User", "Help": "Override Setback", "ValueIDKey": 2533274929954833, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/70/value/281475116269589/,{ "Label": "Monday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 1, "Node": 8, "Genre": "User", "Help": "Schedule for Monday", "ValueIDKey": 281475116269589, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/562950092980245/,{ "Label": "Tuesday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 2, "Node": 8, "Genre": "User", "Help": "Schedule for Tuesday", "ValueIDKey": 562950092980245, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/844425069690901/,{ "Label": "Wednesday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 3, "Node": 8, "Genre": "User", "Help": "Schedule for Wednesday", "ValueIDKey": 844425069690901, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1125900046401557/,{ "Label": "Thursday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 4, "Node": 8, "Genre": "User", "Help": "Schedule for Thursday", "ValueIDKey": 1125900046401557, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1407375023112213/,{ "Label": "Friday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 5, "Node": 8, "Genre": "User", "Help": "Schedule for Friday", "ValueIDKey": 1407375023112213, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1688849999822869/,{ "Label": "Saturday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 6, "Node": 8, "Genre": "User", "Help": "Schedule for Saturday", "ValueIDKey": 1688849999822869, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/value/1970324976533525/,{ "Label": "Sunday", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Schedule", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "Index": 7, "Node": 8, "Genre": "User", "Help": "Schedule for Sunday", "ValueIDKey": 1970324976533525, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/70/,{ "Instance": 1, "CommandClassId": 70, "CommandClass": "COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/117/value/148717588/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Unprotected", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 8, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 148717588, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/128/value/140509201/,{ "Label": "Battery Level", "Value": 79, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 8, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 140509201, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/129/value/140525588/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Wednesday", "Selected_id": 3 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 8, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 140525588, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/129/value/281475117236241/,{ "Label": "Hour", "Value": 13, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 8, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475117236241, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/129/value/562950093946897/,{ "Label": "Minute", "Value": 17, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 8, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950093946897, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159718} -OpenZWave/1/node/8/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/67/value/281475116220434/,{ "Label": "Heating 1", "Value": 21.0, "Units": "C", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 8, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475116220434, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594159422} -OpenZWave/1/node/8/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "CommandClassVersion": 2, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/value/148668435/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 8, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 148668435, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/value/281475125379091/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 8, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475125379091, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/114/value/562950102089747/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 8, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950102089747, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "CommandClassVersion": 2, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/148963347/,{ "Label": "Wake-up Interval", "Value": 300, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 8, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 148963347, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/281475125674003/,{ "Label": "Minimum Wake-up Interval", "Value": 60, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 8, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475125674003, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/562950102384659/,{ "Label": "Maximum Wake-up Interval", "Value": 1800, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 8, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950102384659, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/844425079095315/,{ "Label": "Default Wake-up Interval", "Value": 300, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 8, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425079095315, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/132/value/1125900055805971/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 8, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900055805971, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/value/148996119/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 8, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 148996119, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/value/281475125706775/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 8, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475125706775, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/commandclass/134/value/562950102417431/,{ "Label": "Application Version", "Value": "1.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 8, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950102417431, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594159418} -OpenZWave/1/node/8/instance/1/,{ "Instance": 1, "TimeStamp": 1594159418} -OpenZWave/1/node/16/,{ "NodeID": 16, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0148:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2543/", "ProductPic": "images/eurotronic/eur_spiritz.png", "Description": "• Easy control for water radiators from any Z-Wave Controller • Fits most European water radiators (wide range of additional adaptors for different manufacturers available) • FLiRS for quick response time • LED Backlit LCD • Metal nut for reliable connection to the radiator • 2 buttons for easy temperature regulation • Battery level indicator • Child Lock • Over the Air update • UK-Mode for upside down installation • Open Window detection • Automatic frost protection", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2650/Spirit_Z-Wave_BAL_web_EN_view_05.pdf", "ProductPageURL": "", "InclusionHelp": "Start Inclusion mode of your primary Z-Wave Controller. Press the Boost-Button.", "ExclusionHelp": "Start Exclusion mode of your primary Z-Wave Controller. Now press and hold the boost button of the Spirit Z-Wave Plus for at least 5 seconds.", "ResetHelp": "Please use this procedure only when the network primary controller is missing or otherwise inoperable. Remove batteries. Press and hold boost button. While still holding boost button insert batteries. The LCD shows RES. Release boost button. To perform the factory reset press boost button.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "UAE", "Name": "KOMFORTHAUS Spirit Z-Wave Plus", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588422766, "NodeManufacturerName": "EUROtronic", "NodeProductName": "EUR_SPIRITZ Wall Radiator Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0148", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 7, 8, 9, 10, 12, 13, 14 ]} -OpenZWave/1/node/16/instance/1/,{ "Instance": 1, "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/273252369/,{ "Label": "Level", "Value": 94, "Units": "%", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 16, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 273252369, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422759} -OpenZWave/1/node/16/instance/1/commandclass/38/value/281475249963032/,{ "Label": "Up", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 16, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475249963032, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/562950226673688/,{ "Label": "Down", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 16, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950226673688, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/844425211772944/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 16, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425211772944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/38/value/1125900188483601/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 16, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900188483601, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/64/,{ "Instance": 1, "CommandClassId": 64, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/64/value/273678356/,{ "Label": "Mode", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Heat" }, { "Value": 11, "Label": "Heat Eco" }, { "Value": 15, "Label": "Full Power" }, { "Value": 31, "Label": "Manufacturer Specific" } ], "Selected": "Heat", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "Index": 0, "Node": 16, "Genre": "User", "Help": "Off: No heating, only frost protection. Heat: Room temperature will be kept at the configured setpoint. Heat Eco: Energy save heating mode. Room temperature will be lowered to the configured eco setpoint in order to save energy. Full Power: Full power heating. This mode is left automatically after 5 minutes. Manufacturer Specific: Direct valve control mode. The valve opening percentage can be controlled using the switch multilevel command class.", "ValueIDKey": 273678356, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/281475250438162/,{ "Label": "Heating 1", "Value": 19.0, "Units": "C", "Min": 8, "Max": 28, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 16, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475250438162, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/3096225017544722/,{ "Label": "Heating Econ", "Value": 18.0, "Units": "C", "Min": 8, "Max": 28, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 11, "Node": 16, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating Econ", "ValueIDKey": 3096225017544722, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/28428972921503762/,{ "Label": "Heating 1_minimum", "Value": 8.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 101, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 28428972921503762, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/56576470592569362/,{ "Label": "Heating 1_maximum", "Value": 28.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 201, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 56576470592569362, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/31243722688610322/,{ "Label": "Heating Econ_minimum", "Value": 8.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 111, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 31243722688610322, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/67/value/59391220359675922/,{ "Label": "Heating Econ_maximum", "Value": 28.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 211, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 59391220359675922, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/281475255369748/,{ "Label": "LCD Invert", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Upside Down" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 16, "Genre": "Config", "Help": "Allows rotating the LCD contents by 180 degrees. Default: Normal", "ValueIDKey": 281475255369748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/562950232080401/,{ "Label": "LCD Timeout", "Value": 0, "Units": "sec", "Min": 0, "Max": 30, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 16, "Genre": "Config", "Help": "0: No Timeout, LCD always on. 5-30: Timeout after 5-30s. Default: 0 (LCD always on)", "ValueIDKey": 562950232080401, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/844425208791060/,{ "Label": "Backlight", "Value": { "List": [ { "Value": 0, "Label": "Backlight disabled" }, { "Value": 1, "Label": "Backlight enabled" } ], "Selected": "Backlight enabled", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 16, "Genre": "Config", "Help": "Default: Backlight enabled", "ValueIDKey": 844425208791060, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1125900185501716/,{ "Label": "Battery Report", "Value": { "List": [ { "Value": 0, "Label": "Only send battery status as notification" }, { "Value": 1, "Label": "Send once a day" } ], "Selected": "Send once a day", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 16, "Genre": "Config", "Help": "Default: Send once a day", "ValueIDKey": 1125900185501716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1407375162212369/,{ "Label": "Temperature Report Threshold", "Value": 1, "Units": "0.1°C", "Min": 0, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 16, "Genre": "Config", "Help": "0: Don't send temperature automatically. 1-50: Report temperature at 0.1-5.0°C temperature difference. Default: 5 (Delta = 0.5°C)", "ValueIDKey": 1407375162212369, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422810} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1688850138923025/,{ "Label": "Valve Opening Percentage Report", "Value": 5, "Units": "", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 16, "Genre": "Config", "Help": "0: Don't send Valve opening percentage automatically. 1-100: Report valve opening percentage at a delta of 1-100%. Default: 0", "ValueIDKey": 1688850138923025, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422811} -OpenZWave/1/node/16/instance/1/commandclass/112/value/1970325115633684/,{ "Label": "Open Window Detection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Low sensibility" }, { "Value": 2, "Label": "Medium sensibility" }, { "Value": 3, "Label": "High sensibility" } ], "Selected": "Medium sensibility", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 16, "Genre": "Config", "Help": "Default: Medium sensibility", "ValueIDKey": 1970325115633684, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/112/value/2251800092344337/,{ "Label": "Measured Temperature Offset", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 16, "Genre": "Config", "Help": "206-255: -5.0 to -0.1°C. 0-50: 0°C-5°C. 128: External Temperature Sensor. Default: 0 (0.0°C Offset)", "ValueIDKey": 2251800092344337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/value/282558481/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 16, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 282558481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/value/281475259269142/,{ "Label": "InstallerIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 16, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475259269142, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/94/value/562950235979798/,{ "Label": "UserIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 16, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950235979798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/282886163/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 16, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 282886163, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/281475259596819/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 16, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475259596819, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/562950236307475/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 16, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950236307475, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/844425213018135/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 16, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425213018135, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/114/value/1125900189728791/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 16, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900189728791, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/282902548/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 16, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 282902548, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/281475259613201/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 16, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475259613201, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/562950236323864/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 16, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950236323864, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/844425213034513/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 16, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425213034513, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1125900189745172/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 16, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900189745172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1407375166455830/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 16, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375166455830, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1688850143166488/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 16, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850143166488, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/1970325119877144/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 16, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325119877144, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/2251800096587796/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 16, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800096587796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/115/value/2533275073298454/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 16, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275073298454, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/117/value/282935316/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Protection by Sequence", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 16, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 282935316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/128/value/274726929/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 16, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 274726929, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/value/283213847/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 16, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 283213847, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/value/281475259924503/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 16, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475259924503, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/134/value/562950236635159/,{ "Label": "Application Version", "Value": "0.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 16, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950236635159, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/152/value/283508752/,{ "Label": "Secured", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 16, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 283508752, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/49/value/281475250143250/,{ "Label": "Air Temperature", "Value": 17.260000228881837, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 16, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475250143250, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422760} -OpenZWave/1/node/16/instance/1/commandclass/49/value/72057594319749140/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" } ], "Selected": "Celsius", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 16, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594319749140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/72057594312409105/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 16, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594312409105, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/2251800088166420/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 10, "Label": "Replace Battery Soon" }, { "Value": 11, "Label": "Replace Battery Now" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 16, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800088166420, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/74872344079515671/,{ "Label": "Error Code", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 16, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344079515671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/instance/1/commandclass/113/value/2533275064877076/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 16, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275064877076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} -OpenZWave/1/node/16/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1588422682} -OpenZWave/1/node/17/,{ "NodeID": 17, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": false, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0059:0003:0001", "ZWAProductURL": "https://products.z-wavealliance.org/products/115/", "ProductPic": "images/horstmann/hrt4zw.png", "Description": "ThermostatThe innovative Horstmann CentaurPlus ZW combined wireless room stat and time control offers installers and householders the opportunity to easily and cost effectively update existing combi boiler controls. The CentaurPlus has an integral transmitter and receiver, enabling wireless communication with the latest generation Horstmann HRT4-ZW TPI room thermostat. Suitable for combi boilers Volt free contacts Automatic BST /GMT time change Back lit display Boost and Advance Helps to meet Part L1 of 2010 Building Regs for existing installations Built in Z Wave receiver Industry Standard 6 terminal wall plate ZW wireless technology TPI energy saving software Clear backlit display Temperature range 5-30°C Battery operated for wire free installation", "ProductManualURL": "", "ProductPageURL": "http://www.securetogether.eu/", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "CEPT (Europe)", "Name": "Secure SRT321 Zwave Stat (Tx)", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1596278310, "NodeManufacturerName": "Horstmann (Secure Meters)", "NodeProductName": "HRT4-ZW Thermostat Transmitter", "NodeBasicString": "Controller", "NodeBasic": 1, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "Thermostat", "NodeSpecific": 0, "NodeManufacturerID": "0x0059", "NodeProductType": "0x0001", "NodeProductID": "0x0003", "NodeBaudRate": 40000, "NodeVersion": 3, "NodeGroups": 5, "NodeName": "", "NodeLocation": ""} -OpenZWave/1/node/17/instance/1/,{ "Instance": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/value/281475272146964/,{ "Label": "Temperature sensor reading", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 255, "Label": "Enable" } ], "Selected": "Enable", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 17, "Genre": "Config", "Help": "", "ValueIDKey": 281475272146964, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/value/562950248857620/,{ "Label": "Temperature Scale", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 255, "Label": "Fahrenheit" } ], "Selected": "Celsius", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 17, "Genre": "Config", "Help": "", "ValueIDKey": 562950248857620, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/112/value/844425225568273/,{ "Label": "Temperature Delta T", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 17, "Genre": "Config", "Help": "Delta T in steps of 0.1 degree.", "ValueIDKey": 844425225568273, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "CommandClassVersion": 2, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/value/285736977/,{ "Label": "Basic", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 17, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 285736977, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/value/281475262447633/,{ "Label": "Basic Target", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 1, "Node": 17, "Genre": "Basic", "Help": "", "ValueIDKey": 281475262447633, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/32/value/562950239158291/,{ "Label": "Basic Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 2, "Node": 17, "Genre": "Basic", "Help": "", "ValueIDKey": 562950239158291, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/37/,{ "Instance": 1, "CommandClassId": 37, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "CommandClassVersion": 0, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/37/value/290013200/,{ "Label": "Switch", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "Index": 0, "Node": 17, "Genre": "User", "Help": "Turn On/Off Device", "ValueIDKey": 290013200, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/value/299663379/,{ "Label": "Loaded Config Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 17, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 299663379, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/value/281475276374035/,{ "Label": "Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 17, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475276374035, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/114/value/562950253084691/,{ "Label": "Latest Available Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 17, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950253084691, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/128/value/291504145/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 17, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 291504145, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "CommandClassVersion": 2, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/281475276668947/,{ "Label": "Minimum Wake-up Interval", "Value": 256, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 17, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475276668947, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/562950253379603/,{ "Label": "Maximum Wake-up Interval", "Value": 131071, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 17, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950253379603, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/844425230090259/,{ "Label": "Default Wake-up Interval", "Value": 86400, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 17, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425230090259, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/1125900206800915/,{ "Label": "Wake-up Interval Step", "Value": 1, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 17, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900206800915, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/132/value/299958291/,{ "Label": "Wake-up Interval", "Value": 86400, "Units": "Seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 17, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 299958291, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/value/299991063/,{ "Label": "Library Version", "Value": "2", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 17, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 299991063, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/value/281475276701719/,{ "Label": "Protocol Version", "Value": "2.78", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 17, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475276701719, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/134/value/562950253412375/,{ "Label": "Application Version", "Value": "5.00", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 17, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950253412375, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/49/value/281475266920466/,{ "Label": "Air Temperature", "Value": 29.0, "Units": "C", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 17, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475266920466, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1596284337} -OpenZWave/1/node/17/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "CommandClassVersion": 1, "TimeStamp": 1596278310} -OpenZWave/1/node/17/instance/1/commandclass/67/value/281475267215378/,{ "Label": "Heating 1", "Value": 16.0, "Units": "C", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 17, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475267215378, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1596278310} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/cover.json b/tests/components/ozw/fixtures/cover.json deleted file mode 100644 index ece62617edd..00000000000 --- a/tests/components/ozw/fixtures/cover.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/", - "payload": { - "Label": "Instance 1: Level", - "Value": 0, - "Units": "", - "ValueSet": true, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 37, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 625573905, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueChanged", - "TimeStamp": 1593370642 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/cover_gdo.json b/tests/components/ozw/fixtures/cover_gdo.json deleted file mode 100644 index d171c4f2071..00000000000 --- a/tests/components/ozw/fixtures/cover_gdo.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "topic": "OpenZWave/1/node/6/instance/1/commandclass/102/value/281475083239444/", - "payload": { - "Label": "Barrier State", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Closed" - }, - { - "Value": 1, - "Label": "Closing" - }, - { - "Value": 2, - "Label": "Stopped" - }, - { - "Value": 3, - "Label": "Opening" - }, - { - "Value": 4, - "Label": "Opened" - }, - { - "Value": 5, - "Label": "Unknown" - } - ], - "Selected": "Closed", - "Selected_id": 0 - }, - "Units": "", - "ValueSet": true, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", - "Index": 1, - "Node": 6, - "Genre": "User", - "Help": "The Current State of the Barrier", - "ValueIDKey": 281475083239444, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueChanged", - "TimeStamp": 1593634453 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/cover_gdo_network_dump.csv b/tests/components/ozw/fixtures/cover_gdo_network_dump.csv deleted file mode 100644 index 0fc08b4a34d..00000000000 --- a/tests/components/ozw/fixtures/cover_gdo_network_dump.csv +++ /dev/null @@ -1,45 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1173", "OZWDaemon_Version": "0.1.149", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1593370382, "ManufacturerSpecificDBReady": true, "homeID": 3716538409, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.61", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} -OpenZWave/1/node/6/,{ "NodeID": 6, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/014F:3530:4744", "ZWAProductURL": "", "ProductPic": "images/linear/ngd00z.png", "Description": "Garage Door Remote Controller Accessory opens or closes a sectional garage door remotely through a Z-Wave certified Gateway or Security Panel. Compatible with virtually any garage door opener connected to a sectional garage door. Audible and visual warnings prior to remotely-activated door movement, meeting UL 325 safety requirements. Included tilt sensor reports door \"open\" or \"close\" information.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1458/236956BX1 GD00Z-4 GoControl Instructions.pdf", "ProductPageURL": "", "InclusionHelp": "With the hub in \"Add\" mode, press the release the button on the side of the GD00Z.", "ExclusionHelp": "With the hub in \"Remove\" mode, press the release the button on the side of the GD00Z.", "ResetHelp": "Press and release the button in the side of the unit 5 times within 10 seconds only when the primary controller is not available.", "WakeupHelp": "n/a", "ProductSupportURL": "", "Frequency": "", "Name": "GD00Z-4", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1593368191, "NodeManufacturerName": "Linear (Nortek Security Control LLC)", "NodeProductName": "GD00Z-4 Garage Door Opener Remote Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Barrier AddOn", "NodeSpecific": 7, "NodeManufacturerID": "0x014f", "NodeProductType": "0x4744", "NodeProductID": "0x3530", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Access Control Sensor", "NodeDeviceType": 3078, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 2, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 26, 27, 28, 29, 32, 33 ], "Neighbors": [ 1, 2, 8, 9, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22, 23, 26, 27, 28, 29, 32, 33 ]} -OpenZWave/1/node/6/instance/1/,{ "Instance": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/32/value/101187601/,{ "Label": "Basic", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 6, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 101187601, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/48/,{ "Instance": 1, "CommandClassId": 48, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/48/value/105644048/,{ "Label": "Sensor", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "Index": 0, "Node": 6, "Genre": "User", "Help": "Binary Sensor State", "ValueIDKey": 105644048, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/value/114786321/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 6, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 114786321, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/value/281475091496982/,{ "Label": "InstallerIcon", "Value": 3078, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 6, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475091496982, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/94/value/562950068207638/,{ "Label": "UserIcon", "Value": 3078, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 6, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950068207638, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/,{ "Instance": 1, "CommandClassId": 102, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/value/281475083239444/,{ "Label": "Barrier State", "Value": { "List": [ { "Value": 0, "Label": "Closed" }, { "Value": 1, "Label": "Closing" }, { "Value": 2, "Label": "Stopped" }, { "Value": 3, "Label": "Opening" }, { "Value": 4, "Label": "Opened" }, { "Value": 5, "Label": "Unknown" } ], "Selected": "Closed", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 1, "Node": 6, "Genre": "User", "Help": "The Current State of the Barrier", "ValueIDKey": 281475083239444, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593634453} -OpenZWave/1/node/6/instance/1/commandclass/102/value/562950064144404/,{ "Label": "Supported Signals", "Value": { "List": [ { "Value": 0, "Label": "None" }, { "Value": 1, "Label": "Audible" }, { "Value": 2, "Label": "Visual" }, { "Value": 3, "Label": "Both" } ], "Selected": "Both", "Selected_id": 3 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 2, "Node": 6, "Genre": "Config", "Help": "Supported Operations for the Barrier", "ValueIDKey": 562950064144404, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/value/844425040855056/,{ "Label": "Audible Notification", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 3, "Node": 6, "Genre": "Config", "Help": "Enable Audible Notifications of Barrier State Change", "ValueIDKey": 844425040855056, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/102/value/1125900017565712/,{ "Label": "Visual Notification", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_BARRIER_OPERATOR", "Index": 4, "Node": 6, "Genre": "Config", "Help": "Enable Visual Notifications of Barrier State Change", "ValueIDKey": 1125900017565712, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/115114003/,{ "Label": "Loaded Config Revision", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 6, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 115114003, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/281475091824659/,{ "Label": "Config File Revision", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 6, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475091824659, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/562950068535315/,{ "Label": "Latest Available Config File Revision", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 6, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950068535315, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/844425045245975/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 6, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425045245975, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/114/value/1125900021956631/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 6, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900021956631, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/115130388/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 6, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 115130388, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593368077} -OpenZWave/1/node/6/instance/1/commandclass/115/value/281475091841041/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 6, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475091841041, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593368077} -OpenZWave/1/node/6/instance/1/commandclass/115/value/562950068551704/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 6, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950068551704, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/844425045262353/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 6, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425045262353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1125900021973012/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 6, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900021973012, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1407374998683670/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 6, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407374998683670, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1688849975394328/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 6, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849975394328, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/1970324952104984/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 6, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324952104984, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/2251799928815636/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 6, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799928815636, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/115/value/2533274905526294/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 6, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274905526294, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/value/115441687/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 6, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 115441687, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/value/281475092152343/,{ "Label": "Protocol Version", "Value": "4.05", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 6, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475092152343, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/134/value/562950068862999/,{ "Label": "Application Version", "Value": "2.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 6, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950068862999, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/152/value/115736592/,{ "Label": "Secured", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 6, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 115736592, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 4, "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/72057594144636945/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 6, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594144636945, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/74590868935032849/,{ "Label": "Sensor ID", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 265, "Node": 6, "Genre": "User", "Help": "The ID of the Sensor that triggered the alert", "ValueIDKey": 74590868935032849, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/1688849966972948/,{ "Label": "Access Control", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 68, "Label": "Barrier Failed Operation" }, { "Value": 69, "Label": "Barrier Unattended Operation Disabled" }, { "Value": 70, "Label": "Barrier Malfunction" }, { "Value": 73, "Label": "Barrier Sensor Not Detected" }, { "Value": 74, "Label": "Barrier Battery Low" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 6, "Node": 6, "Genre": "User", "Help": "Access Control Alerts", "ValueIDKey": 1688849966972948, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/instance/1/commandclass/113/value/1970324943683604/,{ "Label": "Home Security", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Tampering - Cover Removed" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 7, "Node": 6, "Genre": "User", "Help": "Home Security Alerts", "ValueIDKey": 1970324943683604, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593368054} -OpenZWave/1/node/6/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1593368054} diff --git a/tests/components/ozw/fixtures/cover_network_dump.csv b/tests/components/ozw/fixtures/cover_network_dump.csv deleted file mode 100644 index 6c469361ed5..00000000000 --- a/tests/components/ozw/fixtures/cover_network_dump.csv +++ /dev/null @@ -1,134 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1173", "OZWDaemon_Version": "0.1.149", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1593370382, "ManufacturerSpecificDBReady": true, "homeID": 3716538409, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.61", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} -OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0303", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgr223.png", "Description": "FIBARO Roller Shutter 3 is a device designed to control roller blinds, awnings, venetian blinds, gates and other single phase, AC powered devices. Roller Shutter 3 allows precise positioning of roller blinds or venetian blind lamellas. The device is equipped with power and energy monitoring. It allows to control connected devices either via the Z-Wave network or via a switch connected directly to it. Main features of FIBARO Roller Shutter 3: - Compatible with any Z-Wave or Z-Wave Plus Controller, - Supports Z-Wave network Security Modes: S0 with AES-128 encryption and S2 with PRNG-based encryption, - To be installed with roller blind motors with electronic or mechanical limit switches, - Advanced microprocessor control, - Active power and energy metering functionality, - Works with various types of switches – momentary, toggle and dedicated roller blind switches, - To be installed in wall switch boxes, - Works as a Z-Wave signal repeater.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/3278/FGR-223-EN-T-v1.3.pdf", "ProductPageURL": "", "InclusionHelp": "To add the device to the Z-Wave network manually: 1. Power the device. 2. Identify the S1 switch. 3. Set the main controller in (Security/non-Security Mode) add mode (see the controller’s manual). 4. Quickly, triple click the S1 switch. 5. If you are adding in Security S2, scan the DSK QR code or input the underlined part of the DSK (label on the bottom of the box). 6. Wait for the adding process to end. 7. Successful adding will be confirmed by the Z-Wave controller’s message. To add the device to the Z-Wave network using Smart Start: 1. Set the main controller in Security S2 Authenticated add mode (see the controller’s manual) 2. Scan the DSK QR code or input the underlined part of the DSK 3. (label on the bottom of the box). 4. Power the device (turn on the mains voltage). 5. LED will start blinking yellow, wait for the adding process to end. 6. Successful adding will be confirmed by the Z-Wave controller’s message.", "ExclusionHelp": "To remove the device from the Z-Wave network: 1. Make sure the device is powered. 2. Identify the S1 switch. 3. Set the main controller in remove mode (see the controller’s manual). 4. Quickly, triple click the S1 switch. 5. Wait for the removing process to end. 6. Successful removing will be confirmed by the Z-Wave controller’s message.", "ResetHelp": "Reset procedure allows to restore the device back to its factory settings, which means all information about the Z-Wave controller and user configuration will be deleted. 1. Switch off the mains voltage (disable the fuse). 2. Remove the device from the wall switch box. 3. Switch on the mains voltage. 4. Press and hold the B-button to enter the menu. 5. Wait for the LED indicator to glow yellow. 6. Quickly release and click the B-button again. 7. After few seconds the device will be restarted, which is signalled with the red LED indicator colour. Please use this procedure only when the network primary controller is missing or otherwise inoperable.\"", "WakeupHelp": "FIBARO Roller Shutter 3 is powered with mains voltage so it is always awake.", "ProductSupportURL": "", "Frequency": "", "Name": "Roller Shutter 3", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1593370496, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGR223 Roller Shutter Controller 3", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Motor Control Class B", "NodeSpecific": 6, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0303", "NodeProductID": "0x1000", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Window Covering Endpoint Aware", "NodeDeviceType": 6400, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ], "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ], "Neighbors": [ 3, 6, 7, 12, 13, 14, 25, 34 ]} -OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Switch type", "Value": { "List": [ { "Value": 0, "Label": "Momentary switches" }, { "Value": 1, "Label": "Toggle switches" }, { "Value": 2, "Label": "Single, momentary switch" } ], "Selected": "Momentary switches", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "This parameter defines as what type the device should treat the switch connected to the S1 and S2 terminals. This parameter is not relevant in gate operating modes (parameter 151 set to 3 or 4). In this case switch always works as a momentary and has to be connected to S1 terminal.", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370186} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036372/,{ "Label": "Inputs orientation", "Value": { "List": [ { "Value": 0, "Label": "Default" }, { "Value": 1, "Label": "Reversed" } ], "Selected": "Default", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter allows reversing the operation of switches connected to S1 and S2 without changing the wiring. Default: S1 -> 1st channel, S2 -> 2nd channel. Reversed: S1 -> 2nd channel, S2 -> 1st channel.", "ValueIDKey": 6755400072036372, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} -OpenZWave/1/node/37/instance/1/commandclass/112/value/7036875048747028/,{ "Label": "Outputs orientation", "Value": { "List": [ { "Value": 0, "Label": "Default" }, { "Value": 1, "Label": "Reversed" } ], "Selected": "Reversed", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 25, "Node": 37, "Genre": "Config", "Help": "This parameter allows reversing the operation of Q1 and Q2 without changing the wiring (in case of invalid motor connection) to ensure proper operation. - Default: Q1 -> 1st channel, Q2 -> 2nd channel. - Reversed: Q1 -> 2nd channel, Q2 -> 1st channel.", "ValueIDKey": 7036875048747028, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300307/,{ "Label": "Alarm configuration - 1st slot", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x00, 0x00, 0x00, 0x00]", "ValueIDKey": 8444249932300307, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370377} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010963/,{ "Label": "Alarm configuration - 2st slot (water)", "Value": 100597760, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x05, 0xFF, 0x00, 0x00] (Water Alarm, any notification, no action)", "ValueIDKey": 8725724909010963, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/9007199885721619/,{ "Label": "Alarm configuration - 3st slot (smoke)", "Value": 33488896, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 32, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x01, 0xFF, 0x00, 0x00] (Smoke Alarm, any notification, no action)", "ValueIDKey": 9007199885721619, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/9288674862432275/,{ "Label": "Alarm configuration - 4st slot (CO)", "Value": 50266112, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 33, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x02, 0xFF, 0x00, 0x00] (CO Alarm, any notification, no action)", "ValueIDKey": 9288674862432275, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/9570149839142931/,{ "Label": "Alarm configuration - 5st slot (heat)", "Value": 83820544, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 34, "Node": 37, "Genre": "Config", "Help": "This parameter determines to which alarm frames and how the device should react. The parameters consist of 4 bytes, three most significant bytes are set according to the official Z-Wave protocol specification. 1B [MSB] - notification Type. 2B - notification Status. 3B - Event/State Parameters. 4B [lSB] action: 0 - no action, 1 - open blinds, 2 - close blinds. Default setting: [0x04, 0xFF, 0x00, 0x00] (Heat Alarm, any notification, no action)", "ValueIDKey": 9570149839142931, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/11258999699406865/,{ "Label": "S1 switch - scenes sent", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 40, "Node": 37, "Genre": "Config", "Help": "This parameter determines which actions result in sending scene IDs assigned to them. Sum of: 1 - Key pressed 1 time. 2 - Key pressed 2 times. 4 - Key pressed 3 times. 8 - Key hold down and key released. Default setting: 0.", "ValueIDKey": 11258999699406865, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370378} -OpenZWave/1/node/37/instance/1/commandclass/112/value/11540474676117521/,{ "Label": "S2 switch - scenes sent", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 41, "Node": 37, "Genre": "Config", "Help": "This parameter determines which actions result in sending scene IDs assigned to them. Sum of: 1 - Key pressed 1 time. 2 - Key pressed 2 times. 4 - Key pressed 3 times. 8 - Key hold down and key released. Default setting: 0.", "ValueIDKey": 11540474676117521, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/16888499233619988/,{ "Label": "Measuring power consumed by the device itself", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 60, "Node": 37, "Genre": "Config", "Help": "This parameter determines whether the power metering should include the amount of active power consumed by the device itself.", "ValueIDKey": 16888499233619988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/17169974210330646/,{ "Label": "Power reports - on change", "Value": 15, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 61, "Node": 37, "Genre": "Config", "Help": "This parameter determines the minimum change in consumed power that will result in sending new power report to the main controller. For loads under 50W, the parameter is not relevant and reports are sent every 5W change. Power reports are sent no often than every 30 seconds. 0: reports are disabled. 1-500 (1-500%): change in power. Default setting: 15.", "ValueIDKey": 17169974210330646, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/17451449187041302/,{ "Label": "Power reports - periodic", "Value": 3600, "Units": "second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32400, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 62, "Node": 37, "Genre": "Config", "Help": "This parameter determines in what time intervals the periodic power reports are sent to the main controller. Periodic reports do not depend on power change (parameter 61). 0: periodic reports are disabled 30-32400 (30-32400s): report interval. Default setting: 3600 (1h).", "ValueIDKey": 17451449187041302, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370379} -OpenZWave/1/node/37/instance/1/commandclass/112/value/18295874117173270/,{ "Label": "Energy reports - on change", "Value": 10, "Units": "0.01 kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 65, "Node": 37, "Genre": "Config", "Help": "This parameter determines the minimum change in consumed energy that will result in sending new energy report to the main controller. 0: reports are disabled. 1-500 (0.01 - 5 kWh): change in energy. Default setting: 10 (0.1 kWh).", "ValueIDKey": 18295874117173270, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} -OpenZWave/1/node/37/instance/1/commandclass/112/value/18577349093883926/,{ "Label": "Energy reports - periodic", "Value": 3600, "Units": "second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32400, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 66, "Node": 37, "Genre": "Config", "Help": "This parameter determines in what time intervals the periodic energy reports are sent to the main controller. Periodic reports do not depend on energy change (parameter 65). 0: periodic reports are disabled. 30-32400 (30-32400s): report interval. Default setting: 3600 (1h)", "ValueIDKey": 18577349093883926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} -OpenZWave/1/node/37/instance/1/commandclass/112/value/42221247137579028/,{ "Label": "Force calibration", "Value": { "List": [ { "Value": 0, "Label": "Device is not calibrated" }, { "Value": 1, "Label": "Device is calibrated" }, { "Value": 2, "Label": "Force device calibration" } ], "Selected": "Device is calibrated", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 150, "Node": 37, "Genre": "Config", "Help": "By setting this parameter to 2 the device enters the calibration mode. The parameter relevant only if the device is set to work in positioning mode (parameter 151 set to 1, 2 or 4).", "ValueIDKey": 42221247137579028, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370129} -OpenZWave/1/node/37/instance/1/commandclass/112/value/42502722114289684/,{ "Label": "Operating mode", "Value": { "List": [ { "Value": 1, "Label": "Roller blind" }, { "Value": 2, "Label": "Venetian blind" }, { "Value": 3, "Label": "gate without positioning" }, { "Value": 4, "Label": "gate with positioning" }, { "Value": 5, "Label": "roller blind with built-in driver" }, { "Value": 6, "Label": "roller blind with built-in driver (impulse)" } ], "Selected": "Roller blind", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 37, "Genre": "Config", "Help": "This parameter allows adjusting operation according to the connected device.", "ValueIDKey": 42502722114289684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370380} -OpenZWave/1/node/37/instance/1/commandclass/112/value/42784197091000339/,{ "Label": "Venetian blind - time of full turn of the slats", "Value": 150, "Units": "0.1 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 37, "Genre": "Config", "Help": "For Venetian blinds (parameter 151 set to 2) the parameter determines time of full turn cycle of the slats. For gates (parameter 151 set to 3 or 4) the parameter determines time after which open gate will start closing automatically (if set to 0, gate will not close). The parameter is irrelevant for other modes. 0-90000 (0 - 900s, every 0.01s) time of turn. Default setting: 150 (1.5s).", "ValueIDKey": 42784197091000339, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43065672067710996/,{ "Label": "Set slats back to previous position", "Value": { "List": [ { "Value": 0, "Label": "Only in case of the main controller operation" }, { "Value": 1, "Label": "In case of the main controller operation, momentary switch operation, or when the limit switch is reached." }, { "Value": 2, "Label": "In case of the main controller operation, momentary switch operation, when the limit switch is reached or after receiving the Switch Multilevel Stop control frame" } ], "Selected": "In case of the main controller operation, momentary switch operation, or when the limit switch is reached.", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 153, "Node": 37, "Genre": "Config", "Help": "For Venetian blinds (parameter 151 set to 2) the parameter determines slats positioning in various situations. The parameter is irrelevant for other modes.", "ValueIDKey": 43065672067710996, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43347147044421654/,{ "Label": "Delay motor stop after reaching end switch", "Value": 10, "Units": "0.1 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 600, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 154, "Node": 37, "Genre": "Config", "Help": "For blinds (parameter 151 set to 1, 2, 5 or 6) the parameter determines the time after which the motor will be stopped after end switch contacts are closed. For gates (parameter 151 set to 3 or 4) the parameter determines the time after which the gate will start closing automatically if S2 contacts are opened (if set to 0, gate will not close). 0-600 (0 - 60s). Default setting: 10 (1s).", "ValueIDKey": 43347147044421654, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43628622021132310/,{ "Label": "Motor operation detection", "Value": 10, "Units": "watt", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 155, "Node": 37, "Genre": "Config", "Help": "Power threshold to be interpreted as reaching a limit switch. 0: reaching a limit switch will not be detected 1-255 (1-255W): report interval. Default setting: 10.", "ValueIDKey": 43628622021132310, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370381} -OpenZWave/1/node/37/instance/1/commandclass/112/value/43910096997842963/,{ "Label": "Time of up movement", "Value": 1500, "Units": "0.01 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 156, "Node": 37, "Genre": "Config", "Help": "This parameter determines the time needed for roller blinds to reach the top. For modes with positioning value is set automatically during calibration, otherwise, it must be set manually. 1-90000 (0.01 - 900.00s). Default setting: 6000 (60s).", "ValueIDKey": 43910096997842963, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370140} -OpenZWave/1/node/37/instance/1/commandclass/112/value/44191571974553619/,{ "Label": "Time of down movement", "Value": 1318, "Units": "0.01 second", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 90000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 157, "Node": 37, "Genre": "Config", "Help": "This parameter determines time needed for roller blinds to reach the bottom. For modes with positioning value is set automatically during calibration, otherwise, it must be set manually. 1-90000 (0.01 - 900.00s). Default setting: 6000 (60s).", "ValueIDKey": 44191571974553619, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370382} -OpenZWave/1/node/37/instance/1/commandclass/145/,{ "Instance": 1, "CommandClassId": 145, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/145/value/627326993/,{ "Label": "Venetian Blind slat position", "Value": 0, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 627326993, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/145/value/281475604037649/,{ "Label": "Venetian blind tilt position", "Value": 0, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_PROPRIETARY", "Index": 1, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 281475604037649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/2533275424358417/,{ "Label": "Instance 1: Target Value", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358417, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370645} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1688850485837841/,{ "Label": "Instance 1: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837841, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1970325462548504/,{ "Label": "Instance 1: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548504, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/2251800439259160/,{ "Label": "Instance 1: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259160, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1407375517515793/,{ "Label": "Instance 1: Dimming Duration", "Value": 16, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515793, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370645} -OpenZWave/1/node/37/instance/1/commandclass/38/value/625573905/,{ "Label": "Instance 1: Level", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573905, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370642} -OpenZWave/1/node/37/instance/1/commandclass/38/value/281475602284568/,{ "Label": "Instance 1: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284568, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/562950578995224/,{ "Label": "Instance 1: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995224, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/844425564094480/,{ "Label": "Instance 1: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094480, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/38/value/1125900540805137/,{ "Label": "Instance 1: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805137, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "CommandClassVersion": 3, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/72057594664370195/,{ "Label": "Scene Count", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594664370195, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/72339069645275155/,{ "Label": "Scene Reset Timeout", "Value": 1000, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 257, "Node": 37, "Genre": "Config", "Help": "", "ValueIDKey": 72339069645275155, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/281475603152916/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" }, { "Value": 4, "Label": "Pressed 2 Times" }, { "Value": 5, "Label": "Pressed 3 Times" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 281475603152916, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/91/value/562950579863572/,{ "Label": "Scene 2", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" }, { "Value": 4, "Label": "Pressed 2 Times" }, { "Value": 5, "Label": "Pressed 3 Times" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579863572, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "Instance 1: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "Instance 1: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 4, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "0000000000000dd0", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} -OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} -OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/117/value/635256852/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Unprotected", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 635256852, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370033} -OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "6.02", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "5.01", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/152/value/635830288/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830288, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/50/value/625770514/,{ "Label": "Electric - kWh", "Value": 0.0, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 625770514, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/1/commandclass/50/value/562950579191826/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579191826, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/1/commandclass/50/value/72057594663698448/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594663698448, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/1/commandclass/50/value/72339069648797720/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 72339069648797720, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/2251800440487956/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 6, "Label": "Over Current Detected" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 37, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800440487956, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/74872344431837207/,{ "Label": "Error Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 37, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344431837207, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/1/commandclass/113/value/2533275417198612/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 37, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275417198612, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/,{ "Instance": 2, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/2533275424358433/,{ "Label": "Instance 2: Target Value", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358433, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370142} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1688850485837857/,{ "Label": "Instance 2: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837857, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1970325462548520/,{ "Label": "Instance 2: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548520, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/2251800439259176/,{ "Label": "Instance 2: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259176, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1407375517515809/,{ "Label": "Instance 2: Dimming Duration", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515809, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/2/commandclass/38/value/625573921/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573921, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370142} -OpenZWave/1/node/37/instance/2/commandclass/38/value/281475602284584/,{ "Label": "Instance 2: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284584, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/562950578995240/,{ "Label": "Instance 2: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995240, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/844425564094496/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094496, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/38/value/1125900540805153/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805153, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/value/634880033/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880033, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/value/281475611590694/,{ "Label": "Instance 2: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590694, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/94/value/562950588301350/,{ "Label": "Instance 2: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301350, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/152/value/635830304/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830304, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/50/,{ "Instance": 2, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/50/value/625770530/,{ "Label": "Electric - kWh", "Value": 0.0, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 625770530, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370035} -OpenZWave/1/node/37/instance/2/commandclass/50/value/562950579191842/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 562950579191842, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370663} -OpenZWave/1/node/37/instance/2/commandclass/50/value/72057594663698464/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 37, "Genre": "User", "Help": "", "ValueIDKey": 72057594663698464, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370035} -OpenZWave/1/node/37/instance/2/commandclass/50/value/72339069648797736/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 72339069648797736, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/72057594664730657/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730657, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/2251800440487972/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 6, "Label": "Over Current Detected" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 37, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800440487972, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/74872344431837223/,{ "Label": "Error Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 37, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344431837223, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/2/commandclass/113/value/2533275417198628/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 37, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275417198628, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/3/,{ "Instance": 3, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/3/commandclass/38/,{ "Instance": 3, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 4, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/2533275424358449/,{ "Label": "Instance 3: Target Value", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "", "ValueIDKey": 2533275424358449, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1688850485837873/,{ "Label": "Instance 3: Step Size", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 6, "Node": 37, "Genre": "User", "Help": "How Many Percent Change when incrementing/decrementing the Level of a Device", "ValueIDKey": 1688850485837873, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1970325462548536/,{ "Label": "Instance 3: Inc", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 7, "Node": 37, "Genre": "User", "Help": "Increment the Level of a Device", "ValueIDKey": 1970325462548536, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/2251800439259192/,{ "Label": "Instance 3: Dec", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Decrement the Level of a Device", "ValueIDKey": 2251800439259192, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1407375517515825/,{ "Label": "Instance 3: Dimming Duration", "Value": 254, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375517515825, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/3/commandclass/38/value/625573937/,{ "Label": "Instance 3: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 37, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 625573937, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1593370034} -OpenZWave/1/node/37/instance/3/commandclass/38/value/281475602284600/,{ "Label": "Instance 3: Up", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475602284600, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/562950578995256/,{ "Label": "Instance 3: Down", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 37, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950578995256, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/844425564094512/,{ "Label": "Instance 3: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425564094512, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/38/value/1125900540805169/,{ "Label": "Instance 3: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900540805169, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/,{ "Instance": 3, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/value/634880049/,{ "Label": "Instance 3: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880049, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/value/281475611590710/,{ "Label": "Instance 3: InstallerIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590710, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/94/value/562950588301366/,{ "Label": "Instance 3: UserIcon", "Value": 6400, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 3, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301366, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369822} -OpenZWave/1/node/37/instance/3/commandclass/152/,{ "Instance": 3, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1593369823} -OpenZWave/1/node/37/instance/3/commandclass/152/value/635830320/,{ "Label": "Instance 3: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 3, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 37, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 635830320, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1593369823} -OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.1" ], "TimeStamp": 1593369823} -OpenZWave/1/node/37/association/2/,{ "Name": "Roller Shutter", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1593369913} -OpenZWave/1/node/37/association/3/,{ "Name": "Slats", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1593369913} diff --git a/tests/components/ozw/fixtures/fan.json b/tests/components/ozw/fixtures/fan.json deleted file mode 100644 index 2684e5f7385..00000000000 --- a/tests/components/ozw/fixtures/fan.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/10/instance/1/commandclass/38/value/172589073/", - "payload": { - "Label": "Level", - "Value": 41, - "Units": "", - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 10, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 172589073, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueAdded", - "TimeStamp": 1589997977 - } -} diff --git a/tests/components/ozw/fixtures/fan_network_dump.csv b/tests/components/ozw/fixtures/fan_network_dump.csv deleted file mode 100644 index 54541271d14..00000000000 --- a/tests/components/ozw/fixtures/fan_network_dump.csv +++ /dev/null @@ -1,51 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1123", "OZWDaemon_Version": "0.1.98", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1589998153, "ManufacturerSpecificDBReady": true, "homeID": 4188283268, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": false, "getControllerLibraryVersion": "Z-Wave 4.54", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} -OpenZWave/1/node/10/,{ "NodeID": 10, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0063:3031:4944", "ZWAProductURL": "", "ProductPic": "images/ge/12724-dimmer.png", "Description": "Transform any home into a smart home with the GE Z-Wave Smart Fan Control. The in-wall fan control easily replaces any standard in-wall switch remotely controls a ceiling fan in your home and features a three-speed control system. Your home will be equipped with ultimate flexibility with the GE Z-Wave Smart Fan Control, capable of being used by itself or with up to four GE add-on switches. Screw terminal installation provides improved space efficiency when replacing existing switches and the integrated LED indicator light allows you to easily locate the switch in a dark room. The GE Z-Wave Smart Fan Control is compatible with any Z-Wave certified gateway, providing access to many popular home automation systems. Take control of your home lighting with GE Z-Wave Smart Lighting Controls!", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2506/Binder2.pdf", "ProductPageURL": "http://www.ezzwave.com", "InclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to include a device to the Z-Wave network. 2. Once the controller is ready to include your device, press and release the top or bottom of the smart fan control switch (rocker) to include it in the network. 3. Once your controller has confirmed the device has been included, refresh the Z-Wave network to optimize performance.", "ExclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to exclude a device from the Z-Wave network. 2. Once the controller is ready to Exclude your device, press and release the top or bottom of the wireless smart switch (rocker) to exclude it from the network.", "ResetHelp": "1. Quickly press ON (Top) button three (3) times then immediately press the OFF (Bottom) button three (3) times. The LED will flash ON/OFF 5 times when completed successfully. Note: This should only be used in the event your network’s primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "In-Wall Smart Fan Control" }, "Event": "nodeQueriesComplete", "TimeStamp": 1589998151, "NodeManufacturerName": "GE (Jasco Products)", "NodeProductName": "14287 Fan Control Switch", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Fan Switch", "NodeSpecific": 8, "NodeManufacturerID": "0x0063", "NodeProductType": "0x4944", "NodeProductID": "0x3131", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Fan Switch", "NodeDeviceType": 1024, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 29, 30, 32, 33 ]} -OpenZWave/1/node/10/instance/1/,{ "Instance": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/172589073/,{ "Label": "Level", "Value": 41, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 10, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 172589073, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/281475149299736/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 10, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475149299736, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/562950126010392/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 10, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950126010392, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/844425111109648/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425111109648, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/38/value/1125900087820305/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900087820305, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/39/value/180994068/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 180994068, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/43/value/172670995/,{ "Label": "Scene", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 10, "Genre": "User", "Help": "", "ValueIDKey": 172670995, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/43/value/281475149381651/,{ "Label": "Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 10, "Genre": "User", "Help": "", "ValueIDKey": 281475149381651, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/value/181895185/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 10, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 181895185, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/value/281475158605846/,{ "Label": "InstallerIcon", "Value": 1024, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 10, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475158605846, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/94/value/562950135316502/,{ "Label": "UserIcon", "Value": 1024, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 10, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950135316502, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/844425108127764/,{ "Label": "LED Light", "Value": { "List": [ { "Value": 0, "Label": "LED on when light off" }, { "Value": 1, "Label": "LED on when light on" }, { "Value": 2, "Label": "LED always off" } ], "Selected": "LED on when light off", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 10, "Genre": "Config", "Help": "Sets when the LED on the switch is lit.", "ValueIDKey": 844425108127764, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1125900084838420/,{ "Label": "Invert Switch", "Value": { "List": [ { "Value": 0, "Label": "No" }, { "Value": 1, "Label": "Yes" } ], "Selected": "No", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 10, "Genre": "Config", "Help": "Change the top of the switch to OFF and the bottom of the switch to ON, if the switch was installed upside down.", "ValueIDKey": 1125900084838420, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1970325014970385/,{ "Label": "Z-Wave Command Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 1970325014970385, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/2251799991681041/,{ "Label": "Z-Wave Command Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2251799991681041, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/2533274968391697/,{ "Label": "Local Control Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 2533274968391697, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/2814749945102353/,{ "Label": "Local Control Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2814749945102353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/3096224921813009/,{ "Label": "ALL ON/ALL OFF Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 3096224921813009, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/112/value/3377699898523665/,{ "Label": "ALL ON/ALL OFF Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 3377699898523665, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/182222867/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 10, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 182222867, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/281475158933523/,{ "Label": "Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 10, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475158933523, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/562950135644179/,{ "Label": "Latest Available Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 10, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950135644179, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/844425112354839/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 10, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425112354839, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/114/value/1125900089065495/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 10, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900089065495, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/182239252/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 182239252, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/281475158949905/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 10, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475158949905, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/562950135660568/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 10, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950135660568, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/844425112371217/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425112371217, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1125900089081876/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900089081876, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1407375065792534/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 10, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375065792534, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1688850042503192/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 10, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850042503192, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1970325019213848/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 10, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325019213848, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2251799995924500/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 10, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799995924500, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2533274972635158/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 10, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274972635158, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/value/182550551/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 10, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 182550551, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/value/281475159261207/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 10, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475159261207, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/instance/1/commandclass/134/value/562950135971863/,{ "Label": "Application Version", "Value": "5.22", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 10, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950135971863, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} -OpenZWave/1/node/10/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 5, "Members": [ "1.0" ], "TimeStamp": 1589997977} -OpenZWave/1/node/10/association/2/,{ "Name": "Group 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1589998004} -OpenZWave/1/node/10/association/3/,{ "Name": "Group 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1589998004} diff --git a/tests/components/ozw/fixtures/generic_network_dump.csv b/tests/components/ozw/fixtures/generic_network_dump.csv deleted file mode 100644 index 5ca41e879ab..00000000000 --- a/tests/components/ozw/fixtures/generic_network_dump.csv +++ /dev/null @@ -1,284 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/1/,{ "NodeID": 1, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": false, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:005A:0101", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw090.png", "Description": "Aeotec Z-Stick Gen5 is a USB controller. When connected to a host controller via USB, it enables the host controller to take part in the Z-Wave network. Products that are Z-Wave certified can be used and communicate with other Z-Wave certified devices.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1355/Z Stick Gen5 manual 1.pdf", "ProductPageURL": "", "InclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Inclusion” button on your PC/host Controller application.", "ExclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Exclusion” button on your PC/host Controller application.", "ResetHelp": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable. Press and hold the Action Button on Z-Stick for 20 seconds and then release.", "WakeupHelp": "N/A", "ProductSupportURL": "", "Frequency": "", "Name": "Z-Stick Gen5", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566911, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW090 Z-Stick Gen5 US", "NodeBasicString": "Static Controller", "NodeBasic": 2, "NodeGenericString": "Static Controller", "NodeGeneric": 2, "NodeSpecificString": "Static PC Controller", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0101", "NodeProductID": "0x005a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 31, 32, 33, 36, 37, 39 ]} -OpenZWave/1/node/1/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/32/value/17301521/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 1, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 17301521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/22799473140563988/,{ "Label": "LED indicator configuration", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 1, "Genre": "Config", "Help": "Enable/Disable LED indicator when plugged in", "ValueIDKey": 22799473140563988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/61924494903345172/,{ "Label": "Configuration of the RF power level", "Value": { "List": [ { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" } ], "Selected": "10" }, "Units": "", "Min": 1, "Max": 10, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 220, "Node": 1, "Genre": "Config", "Help": "1~10, other= ignore. A total of 10 levels, level 1 as the weak output power, and so on, 10 for most output power level", "ValueIDKey": 61924494903345172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/68116944390979604/,{ "Label": "Security network enabled", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 242, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68116944390979604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/68398419367690259/,{ "Label": "Security network key", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 243, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68398419367690259, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/70931694158086164/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 1, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694158086164, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/112/value/71776119088218131/,{ "Label": "Reset default configuration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 1, "Genre": "Config", "Help": "Reset to the default configuration", "ValueIDKey": 71776119088218131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/31227923/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 1, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 31227923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/281475007938579/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 1, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475007938579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/562949984649235/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 1, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562949984649235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/844424961359895/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 1, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844424961359895, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/1/instance/1/commandclass/114/value/1125899938070551/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 1, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125899938070551, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "Smart Plug is a Z-Wave Switch plugin module specifically used to enable Z-Wave command and control (on/off) of any plug-in tool. It can report wattage consumption or kWh energy usage.Smart Plug is also a security Z-Wave device and supports the Over The Air (OTA) feature for the product’s firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1789/HKZW-SO01_manual.pdf", "ProductPageURL": "", "InclusionHelp": "Set the Z-Wave network main controller into learning mode. Short press the Z-button, the LED will keep turning on or off, which indicates the inclusion is successful.", "ExclusionHelp": "Set the Z-Wave network main controller into remove mode. Triple click the Z-button, the LED will blink slowly, which indicates the inclusion is successful.", "ResetHelp": "Press and hold the Z-button for more than 20 seconds, the LED will blink faster and faster when the button is pressed. If holding time more than 20seconds, the LED indicator will be on for 3 seconds then it blinking slowly, which indicates the reseting is successful. Use this procedure only in the event that the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} -OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/37/,{ "Instance": 1, "CommandClassId": 37, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/,{ "Label": "Switch", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "Index": 0, "Node": 32, "Genre": "User", "Help": "Turn On/Off Device", "ValueIDKey": 541671440, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/39/value/550092820/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 550092820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/43/value/541769747/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541769747, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/43/value/281475518480403/,{ "Label": "Duration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 281475518480403, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/541884434/,{ "Label": "Electric - kWh", "Value": 0.06199999898672104, "Units": "kWh", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541884434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566931} -OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/1125900448727058/,{ "Label": "Electric - V", "Value": 123.90499877929688, "Units": "V", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1125900448727058, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566933} -OpenZWave/1/node/32/instance/1/commandclass/50/value/1407375425437714/,{ "Label": "Electric - A", "Value": 0.0, "Units": "A", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1407375425437714, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/72057594579812368/,{ "Label": "Exporting", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 72057594579812368, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/72339069564911640/,{ "Label": "Reset", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 32, "Genre": "System", "Help": "", "ValueIDKey": 72339069564911640, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/value/550993937/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 32, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 550993937, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/value/281475527704598/,{ "Label": "InstallerIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 32, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475527704598, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/94/value/562950504415254/,{ "Label": "UserIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 32, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950504415254, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/5629500081307668/,{ "Label": "Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 32, "Genre": "Config", "Help": "Smart Plug keep detecting the load power, once the current exceeds 16.5a for more than 5s, smart plug's relay will turn off", "ValueIDKey": 5629500081307668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/5910975058018324/,{ "Label": "Device status after power failure", "Value": { "List": [ { "Value": 0, "Label": "Memorize" }, { "Value": 1, "Label": "On" }, { "Value": 2, "Label": "Off" } ], "Selected": "Memorize" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 32, "Genre": "Config", "Help": "Define how the plug reacts after the power supply is back on. 0 - Smart Plug memorizes its state after a power failure. 1 - Smart Plug does not memorize its state after a power failure. Connected device will be on after the power supply is reconnected. 2 - Smart Plug does not memorize its state after a power failure. Connected device will be off after the power supply is reconnected.", "ValueIDKey": 5910975058018324, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/6755399988150292/,{ "Label": "Notification when load status change", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Basic" }, { "Value": 2, "Label": "Basic without Z-WAVE Command" } ], "Selected": "Basic" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 32, "Genre": "Config", "Help": "Smart Plug can send notifications to association device(Group Lifeline) when state of smart plug's load change 0 - The function is disabled 1 - Send Basic report. 2 - Send Basic report only when Load condition is not changed by Z-WAVE Command", "ValueIDKey": 6755399988150292, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/7599824918282260/,{ "Label": "Indicator Modes", "Value": { "List": [ { "Value": 0, "Label": "Enabled" }, { "Value": 1, "Label": "Disabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 27, "Node": 32, "Genre": "Config", "Help": "After smart plug being included into a Z-Wave network, the LED in the device will indicator the state of load. 0 - The LED will follow the status(on/off) of its load 1 - When the state of Switch's load changed, THe LED will follow the status(on/off) of its load, but the red LED will turn off after 5 seconds if there is no any switch action.", "ValueIDKey": 7599824918282260, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/42502722030403606/,{ "Label": "Threshold of power report", "Value": 50, "Units": "W", "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 32, "Genre": "Config", "Help": "Power threshold to be interpereted, when the change value of load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline)", "ValueIDKey": 42502722030403606, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/42784197007114257/,{ "Label": "Percentage threshold of power report", "Value": 10, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 32, "Genre": "Config", "Help": "Power percentage threshold to be interpreted, when change value of the load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline).", "ValueIDKey": 42784197007114257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48132221564616723/,{ "Label": "Power report frequency", "Value": 30, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 171, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48132221564616723, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48413696541327379/,{ "Label": "Energy report frequency", "Value": 300, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 172, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48413696541327379, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48695171518038035/,{ "Label": "Voltage report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 173, "Node": 32, "Genre": "Config", "Help": "The interval of sending voltage report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48695171518038035, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/112/value/48976646494748691/,{ "Label": "Electricity report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 174, "Node": 32, "Genre": "Config", "Help": "The interval of sending electricity report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48976646494748691, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/551321619/,{ "Label": "Loaded Config Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 32, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 551321619, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/281475528032275/,{ "Label": "Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 32, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475528032275, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/562950504742931/,{ "Label": "Latest Available Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 32, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950504742931, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/844425481453591/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 32, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425481453591, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/114/value/1125900458164247/,{ "Label": "Serial Number", "Value": "0107020900000000000607034800010001000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 32, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900458164247, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/551338004/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 551338004, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/281475528048657/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 32, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475528048657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/562950504759320/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 32, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950504759320, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/844425481469969/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 32, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425481469969, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1125900458180628/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 32, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900458180628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1407375434891286/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 32, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375434891286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1688850411601944/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 32, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850411601944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/1970325388312600/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 32, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325388312600, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/2251800365023252/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 32, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800365023252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/115/value/2533275341733910/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 32, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275341733910, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/value/551649303/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 32, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 551649303, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/value/281475528359959/,{ "Label": "Protocol Version", "Value": "4.24", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 32, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475528359959, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/134/value/562950505070615/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 32, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950505070615, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/32/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.0", "255.0" ], "TimeStamp": 1579566915} -OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "Aeotec by Aeon Labs Water Sensor 6 brings intelligence to a new level, one that is suited to both safety and convenience. It contains 4 sensing points, which would be more accurately to detect the presence and absence of water or detect whether there is water leak in some places of your home. The Water Sensor 6 has an inbuilt buzzer that can play alarm sounds to let you know when the water is detected. The Water Sensor 6 is also a security Z-Wave device that supports Over The Air (OTA) for firmware updates.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2437/Aeon Labs Water Sensor 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action Button that you can find on the product.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action Button that you can find on the product.", "ResetHelp": "Press and hold the Action Button that you can find on the product for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} -OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/32/value/604504081/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 36, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 604504081, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/49/value/281475585687570/,{ "Label": "Air Temperature", "Value": 17.100000381469728, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 36, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475585687570, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/49/value/72057594655293460/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 36, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594655293460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/value/618102801/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102801, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/value/281475594813462/,{ "Label": "Instance 1: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813462, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/94/value/562950571524118/,{ "Label": "Instance 1: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524118, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/562950567624724/,{ "Label": "Waking up for 10 minutes when re-power on", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 36, "Genre": "Config", "Help": "Enable/Disable waking up for 10 minutes when re-power on (battery mode) the Water Sensor.", "ValueIDKey": 562950567624724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/2251800427888657/,{ "Label": "Timeout of awake after the Wake Up CC is sent out", "Value": 30, "Units": "seconds", "Min": 8, "Max": 127, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 36, "Genre": "Config", "Help": "Set the timeout of awake after the Wake Up CC is sent out. Available rang is 8 to 127 seconds.", "ValueIDKey": 2251800427888657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/2533275404599316/,{ "Label": "Current power mode", "Value": { "List": [ { "Value": 0, "Label": "USB power, sleeping mode after re-power on" }, { "Value": 1, "Label": "USB power, keep awake for 10 minutes after re-power on" }, { "Value": 2, "Label": "USB power, always awake state" }, { "Value": 256, "Label": "Battery power, sleeping mode after re-power on" }, { "Value": 257, "Label": "Battery power, keep awake for 10 minutes after re-power on" }, { "Value": 258, "Label": "Battery power, always awake state" } ], "Selected": "USB power, sleeping mode after re-power on" }, "Units": "", "Min": 0, "Max": 258, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 36, "Genre": "Config", "Help": "Report the current power mode and the product state for battery power mode", "ValueIDKey": 2533275404599316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/2814750381309971/,{ "Label": "Alarm time for the Buzzer", "Value": 1968650, "Units": "", "Min": 655360, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 36, "Genre": "Config", "Help": "Set the alarm time for the Buzzer when the sensor is triggered. 1 to 255 Repeated cycle of Buzzer alarm. 256 to 65535 the time of Buzzer keeping ON state (MSB). 65536 to 2147483647 The time of Buzzer keeping OFF state.", "ValueIDKey": 2814750381309971, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/10977524705918993/,{ "Label": "Set the low battery value", "Value": 20, "Units": "%", "Min": 10, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 36, "Genre": "Config", "Help": "10% to 50%", "ValueIDKey": 10977524705918993, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/13510799496314897/,{ "Label": "Sensor report", "Value": 55, "Units": "", "Min": 0, "Max": 55, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 48, "Node": 36, "Genre": "Config", "Help": "Enable/disable the sensor report: Bit 7 - Bit 6 - Bit 5 Notification Report for Overheat alarm. Bit 4 Notification Report for Under heat alarm. Bit 3 - Bit 2 Configuration Report for Tilt sensor. Bit 1 Notification Report for Vibration event. Bit 0 Notification Report for Water Leak event. Note: if the value = 1+2+4+16+32=55, which means if any sensor will report alarm.", "ValueIDKey": 13510799496314897, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/13792274473025555/,{ "Label": "Upper limit value", "Value": 26214400, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 49, "Node": 36, "Genre": "Config", "Help": "Set the upper limit value (overheat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value. Default: 0x01900000 => 40.0C", "ValueIDKey": 13792274473025555, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/14073749449736211/,{ "Label": "Lower limit value", "Value": 0, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 50, "Node": 36, "Genre": "Config", "Help": "Set the lower limit value (under heat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value", "ValueIDKey": 14073749449736211, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/16044074286710806/,{ "Label": "Recover limit value of temperature sensor", "Value": 5120, "Units": "", "Min": 100, "Max": 4080, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 57, "Node": 36, "Genre": "Config", "Help": "Set the recover limit value of temperature sensor. Note: 1. When the current measurement less than or equal (Upper limit - Recover limit), the upper limit report is enabled and then it would send out a sensor report when the next measurement is more than the upper limit. After that the upper limit report would be disabled again until the measurement less than or equal (Upper limit - Recover limit). 2. When the current measurement greater than or equal (Lower limit + Recover limit), the lower limit report is enabled and then it would send out a sensor report when the next measurement is less than the lower limit. After that the lower limit report would be disabled again until the measurement >= (Lower limit + Recover limit). 3. High byte is the recover limit value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 4. Recover limit range: 1.0 to 25.5 C/F (0x0100 to 0xFF00 or 0x0101 to 0xFF01). E.g. The default recover limit value is 2.0 C/F (0x1400/0x1401), when the measurement is less than (Upper limit - 2), the upper limit report would be enabled one time or when the measurement is more than (Lower limit + 2), the lower limit report would be enabled one time.", "ValueIDKey": 16044074286710806, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/18014399123685396/,{ "Label": "Unit of the automatic temperature report", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 64, "Node": 36, "Genre": "Config", "Help": "Set the default unit of the automatic temperature report in parameter 101-103", "ValueIDKey": 18014399123685396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/23643898657898516/,{ "Label": "Get the state of tilt sensor", "Value": { "List": [ { "Value": 0, "Label": "Horizontal" }, { "Value": 1, "Label": "Vertical" } ], "Selected": "Horizontal" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 36, "Genre": "Config", "Help": "Get the state of tilt sensor", "ValueIDKey": 23643898657898516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/24206848611319828/,{ "Label": "Buzzer", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 86, "Node": 36, "Genre": "Config", "Help": "Enable/ disable the buzzer.", "ValueIDKey": 24206848611319828, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/24488323588030490/,{ "Label": "Sensor is triggered the buzzer will alarm", "Value": [ { "Label": "Vibration", "Help": "If the vibration is triggered, the buzzer will alarm.", "Value": 1, "Position": 1 }, { "Label": "Tilt Sensor", "Help": "If the Tilt Sensor is triggered, the buzzer will alarm.", "Value": 1, "Position": 2 }, { "Label": "UnderHeat", "Help": "If the Under Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 4 }, { "Label": "OverHeat", "Help": "If the Over Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 5 } ], "Units": "", "Min": 0, "Max": 55, "Type": "BitSet", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 87, "Node": 36, "Genre": "Config", "Help": "What Sensors Trigger the Buzzer", "ValueIDKey": 24488323588030490, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/24769798564741140/,{ "Label": "Probe 1 Basic Set on grp 3", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 88, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 3 when the Sensor probe 1 is triggered.", "ValueIDKey": 24769798564741140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/25051273541451796/,{ "Label": "Probe 2 Basic Set on grp 4", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 89, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 4 when the Sensor probe 2 is triggered.", "ValueIDKey": 25051273541451796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/26458648425005076/,{ "Label": "Battery report selection", "Value": { "List": [ { "Value": 0, "Label": "USB power level" }, { "Value": 1, "Label": "CR123A battery level" } ], "Selected": "USB power level" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 94, "Node": 36, "Genre": "Config", "Help": "To set which power source level is reported via the Battery CC.", "ValueIDKey": 26458648425005076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/28428973261979668/,{ "Label": "Unsolicited report", "Value": { "List": [ { "Value": 0, "Label": "Send Nothing" }, { "Value": 1, "Label": "Battery Report" }, { "Value": 2, "Label": "Multilevel sensor report for temperature" }, { "Value": 3, "Label": "Battery Report and Multilevel sensor report for temperature" } ], "Selected": "Battery Report and Multilevel sensor report for temperature" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 36, "Genre": "Config", "Help": "To set what unsolicited report would be sent to the Lifeline group.", "ValueIDKey": 28428973261979668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/31243723029086227/,{ "Label": "Unsolicited report interval time", "Value": 3600, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 36, "Genre": "Config", "Help": "To set the interval time of sending reports in Report group 1", "ValueIDKey": 31243723029086227, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/37999122470141972/,{ "Label": "Water leak event report selection", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Send notification report to association group 1" }, { "Value": 2, "Label": "Send configuration 0x88 report to association group 2" }, { "Value": 3, "Label": "Send notification report to association group 1 and Send configuration 0x88 report to association group 2" } ], "Selected": "Send notification report to association group 1" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 135, "Node": 36, "Genre": "Config", "Help": "To set which sensor report can be sent when the water leak event is triggered and if the receiving device is a non-multichannel device.", "ValueIDKey": 37999122470141972, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/38280597446852628/,{ "Label": "Report Type to Send", "Value": { "List": [ { "Value": 0, "Label": "Absence of water is triggered by probe 1 and 2" }, { "Value": 1, "Label": "Presence of water is triggered by probe 1" }, { "Value": 2, "Label": "Presence of water is triggered by probe 2" }, { "Value": 3, "Label": "Presence of water is triggered by probe 1 and 2" } ], "Selected": "Absence of water is triggered by probe 1 and 2" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 136, "Node": 36, "Genre": "Config", "Help": "When the parameter 0x87 is set to 2 or 3, it can get the sensor probes status through this configuration value.", "ValueIDKey": 38280597446852628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/56576470933045270/,{ "Label": "Temperature sensor calibration", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 201, "Node": 36, "Genre": "Config", "Help": "Temperature calibration (the available value range is [-128, 127] or [-12.8C, 12.7C]). Note: 1. High byte is the calibration value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 2. The calibration value (high byte) contains one decimal point. E.g. if the value is set to 20 (0x1400), the calibration value is 2.0 C (EU/AU version) or if the value is set to 20 (0x1401), the calibration value is 2.0 F(US version). 3. The calibration value (high byte) = standard value - measure value. E.g. If measure value =25.3C and the standard value = 23.2C, so the calibration value= 23.2C - 25.3C= -2.1C (0xEB). If the measure value =30.1C and the standard value = 33.2C, so the calibration value= 33.2C - 30.1C=3.1C (0x1F).", "ValueIDKey": 56576470933045270, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/70931694745288724/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 36, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694745288724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/112/value/71776119675420692/,{ "Label": "Reset To Factory Defaults", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Reset to factory default setting" }, { "Value": 1431655765, "Label": "Reset to factory default setting and removed from the z-wave network" } ], "Selected": "Reset to factory default setting" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 36, "Genre": "Config", "Help": "Reset to factory defaults", "ValueIDKey": 71776119675420692, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/,{ "Label": "Instance 1: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/113/value/72057594647953425/,{ "Label": "Instance 1: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953425, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/618430483/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 36, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 618430483, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/281475595141139/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 36, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475595141139, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/562950571851795/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 36, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950571851795, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/844425548562455/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 36, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425548562455, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/114/value/1125900525273111/,{ "Label": "Serial Number", "Value": "0d000100010108010100000004030800000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 36, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900525273111, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/618446868/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 36, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 618446868, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/281475595157521/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 36, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475595157521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/562950571868184/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 36, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950571868184, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/844425548578833/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 36, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425548578833, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1125900525289492/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 36, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900525289492, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1407375502000150/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 36, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375502000150, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1688850478710808/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 36, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850478710808, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/1970325455421464/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 36, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325455421464, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/2251800432132116/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 36, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800432132116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/115/value/2533275408842774/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 36, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275408842774, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/281475595436051/,{ "Label": "Minimum Wake-up Interval", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 36, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475595436051, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/562950572146707/,{ "Label": "Maximum Wake-up Interval", "Value": 16777200, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 36, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950572146707, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/844425548857363/,{ "Label": "Default Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 36, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425548857363, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/1125900525568019/,{ "Label": "Wake-up Interval Step", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 36, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900525568019, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/132/value/618725395/,{ "Label": "Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 36, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 618725395, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/value/618758167/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 36, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 618758167, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/value/281475595468823/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 36, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475595468823, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/134/value/562950572179479/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 36, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950572179479, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/,{ "Instance": 2, "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/value/618102817/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102817, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/value/281475594813478/,{ "Label": "Instance 2: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813478, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/94/value/562950571524134/,{ "Label": "Instance 2: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524134, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/113/value/1407375493578788/,{ "Label": "Instance 2: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578788, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/2/commandclass/113/value/72057594647953441/,{ "Label": "Instance 2: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953441, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/2/,{ "Name": "Send the configuration parameter 0x88", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/3/,{ "Name": "Send Basic Set when the Sensor probe 1 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/36/association/4/,{ "Name": "Send Basic Set when the Sensor probe 2 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0005:0002", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa005.png", "Description": "Aeotec TriSensor is a universal Z-Wave Plus compatible product, consists of temperature, lighting and motion sensors, powered by a CR123A battery. It can be included and operated in any Z-Wave network with other Z-Wave certified devices from other manufacturers and/or other applications. By the built-in motion sensor, an alam will be sent to the gateway when the motion sensor is triggered.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2919/TriSensor user manual 20180416.pdf", "ProductPageURL": "", "InclusionHelp": "Press once TriSensor’s Action Button. If it is the first installation, the yellow LED will keep solid until whole network processing is complete. If successful, the LED will flash white -> green -> white -> green, after 2 seconds finished. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once. If it is the S2 encryption network, please enter the first 5 digits of DSK.", "ExclusionHelp": "Press once TriSensor’s Action Button, the Purple LED will keep solid until whole network processing is complete. If the exclusion is successful, the LED will flash white -> green - >white -> green and then LED will pulse a blue. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once.", "ResetHelp": "1. Power up the device. 2. Press and hold the button for 15s until Red Led is blinking,then release the button. Note: Please use this procedure only when the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "Press and hold the button at least 2s until Red Led is on and then release the button,device will send wakeup notification to controller if device is in a Z-Wave network.", "ProductSupportURL": "", "Frequency": "", "Name": "TriSensor", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA005 TriSensor", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0102", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3} -OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/32/value/621281297/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 37, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 621281297, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/48/,{ "Instance": 1, "CommandClassId": 48, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/,{ "Label": "Sensor", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Binary Sensor State", "ValueIDKey": 625737744, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/281475602464786/,{ "Label": "Air Temperature", "Value": 20.700000762939454, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475602464786, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/844425555886098/,{ "Label": "Illuminance", "Value": 0.0, "Units": "Lux", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Luminance Sensor Value", "ValueIDKey": 844425555886098, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/1234567890/,{ "Label": "Relative Humidity", "Value": 56.7, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1234567890, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678901/,{ "Label": "Pressure", "Value": 123, "Units": "inHg", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Pressure Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/72057594672070676/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 37, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594672070676, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678902/,{ "Label": "Fake Power", "Value": 123, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 90, "Node": 37, "Genre": "User", "Help": "Power Sensor Value", "ValueIDKey": 12345678902, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678903/,{ "Label": "Fake Energy", "Value": 456, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 91, "Node": 37, "Genre": "User", "Help": "Energy Sensor Value", "ValueIDKey": 12345678903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678904/,{ "Label": "Fake Electric", "Value": 789, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 92, "Node": 37, "Genre": "User", "Help": "Electric Sensor Value", "ValueIDKey": 12345678904, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678905/,{ "Label": "Fake String", "Value": "fake", "Units": "", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Fake String Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/49/value/72620544625491988/,{ "Label": "Illuminance Units", "Value": { "List": [ { "Value": 1, "Label": "Lux" } ], "Selected": "Lux" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 258, "Node": 37, "Genre": "System", "Help": "Luminance Sensor Available Units", "ValueIDKey": 72620544625491988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/281475607691286/,{ "Label": "Motion Re-trigger Time", "Value": 30, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the delay time before PIR sensor can be triggered again to reset motion timeout counter. Value = 0 will disable PIR sensor from triggering until motion timeout has finished.", "ValueIDKey": 281475607691286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/562950584401942/,{ "Label": "Motion clear time", "Value": 240, "Units": "Second", "Min": 1, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 37, "Genre": "Config", "Help": "This configures the clear time when your motion sensor times out and sends a no motion status.", "ValueIDKey": 562950584401942, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/844425561112596/,{ "Label": "Motion Sensitivity", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" }, { "Value": 11, "Label": "11" } ], "Selected": "8" }, "Units": "", "Min": 0, "Max": 11, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the sensitivity that motion detect. 0 - PIR sensor disabled. 1 - Lowest sensitivity. 11 - Highest sensitivity.", "ValueIDKey": 844425561112596, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1125900537823252/,{ "Label": "Binary Sensor Report", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 37, "Genre": "Config", "Help": "Enable/disable sensor binary report when motion event is detected or cleared", "ValueIDKey": 1125900537823252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1407375514533908/,{ "Label": "Disable BASIC_SET to Associated nodes", "Value": { "List": [ { "Value": 0, "Label": "Disabled All Group Basic Set Command" }, { "Value": 1, "Label": "Enabled Group 2" }, { "Value": 2, "Label": "Enabled Group 3 " }, { "Value": 3, "Label": "Enabled Group 2 and Group 3" } ], "Selected": "Enabled Group 2 and Group 3" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the enabled or disabled send BASIC_SET command to nodes that associated in group 2 and group 3.", "ValueIDKey": 1407375514533908, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1688850491244564/,{ "Label": "Basic Set Value Settings for Group 2", "Value": { "List": [ { "Value": 0, "Label": "0xFF when motion is triggered and 0x00 when motion is cleared" }, { "Value": 1, "Label": "0x00 when motion is triggered and 0xFF when motion is cleared" }, { "Value": 2, "Label": "0xFF when motion is triggered" }, { "Value": 3, "Label": "0x00 when motion is triggered" }, { "Value": 4, "Label": "0x00 when motion event is cleared" }, { "Value": 5, "Label": "0xFF when motion event is cleared" } ], "Selected": "0xFF when motion is triggered and 0x00 when motion is cleared" }, "Units": "", "Min": 0, "Max": 5, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 37, "Genre": "Config", "Help": "Define Basic Set Value when motion event is triggered and / or cleared", "ValueIDKey": 1688850491244564, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/1970325467955222/,{ "Label": "Temperature Alarm Value", "Value": 239, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the threshold value that alarm level for temperature. When the current ambient temperature value is larger than this configuration value, device will send a BASIC_SET = 0xFF to nodes associated in group 3. If current temperature value is less than this value, device will send a BASIC_SET = 0x00 to nodes associated in group 3. Value = [Value] x 0.1(Celsius / Fahrenheit) Available Settings: -400 to 850 (40.0 to 85.0 Celsius) or -400 to 1185 (-40.0 to 118.5 Fahrenheit). Default value: 239 (23.9 Celsius) or 750 (75.0 Fahrenheit)", "ValueIDKey": 1970325467955222, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/2814750398087188/,{ "Label": "LED over TriSensor", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 37, "Genre": "Config", "Help": "Enable or Disable LED over TriSensor This completely disables all LED reaction regardless of Parameter 9 - 13 settings", "ValueIDKey": 2814750398087188, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3096225374797844/,{ "Label": "Motion report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Green" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a motion report.", "ValueIDKey": 3096225374797844, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3377700351508500/,{ "Label": "Temperature report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a temperature report.", "ValueIDKey": 3377700351508500, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3659175328219156/,{ "Label": "Light report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a light report.", "ValueIDKey": 3659175328219156, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/3940650304929812/,{ "Label": "Battery report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 37, "Genre": "Config", "Help": "It is possible to change the color of what the LED blinks when your TriSensor sends a battery report.", "ValueIDKey": 3940650304929812, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/4222125281640468/,{ "Label": "Wakeup report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a wakeup report.", "ValueIDKey": 4222125281640468, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Temperature Scale Setting", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "Configure temperature sensor scale type, Temperature to report in Celsius or Fahrenheit", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/5910975141904406/,{ "Label": "Temperature Threshold reporting", "Value": 20, "Units": "0.1", "Min": 0, "Max": 250, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in temperature to induce an automatic report for temperature sensor. Scale is identical setting in Parameter No.20. 0-> Disable Threshold Report for Temperature Sensor. Setting of value 20 can be a change of -2.0 or +2.0 (C or F depending on Parameter No.20) to induce automatic report or setting a value of 2 will be a change of 0.2(C or F). Available Settings: 0 to 250.", "ValueIDKey": 5910975141904406, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6192450118615062/,{ "Label": "Light intensity Threshold Value to Report", "Value": 100, "Units": "Lux", "Min": 0, "Max": 10000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 22, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in lux to induce an automatic report for light sensor.", "ValueIDKey": 6192450118615062, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6473925095325718/,{ "Label": "Temperature Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 23, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for temperature sensor report. This value is larger, the battery life is longer. And the temperature value changed is not obvious.", "ValueIDKey": 6473925095325718, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036374/,{ "Label": "Light Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for light sensor report. This value is larger, the battery life is longer. And the light intensity changed is not obvious.", "ValueIDKey": 6755400072036374, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300310/,{ "Label": "Temperature Offset Value", "Value": 0, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "The current measuring temperature value can be add and minus a value by this setting. The scale can be decided by Parameter Number 20. Temperature Offset Value = [Value] * 0.1(Celsius / Fahrenheit) Available Settings: -200 to 200.", "ValueIDKey": 8444249932300310, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010966/,{ "Label": "Light Intensity Offset Value", "Value": 0, "Units": "Lux", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "The current measuring light intensity value can be add and minus a value by this setting. Available Settings: -1000 to 1000.", "ValueIDKey": 8725724909010966, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/112/value/28147498302046230/,{ "Label": "Light Sensor Calibrated Coefficient", "Value": 1024, "Units": "", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 37, "Genre": "Config", "Help": "This configuration defines the calibrated scale for ambient light intensity. Because the method and position that the sensor mounted and the cover of sensor will bring measurement error, user can get more real light intensity by this parameter setting. User should run the steps as blows for calibrating 1) Set this parameter value to default (Assumes the sensor has been added in a Z- Wave Network). 2) Place a digital light meter close to sensor and keep the same direction, monitor the light intensity value (Vm) which report to controller and record it. The same time user should record the value (Vs) of light meter. 3) The scale calibration formula: k = Vm / Vs. 4) The value of k is then multiplied by 1024 and rounded to the nearest whole number. 5) Set the value getting in 5) to this parameter, calibrate finished. For example, Vm = 300, Vs = 2600, then k = 2600 / 300 = 8.6667 k = 8.6667 * 1024 = 8874.7 => 8875. The parameter should be set to 8875.", "ValueIDKey": 28147498302046230, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/,{ "Label": "Home Security", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 8, "Label": "Motion Detected at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 7, "Node": 37, "Genre": "User", "Help": "Home Security Alerts", "ValueIDKey": 1970325463777300, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/128/value/627048465/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 627048465, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/281475612213267/,{ "Label": "Minimum Wake-up Interval", "Value": 1800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 37, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475612213267, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/562950588923923/,{ "Label": "Maximum Wake-up Interval", "Value": 64800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 37, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950588923923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/844425565634579/,{ "Label": "Default Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 37, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425565634579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/1125900542345235/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 37, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900542345235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/132/value/635502611/,{ "Label": "Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 37, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 635502611, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "2.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0", "1.1" ], "TimeStamp": 1579566891} -OpenZWave/1/node/37/association/2/,{ "Name": "BasicSet report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/37/association/3/,{ "Name": "Temperature Alarm report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} -OpenZWave/1/node/39/statistics/,{ "sendCount": 57, "sentFailed": 0, "retries": 1, "receivedPackets": 3594, "receivedDupPackets": 12, "receivedUnsolicited": 3546, "lastSentTimeStamp": 1595764791, "lastReceivedTimeStamp": 1595802261, "lastRequestRTT": 26, "averageRequestRTT": 29, "lastResponseRTT": 38, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/statistics/,{ "SOFCnt": 92220, "ACKWaiting": 0, "readAborts": 0, "badChecksum": 0, "readCnt": 92220, "writeCnt": 2150, "CANCnt": 0, "NAKCnt": 0, "ACKCnt": 2150, "OOFCnt": 0, "dropped": 27, "retries": 0, "callbacks": 1, "badroutes": 0, "noack": 18, "netbusy": 0, "notidle": 0, "txverified": 0, "nondelivery": 0, "routedbusy": 0, "broadcastReadCnt": 42190, "broadcastWriteCnt": 25} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light.json b/tests/components/ozw/fixtures/light.json deleted file mode 100644 index 81fd82a4a2b..00000000000 --- a/tests/components/ozw/fixtures/light.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/", - "payload": { - "Label": "Level", - "Value": 0, - "Units": "", - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 39, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 659128337, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/light_network_dump.csv b/tests/components/ozw/fixtures/light_network_dump.csv deleted file mode 100644 index e9c0d8fb74b..00000000000 --- a/tests/components/ozw/fixtures/light_network_dump.csv +++ /dev/null @@ -1,320 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0900", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgrgbwm441.png", "Description": "RGBW Controller", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "FIBARO RGBW Dimmer", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1597142985, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGRGBWM441 RGBW Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0900", "NodeProductID": "0x1000", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 5, "NodeName": "Kitchen RGB Strip", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ]} -OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/,{ "Label": "Color", "Value": "#FFFFFF00", "Units": "#RRGGBBWW", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 122470423, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/281475099181076/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Cool White", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 7, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475099181076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Enable/Disable ALL ON/OFF", "Value": { "List": [ { "Value": 0, "Label": "ALL ON disabled/ ALL OFF disabled" }, { "Value": 1, "Label": "ALL ON disabled/ ALL OFF active" }, { "Value": 2, "Label": "ALL ON active / ALL OFF disabled" }, { "Value": 255, "Label": "ALL ON active / ALL OFF active" } ], "Selected": "ALL ON active / ALL OFF active", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "Enable/Disable ALL ON/OFF", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Associations command class choice", "Value": { "List": [ { "Value": 0, "Label": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 1, "Label": "Normal (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE" }, { "Value": 2, "Label": "Normal (RGBW) - COLOR_CONTROL_SET" }, { "Value": 3, "Label": "Brightness - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 4, "Label": "Rainbow (RGBW) - COLOR_CONTROL_SET" } ], "Selected": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "Choose which command classes are sent to associated devices.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Outputs state change mode", "Value": { "List": [ { "Value": 0, "Label": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)" }, { "Value": 1, "Label": "MODE 2 - Constant Time (RGB/RBGW only. Time is defined by parameter 11)" } ], "Selected": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "Choose the behaviour of transitions between different levels.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060049/,{ "Label": "Dimming step value (for MODE 1)", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "Size of the step for each change in level during the transition.", "ValueIDKey": 2533274918060049, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Time between dimming steps (for MODE 1)", "Value": 10, "Units": "ms", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 60000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "Time between each step in a transition between levels. Setting this to zero means an instantaneous change.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481361/,{ "Label": "Time to complete the entire transition (for MODE 2)", "Value": 67, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "0 - immediate change; 1->63: 20ms->126ms (value*20ms); 65->127: 1s->63s (value-64)*1s; 129->191: 10s->630s (value-128)*10s; 193->255: 1min->63min (value-192)*1min. Default setting: 67 (3s)", "ValueIDKey": 3096224871481361, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Maximum dimmer level", "Value": 255, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 3, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "Maximum brightness level for the dimmer", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3659174824902673/,{ "Label": "Minimum dimmer level", "Value": 2, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 2, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 7, "Genre": "Config", "Help": "Minimum brightness level for the dimmer", "ValueIDKey": 3659174824902673, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3940649801613334/,{ "Label": "Inputs / Outputs configuration", "Value": 4369, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 7, "Genre": "Config", "Help": "This is too complex to describe here, since this value is built up from 4-bits for each of the 4 channels. Refer to the table in the product manual. Default value is 4369 (1111 in hex).", "ValueIDKey": 3940649801613334, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4222124778323988/,{ "Label": "Option double click", "Value": { "List": [ { "Value": 0, "Label": "Double click disabled" }, { "Value": 1, "Label": "Double click enabled" } ], "Selected": "Double click enabled", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 7, "Genre": "Config", "Help": "Option double click (lighting set at 100%). 0 - Double click disabled, 1 - Double click enabled. Default setting 1", "ValueIDKey": 4222124778323988, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4503599755034644/,{ "Label": "Saving state before power failure", "Value": { "List": [ { "Value": 0, "Label": "State NOT saved at power failure, all outputs are set to OFF upon power restore" }, { "Value": 1, "Label": "State saved at power failure, all outputs are set to previous state upon power restore" } ], "Selected": "State saved at power failure, all outputs are set to previous state upon power restore", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 7, "Genre": "Config", "Help": "Saving state before power failure", "ValueIDKey": 4503599755034644, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/8444249428983828/,{ "Label": "Alarm", "Value": { "List": [ { "Value": 0, "Label": "INACTIVE - no response to alarm frames" }, { "Value": 1, "Label": "ALARM ON - the device turns on once alarm is detected (all channels set to 99%)" }, { "Value": 2, "Label": "ALARM OFF - the device turns off once alarm is detected (all channels set to 0%)" }, { "Value": 3, "Label": "ALARM PROGRAM - alarm sequence turns on (program selected in parameter 38)" } ], "Selected": "INACTIVE - no response to alarm frames", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 7, "Genre": "Config", "Help": "Alarm of any type (general alarm, water flooding alarm, smoke alarm: CO, CO2, temperature alarm). Default setting 0 (Inactive)", "ValueIDKey": 8444249428983828, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10696049242669073/,{ "Label": "Alarm sequence program", "Value": 10, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 38, "Node": 7, "Genre": "Config", "Help": "Program number selected from the 10 available.", "ValueIDKey": 10696049242669073, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10977524219379734/,{ "Label": "Active PROGRAM alarm time", "Value": 600, "Units": "s", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 7, "Genre": "Config", "Help": "In ALARM PROGRAM mode (see parameter 30), this defines the time in seconds the program lasts (1s->65534s)", "ValueIDKey": 10977524219379734, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/11821949149511700/,{ "Label": "Command class reporting Outputs status change", "Value": { "List": [ { "Value": 0, "Label": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)" }, { "Value": 1, "Label": "Reporting as a result inputs actions (SWITCH MULTILEVEL)" }, { "Value": 2, "Label": "Reporting as a result inputs actions (COLOUR_CONTROL)" } ], "Selected": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 42, "Node": 7, "Genre": "Config", "Help": "Specify which command class is used to report output status changes", "ValueIDKey": 11821949149511700, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12103424126222353/,{ "Label": "Reporting 0-10v analog inputs change threshold", "Value": 5, "Units": "*0.1V", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 43, "Node": 7, "Genre": "Config", "Help": "Parameter defines a value by which input voltage must change in order to be reported to the main controller. New value is calculated based on last reported value: Default setting: 5 (0.5V). Range: 1->100 - (0.1V->10V).", "ValueIDKey": 12103424126222353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12384899102933014/,{ "Label": "Power load reporting frequency", "Value": 30, "Units": "s", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 44, "Node": 7, "Genre": "Config", "Help": "Sent if last reported value differs from the current value. Reports will also be sent in case of polling. Default setting: 30 (30s). Range: 1->65534 (1s->65534s) - interval between reports. Zero means reports are only sent in the case of polling, or at turning OFF the device", "ValueIDKey": 12384899102933014, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12666374079643665/,{ "Label": "Reporting changes in energy consumed by controlled devices", "Value": 10, "Units": "*0.01kWh", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 254, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 45, "Node": 7, "Genre": "Config", "Help": "Interval between energy consumption reports (in kWh). New reported energy consumption value is calculated based on last reported value. 1->254 (0.01kWh->2.54kWh). Zero means changes in consumed energy will not be reported, except in case of polling.", "ValueIDKey": 12666374079643665, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/19984723474120724/,{ "Label": "Response to BRIGHTNESS set to 0%", "Value": { "List": [ { "Value": 0, "Label": "Illumination colour set to white (all channels controlled together)" }, { "Value": 1, "Label": "Last set colour is memorized" } ], "Selected": "Last set colour is memorized", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 71, "Node": 7, "Genre": "Config", "Help": "Set whether to remember the previous RGB mix after the brightness has fallen to zero (black)", "ValueIDKey": 19984723474120724, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20266198450831377/,{ "Label": "Starting predefined program", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 72, "Node": 7, "Genre": "Config", "Help": "First predefined program to use when device is set to work in RGB/RGBW mode (parameter 14)", "ValueIDKey": 20266198450831377, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20547673427542036/,{ "Label": "Triple Click Action", "Value": { "List": [ { "Value": 0, "Label": "NODE INFO control frame is sent" }, { "Value": 1, "Label": "Start favourite program" } ], "Selected": "NODE INFO control frame is sent", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 73, "Node": 7, "Genre": "Config", "Help": "Behaviour when an input is triple-clicked", "ValueIDKey": 20547673427542036, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/122257425/,{ "Label": "Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 7, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 122257425, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/38/value/281475098968088/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475098968088, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/562950075678744/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 7, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950075678744, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/844425060778000/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425060778000, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/1125900037488657/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900037488657, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/value/130662420/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 130662420, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/association/1/,{ "Name": "Input 1", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/2/,{ "Name": "Input 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/3/,{ "Name": "Input 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/4/,{ "Name": "Input 4", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/5/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1597142799} -OpenZWave/1/node/7/statistics/,{ "sendCount": 9, "sentFailed": 0, "retries": 0, "receivedPackets": 8, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597142969, "lastReceivedTimeStamp": 1597142969, "lastRequestRTT": 26, "averageRequestRTT": 42, "lastResponseRTT": 55, "averageResponseRTT": 62, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Enable/Disable ALL ON/OFF", "Value": { "List": [ { "Value": 0, "Label": "ALL ON disabled/ ALL OFF disabled" }, { "Value": 1, "Label": "ALL ON disabled/ ALL OFF active" }, { "Value": 2, "Label": "ALL ON active / ALL OFF disabled" }, { "Value": 255, "Label": "ALL ON active / ALL OFF active" } ], "Selected": "ALL ON active / ALL OFF active", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "Enable/Disable ALL ON/OFF", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Associations command class choice", "Value": { "List": [ { "Value": 0, "Label": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 1, "Label": "Normal (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE" }, { "Value": 2, "Label": "Normal (RGBW) - COLOR_CONTROL_SET" }, { "Value": 3, "Label": "Brightness - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 4, "Label": "Rainbow (RGBW) - COLOR_CONTROL_SET" } ], "Selected": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "Choose which command classes are sent to associated devices.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Outputs state change mode", "Value": { "List": [ { "Value": 0, "Label": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)" }, { "Value": 1, "Label": "MODE 2 - Constant Time (RGB/RBGW only. Time is defined by parameter 11)" } ], "Selected": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "Choose the behaviour of transitions between different levels.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060049/,{ "Label": "Dimming step value (for MODE 1)", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "Size of the step for each change in level during the transition.", "ValueIDKey": 2533274918060049, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Time between dimming steps (for MODE 1)", "Value": 10, "Units": "ms", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 60000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "Time between each step in a transition between levels. Setting this to zero means an instantaneous change.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481361/,{ "Label": "Time to complete the entire transition (for MODE 2)", "Value": 67, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "0 - immediate change; 1->63: 20ms->126ms (value*20ms); 65->127: 1s->63s (value-64)*1s; 129->191: 10s->630s (value-128)*10s; 193->255: 1min->63min (value-192)*1min. Default setting: 67 (3s)", "ValueIDKey": 3096224871481361, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Maximum dimmer level", "Value": 255, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 3, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "Maximum brightness level for the dimmer", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3659174824902673/,{ "Label": "Minimum dimmer level", "Value": 2, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 2, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 7, "Genre": "Config", "Help": "Minimum brightness level for the dimmer", "ValueIDKey": 3659174824902673, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3940649801613334/,{ "Label": "Inputs / Outputs configuration", "Value": 4369, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 7, "Genre": "Config", "Help": "This is too complex to describe here, since this value is built up from 4-bits for each of the 4 channels. Refer to the table in the product manual. Default value is 4369 (1111 in hex).", "ValueIDKey": 3940649801613334, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4222124778323988/,{ "Label": "Option double click", "Value": { "List": [ { "Value": 0, "Label": "Double click disabled" }, { "Value": 1, "Label": "Double click enabled" } ], "Selected": "Double click enabled", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 7, "Genre": "Config", "Help": "Option double click (lighting set at 100%). 0 - Double click disabled, 1 - Double click enabled. Default setting 1", "ValueIDKey": 4222124778323988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4503599755034644/,{ "Label": "Saving state before power failure", "Value": { "List": [ { "Value": 0, "Label": "State NOT saved at power failure, all outputs are set to OFF upon power restore" }, { "Value": 1, "Label": "State saved at power failure, all outputs are set to previous state upon power restore" } ], "Selected": "State saved at power failure, all outputs are set to previous state upon power restore", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 7, "Genre": "Config", "Help": "Saving state before power failure", "ValueIDKey": 4503599755034644, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/8444249428983828/,{ "Label": "Alarm", "Value": { "List": [ { "Value": 0, "Label": "INACTIVE - no response to alarm frames" }, { "Value": 1, "Label": "ALARM ON - the device turns on once alarm is detected (all channels set to 99%)" }, { "Value": 2, "Label": "ALARM OFF - the device turns off once alarm is detected (all channels set to 0%)" }, { "Value": 3, "Label": "ALARM PROGRAM - alarm sequence turns on (program selected in parameter 38)" } ], "Selected": "INACTIVE - no response to alarm frames", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 7, "Genre": "Config", "Help": "Alarm of any type (general alarm, water flooding alarm, smoke alarm: CO, CO2, temperature alarm). Default setting 0 (Inactive)", "ValueIDKey": 8444249428983828, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10696049242669073/,{ "Label": "Alarm sequence program", "Value": 10, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 38, "Node": 7, "Genre": "Config", "Help": "Program number selected from the 10 available.", "ValueIDKey": 10696049242669073, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10977524219379734/,{ "Label": "Active PROGRAM alarm time", "Value": 600, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 7, "Genre": "Config", "Help": "In ALARM PROGRAM mode (see parameter 30), this defines the time in seconds the program lasts (1s->65534s)", "ValueIDKey": 10977524219379734, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/11821949149511700/,{ "Label": "Command class reporting Outputs status change", "Value": { "List": [ { "Value": 0, "Label": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)" }, { "Value": 1, "Label": "Reporting as a result inputs actions (SWITCH MULTILEVEL)" }, { "Value": 2, "Label": "Reporting as a result inputs actions (COLOUR_CONTROL)" } ], "Selected": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 42, "Node": 7, "Genre": "Config", "Help": "Specify which command class is used to report output status changes", "ValueIDKey": 11821949149511700, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12103424126222353/,{ "Label": "Reporting 0-10v analog inputs change threshold", "Value": 5, "Units": "*0.1V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 43, "Node": 7, "Genre": "Config", "Help": "Parameter defines a value by which input voltage must change in order to be reported to the main controller. New value is calculated based on last reported value: Default setting: 5 (0.5V). Range: 1->100 - (0.1V->10V).", "ValueIDKey": 12103424126222353, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12384899102933014/,{ "Label": "Power load reporting frequency", "Value": 30, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 44, "Node": 7, "Genre": "Config", "Help": "Sent if last reported value differs from the current value. Reports will also be sent in case of polling. Default setting: 30 (30s). Range: 1->65534 (1s->65534s) - interval between reports. Zero means reports are only sent in the case of polling, or at turning OFF the device", "ValueIDKey": 12384899102933014, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12666374079643665/,{ "Label": "Reporting changes in energy consumed by controlled devices", "Value": 10, "Units": "*0.01kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 254, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 45, "Node": 7, "Genre": "Config", "Help": "Interval between energy consumption reports (in kWh). New reported energy consumption value is calculated based on last reported value. 1->254 (0.01kWh->2.54kWh). Zero means changes in consumed energy will not be reported, except in case of polling.", "ValueIDKey": 12666374079643665, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/19984723474120724/,{ "Label": "Response to BRIGHTNESS set to 0%", "Value": { "List": [ { "Value": 0, "Label": "Illumination colour set to white (all channels controlled together)" }, { "Value": 1, "Label": "Last set colour is memorized" } ], "Selected": "Last set colour is memorized", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 71, "Node": 7, "Genre": "Config", "Help": "Set whether to remember the previous RGB mix after the brightness has fallen to zero (black)", "ValueIDKey": 19984723474120724, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20266198450831377/,{ "Label": "Starting predefined program", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 72, "Node": 7, "Genre": "Config", "Help": "First predefined program to use when device is set to work in RGB/RGBW mode (parameter 14)", "ValueIDKey": 20266198450831377, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20547673427542036/,{ "Label": "Triple Click Action", "Value": { "List": [ { "Value": 0, "Label": "NODE INFO control frame is sent" }, { "Value": 1, "Label": "Start favourite program" } ], "Selected": "NODE INFO control frame is sent", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 73, "Node": 7, "Genre": "Config", "Help": "Behaviour when an input is triple-clicked", "ValueIDKey": 20547673427542036, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0900", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgrgbwm441.png", "Description": "RGBW Controller", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "FIBARO RGBW Dimmer", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1597143009, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGRGBWM441 RGBW Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0900", "NodeProductID": "0x1000", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 5, "NodeName": "Kitchen RGB Strip", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ], "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ]} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/010F:1000:0900", "ZWAProductURL": "", "ProductPic": "images/fibaro/fgrgbwm441.png", "Description": "RGBW Controller", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "FIBARO RGBW Dimmer", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1597143009, "NodeManufacturerName": "FIBARO System", "NodeProductName": "FGRGBWM441 RGBW Controller", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x010f", "NodeProductType": "0x0900", "NodeProductID": "0x1000", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 5, "NodeName": "Kitchen RGB Strip", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ], "Neighbors": [ 1, 3, 6, 8, 12, 13, 14, 18, 22, 26, 27, 28 ]} -OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/,{ "Label": "Color", "Value": "#FFFFFF00", "Units": "#RRGGBBWW", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 122470423, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/51/value/281475099181076/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Cool White", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 7, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475099181076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Enable/Disable ALL ON/OFF", "Value": { "List": [ { "Value": 0, "Label": "ALL ON disabled/ ALL OFF disabled" }, { "Value": 1, "Label": "ALL ON disabled/ ALL OFF active" }, { "Value": 2, "Label": "ALL ON active / ALL OFF disabled" }, { "Value": 255, "Label": "ALL ON active / ALL OFF active" } ], "Selected": "ALL ON active / ALL OFF active", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "Enable/Disable ALL ON/OFF", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Associations command class choice", "Value": { "List": [ { "Value": 0, "Label": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 1, "Label": "Normal (RGBW) - COLOR_CONTROL_SET/START/STOP_STATE_CHANGE" }, { "Value": 2, "Label": "Normal (RGBW) - COLOR_CONTROL_SET" }, { "Value": 3, "Label": "Brightness - BASIC SET/SWITCH_MULTILEVEL_START/STOP" }, { "Value": 4, "Label": "Rainbow (RGBW) - COLOR_CONTROL_SET" } ], "Selected": "Normal (Dimmer) - BASIC SET/SWITCH_MULTILEVEL_START/STOP", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "Choose which command classes are sent to associated devices.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Outputs state change mode", "Value": { "List": [ { "Value": 0, "Label": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)" }, { "Value": 1, "Label": "MODE 2 - Constant Time (RGB/RBGW only. Time is defined by parameter 11)" } ], "Selected": "MODE 1 - Constant Speed (speed is defined by parameters 9 and 10)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "Choose the behaviour of transitions between different levels.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143008} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060049/,{ "Label": "Dimming step value (for MODE 1)", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "Size of the step for each change in level during the transition.", "ValueIDKey": 2533274918060049, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Time between dimming steps (for MODE 1)", "Value": 10, "Units": "ms", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 60000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "Time between each step in a transition between levels. Setting this to zero means an instantaneous change.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481361/,{ "Label": "Time to complete the entire transition (for MODE 2)", "Value": 67, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "0 - immediate change; 1->63: 20ms->126ms (value*20ms); 65->127: 1s->63s (value-64)*1s; 129->191: 10s->630s (value-128)*10s; 193->255: 1min->63min (value-192)*1min. Default setting: 67 (3s)", "ValueIDKey": 3096224871481361, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Maximum dimmer level", "Value": 255, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 3, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "Maximum brightness level for the dimmer", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3659174824902673/,{ "Label": "Minimum dimmer level", "Value": 2, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 2, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 7, "Genre": "Config", "Help": "Minimum brightness level for the dimmer", "ValueIDKey": 3659174824902673, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/3940649801613334/,{ "Label": "Inputs / Outputs configuration", "Value": 4369, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 7, "Genre": "Config", "Help": "This is too complex to describe here, since this value is built up from 4-bits for each of the 4 channels. Refer to the table in the product manual. Default value is 4369 (1111 in hex).", "ValueIDKey": 3940649801613334, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4222124778323988/,{ "Label": "Option double click", "Value": { "List": [ { "Value": 0, "Label": "Double click disabled" }, { "Value": 1, "Label": "Double click enabled" } ], "Selected": "Double click enabled", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 7, "Genre": "Config", "Help": "Option double click (lighting set at 100%). 0 - Double click disabled, 1 - Double click enabled. Default setting 1", "ValueIDKey": 4222124778323988, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/4503599755034644/,{ "Label": "Saving state before power failure", "Value": { "List": [ { "Value": 0, "Label": "State NOT saved at power failure, all outputs are set to OFF upon power restore" }, { "Value": 1, "Label": "State saved at power failure, all outputs are set to previous state upon power restore" } ], "Selected": "State saved at power failure, all outputs are set to previous state upon power restore", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 7, "Genre": "Config", "Help": "Saving state before power failure", "ValueIDKey": 4503599755034644, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/8444249428983828/,{ "Label": "Alarm", "Value": { "List": [ { "Value": 0, "Label": "INACTIVE - no response to alarm frames" }, { "Value": 1, "Label": "ALARM ON - the device turns on once alarm is detected (all channels set to 99%)" }, { "Value": 2, "Label": "ALARM OFF - the device turns off once alarm is detected (all channels set to 0%)" }, { "Value": 3, "Label": "ALARM PROGRAM - alarm sequence turns on (program selected in parameter 38)" } ], "Selected": "INACTIVE - no response to alarm frames", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 7, "Genre": "Config", "Help": "Alarm of any type (general alarm, water flooding alarm, smoke alarm: CO, CO2, temperature alarm). Default setting 0 (Inactive)", "ValueIDKey": 8444249428983828, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10696049242669073/,{ "Label": "Alarm sequence program", "Value": 10, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 38, "Node": 7, "Genre": "Config", "Help": "Program number selected from the 10 available.", "ValueIDKey": 10696049242669073, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/10977524219379734/,{ "Label": "Active PROGRAM alarm time", "Value": 600, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 7, "Genre": "Config", "Help": "In ALARM PROGRAM mode (see parameter 30), this defines the time in seconds the program lasts (1s->65534s)", "ValueIDKey": 10977524219379734, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/11821949149511700/,{ "Label": "Command class reporting Outputs status change", "Value": { "List": [ { "Value": 0, "Label": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)" }, { "Value": 1, "Label": "Reporting as a result inputs actions (SWITCH MULTILEVEL)" }, { "Value": 2, "Label": "Reporting as a result inputs actions (COLOUR_CONTROL)" } ], "Selected": "Reporting as a result of inputs and controllers actions (SWITCH MULTILEVEL)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 42, "Node": 7, "Genre": "Config", "Help": "Specify which command class is used to report output status changes", "ValueIDKey": 11821949149511700, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12103424126222353/,{ "Label": "Reporting 0-10v analog inputs change threshold", "Value": 5, "Units": "*0.1V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 43, "Node": 7, "Genre": "Config", "Help": "Parameter defines a value by which input voltage must change in order to be reported to the main controller. New value is calculated based on last reported value: Default setting: 5 (0.5V). Range: 1->100 - (0.1V->10V).", "ValueIDKey": 12103424126222353, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12384899102933014/,{ "Label": "Power load reporting frequency", "Value": 30, "Units": "s", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 65534, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 44, "Node": 7, "Genre": "Config", "Help": "Sent if last reported value differs from the current value. Reports will also be sent in case of polling. Default setting: 30 (30s). Range: 1->65534 (1s->65534s) - interval between reports. Zero means reports are only sent in the case of polling, or at turning OFF the device", "ValueIDKey": 12384899102933014, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/12666374079643665/,{ "Label": "Reporting changes in energy consumed by controlled devices", "Value": 10, "Units": "*0.01kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 254, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 45, "Node": 7, "Genre": "Config", "Help": "Interval between energy consumption reports (in kWh). New reported energy consumption value is calculated based on last reported value. 1->254 (0.01kWh->2.54kWh). Zero means changes in consumed energy will not be reported, except in case of polling.", "ValueIDKey": 12666374079643665, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/19984723474120724/,{ "Label": "Response to BRIGHTNESS set to 0%", "Value": { "List": [ { "Value": 0, "Label": "Illumination colour set to white (all channels controlled together)" }, { "Value": 1, "Label": "Last set colour is memorized" } ], "Selected": "Last set colour is memorized", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 71, "Node": 7, "Genre": "Config", "Help": "Set whether to remember the previous RGB mix after the brightness has fallen to zero (black)", "ValueIDKey": 19984723474120724, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20266198450831377/,{ "Label": "Starting predefined program", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 10, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 72, "Node": 7, "Genre": "Config", "Help": "First predefined program to use when device is set to work in RGB/RGBW mode (parameter 14)", "ValueIDKey": 20266198450831377, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/112/value/20547673427542036/,{ "Label": "Triple Click Action", "Value": { "List": [ { "Value": 0, "Label": "NODE INFO control frame is sent" }, { "Value": 1, "Label": "Start favourite program" } ], "Selected": "NODE INFO control frame is sent", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 73, "Node": 7, "Genre": "Config", "Help": "Behaviour when an input is triple-clicked", "ValueIDKey": 20547673427542036, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597143009} -OpenZWave/1/node/7/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/122257425/,{ "Label": "Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 7, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 122257425, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/38/value/281475098968088/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475098968088, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/562950075678744/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 7, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950075678744, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/844425060778000/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425060778000, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/38/value/1125900037488657/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900037488657, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/39/value/130662420/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 130662420, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1597142969} -OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 0, "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1597142799} -OpenZWave/1/node/7/association/1/,{ "Name": "Input 1", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/2/,{ "Name": "Input 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/3/,{ "Name": "Input 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/4/,{ "Name": "Input 4", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1597142866} -OpenZWave/1/node/7/association/5/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1597142799} -OpenZWave/1/node/7/statistics/,{ "sendCount": 30, "sentFailed": 0, "retries": 0, "receivedPackets": 29, "receivedDupPackets": 1, "receivedUnsolicited": 0, "lastSentTimeStamp": 1597143009, "lastReceivedTimeStamp": 1597143009, "lastRequestRTT": 26, "averageRequestRTT": 25, "lastResponseRTT": 37, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/2/,{ "NodeID": 2, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0063:3031:4944", "ZWAProductURL": "", "ProductPic": "images/ge/12724-dimmer.png", "Description": "Transform any home into a smart home with the GE Z-Wave Smart Fan Control. The in-wall fan control easily replaces any standard in-wall switch remotely controls a ceiling fan in your home and features a three-speed control system. Your home will be equipped with ultimate flexibility with the GE Z-Wave Smart Fan Control, capable of being used by itself or with up to four GE add-on switches. Screw terminal installation provides improved space efficiency when replacing existing switches and the integrated LED indicator light allows you to easily locate the switch in a dark room. The GE Z-Wave Smart Fan Control is compatible with any Z-Wave certified gateway, providing access to many popular home automation systems. Take control of your home lighting with GE Z-Wave Smart Lighting Controls!", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2506/Binder2.pdf", "ProductPageURL": "http://www.ezzwave.com", "InclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to include a device to the Z-Wave network. 2. Once the controller is ready to include your device, press and release the top or bottom of the smart fan control switch (rocker) to include it in the network. 3. Once your controller has confirmed the device has been included, refresh the Z-Wave network to optimize performance.", "ExclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to exclude a device from the Z-Wave network. 2. Once the controller is ready to Exclude your device, press and release the top or bottom of the wireless smart switch (rocker) to exclude it from the network.", "ResetHelp": "1. Quickly press ON (Top) button three (3) times then immediately press the OFF (Bottom) button three (3) times. The LED will flash ON/OFF 5 times when completed successfully. Note: This should only be used in the event your network’s primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "In-Wall Smart Fan Control", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1594407756, "NodeManufacturerName": "GE (Jasco Products)", "NodeProductName": "12724 3-Way Dimmer Switch", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0063", "NodeProductType": "0x4944", "NodeProductID": "0x3031", "NodeBaudRate": 40000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "Master_Bedroom_L", "NodeLocation": "Master Bedroom", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 6, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 23, 24, 26, 27, 28, 29, 30, 33 ], "Neighbors": [ 1, 6, 9, 10, 11, 12, 15, 16, 17, 18, 19, 20, 21, 23, 24, 26, 27, 28, 29, 30, 33 ]} -OpenZWave/1/node/2/instance/1/,{ "Instance": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/112/value/844424973910036/,{ "Label": "LED Light", "Value": { "List": [ { "Value": 0, "Label": "LED on when light off" }, { "Value": 1, "Label": "LED on when light on" }, { "Value": 2, "Label": "LED always off" } ], "Selected": "LED on when light on", "Selected_id": 1 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 2, "Genre": "Config", "Help": "Sets when the LED on the switch is lit.", "ValueIDKey": 844424973910036, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407754} -OpenZWave/1/node/2/instance/1/commandclass/112/value/1125899950620692/,{ "Label": "Invert Switch", "Value": { "List": [ { "Value": 0, "Label": "No" }, { "Value": 1, "Label": "Yes" } ], "Selected": "No", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 2, "Genre": "Config", "Help": "Change the top of the switch to OFF and the bottom of the switch to ON, if the switch was installed upside down.", "ValueIDKey": 1125899950620692, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407754} -OpenZWave/1/node/2/instance/1/commandclass/112/value/1970324880752657/,{ "Label": "Z-Wave Command Dim Step", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 2, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 1970324880752657, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/2251799857463313/,{ "Label": "Z-Wave Command Dim Rate", "Value": 1, "Units": "x 10 milliseconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 2, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2251799857463313, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/2533274834173969/,{ "Label": "Local Control Dim Step", "Value": 1, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 2, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 2533274834173969, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/2814749810884625/,{ "Label": "Local Control Dim Rate", "Value": 5, "Units": "x 10 milliseconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 2, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2814749810884625, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/3096224787595281/,{ "Label": "ALL ON/ALL OFF Dim Step", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 2, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 3096224787595281, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407755} -OpenZWave/1/node/2/instance/1/commandclass/112/value/3377699764305937/,{ "Label": "ALL ON/ALL OFF Dim Rate", "Value": 5, "Units": "x 10 milliseconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 2, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 3377699764305937, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407756} -OpenZWave/1/node/2/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/38371345/,{ "Label": "Level", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 2, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 38371345, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407637} -OpenZWave/1/node/2/instance/1/commandclass/38/value/281475015082008/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 2, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475015082008, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/562949991792664/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 2, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562949991792664, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/844424976891920/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 2, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844424976891920, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/38/value/1125899953602577/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 2, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125899953602577, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/39/value/46776340/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 2, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 46776340, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407636} -OpenZWave/1/node/2/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/48005139/,{ "Label": "Loaded Config Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 2, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 48005139, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/281475024715795/,{ "Label": "Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 2, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475024715795, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/562950001426451/,{ "Label": "Latest Available Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 2, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950001426451, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/844424978137111/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 2, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844424978137111, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/114/value/1125899954847767/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 2, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125899954847767, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/48021524/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 2, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 48021524, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407636} -OpenZWave/1/node/2/instance/1/commandclass/115/value/281475024732177/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 2, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475024732177, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1594407636} -OpenZWave/1/node/2/instance/1/commandclass/115/value/562950001442840/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 2, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950001442840, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/844424978153489/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 2, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844424978153489, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1125899954864148/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 2, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125899954864148, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1407374931574806/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 2, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407374931574806, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1688849908285464/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 2, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849908285464, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/1970324884996120/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 2, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324884996120, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/2251799861706772/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 2, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799861706772, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/115/value/2533274838417430/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 2, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274838417430, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/value/48332823/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 2, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 48332823, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/value/281475025043479/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 2, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475025043479, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/2/instance/1/commandclass/134/value/562950001754135/,{ "Label": "Application Version", "Value": "3.37", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 2, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950001754135, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617} -OpenZWave/1/node/12/,{ "NodeID": 12, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:0063:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw099.png", "Description": "Aeotec Smart Dimmer 6 is a low-cost Z-Wave Dimmer plug-in module specifically used to enable Z-Wave command and control (on/off/dim) of any plug-in tool. It can report immediate wattage consumption or kWh energy usage over a period of time. In the event of power failure, non-volatile memory retains all programmed information relating to the unit’s operating status. Its surface has a Smart RGB LED, which can be used for indicating the output load status or strength of the wireless signal. You can configure its indication colour according to your favour. The Smart Dimmer 6 is also a security Z-Wave device and supports Over The Air (OTA) feature for the products firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2246/Aeon Labs Smart Dimmer 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action button that you can find on the product's housing.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action button that you can find on the product's housing.", "ResetHelp": "Press and hold the Action button that you can find on the product's housing for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Dimmer 6", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1605630092, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW099 Smart Dimmer 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0103", "NodeProductID": "0x0063", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 2, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Light Dimmer Switch", "NodeDeviceType": 1536, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ]} -OpenZWave/1/node/12/instance/1/,{ "Instance": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/value/844425141682196/,{ "Label": "Current Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Deactivate Overload Protection (Default)" }, { "Value": 1, "Label": "Active Overload Protection" } ], "Selected": "Deactivate Overload Protection (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 12, "Genre": "Config", "Help": "Load will be closed when the Current overruns (US 15.5A, Others 16.2) for more than 2 minutes", "ValueIDKey": 844425141682196, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157} -OpenZWave/1/node/12/instance/1/commandclass/112/value/5629499745763348/,{ "Label": "Output Load Status", "Value": { "List": [ { "Value": 0, "Label": "Last status (Default)" }, { "Value": 1, "Label": "Always on" }, { "Value": 2, "Label": "Always off" } ], "Selected": "Last status (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 12, "Genre": "Config", "Help": "Configure the output load status after re-power on.", "ValueIDKey": 5629499745763348, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157} -OpenZWave/1/node/12/instance/1/commandclass/112/value/22517998348402708/,{ "Label": "Notification status", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Hail" }, { "Value": 2, "Label": "Basic" } ], "Selected": "Nothing", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 12, "Genre": "Config", "Help": "Defines the automated status notification of an associated device when status changes", "ValueIDKey": 22517998348402708, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/22799473325113364/,{ "Label": "Configure the state of the LED", "Value": { "List": [ { "Value": 0, "Label": "The LED will follow the status (on/off) of its load. (Default)" }, { "Value": 1, "Label": "When the state of the Switch changes, the LED will follow the status (on/off) of its load, but the LED will turn off after 5 seconds." }, { "Value": 2, "Label": "Night Light Mode" } ], "Selected": "The LED will follow the status (on/off) of its load. (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 12, "Genre": "Config", "Help": "Configure what the LED Ring displays during operations", "ValueIDKey": 22799473325113364, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/23362423278534675/,{ "Label": "Night Light Color", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 83, "Node": 12, "Genre": "Config", "Help": "Configure the RGB Value when in Night Light Mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23362423278534675, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/23643898255245331/,{ "Label": "RGB Brightness in Energy Mode", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 12, "Genre": "Config", "Help": "Configure the brightness level of RGB LED (0%-100%) when it is in Energy Mode/momentary indicate mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23643898255245331, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/25332748115509264/,{ "Label": "Enables/disables parameter 91/92", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 90, "Node": 12, "Genre": "Config", "Help": "Enable/disable Wattage threshold and percent.", "ValueIDKey": 25332748115509264, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158} -OpenZWave/1/node/12/instance/1/commandclass/112/value/25614223092219926/,{ "Label": "Minimum Change to send Report (Watt)", "Value": 25, "Units": "watts", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 91, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum change in wattage for a Report to be sent (default 25 W)", "ValueIDKey": 25614223092219926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/25895698068930577/,{ "Label": "Minimum Change to send Report (%)", "Value": 5, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 92, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum percentage change in wattage for a Report to be sent (Default 5)", "ValueIDKey": 25895698068930577, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28147497882615832/,{ "Label": "Default Group Reports", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 12, "Genre": "Config", "Help": "Set report types for groups 1, 2 and 3 to default.", "ValueIDKey": 28147497882615832, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28428972859326483/,{ "Label": "Report type sent in Reporting Group 1", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28428972859326483, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28710447836037139/,{ "Label": "Report type sent in Reporting Group 2", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 102, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28710447836037139, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/28991922812747795/,{ "Label": "Report type sent in Reporting Group 3", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 103, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28991922812747795, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159} -OpenZWave/1/node/12/instance/1/commandclass/112/value/30962247649722392/,{ "Label": "Set 111 to 113 to default", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 110, "Node": 12, "Genre": "Config", "Help": "Set time interval for sending reports for groups 1, 2 and 3 to default.", "ValueIDKey": 30962247649722392, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/112/value/31243722626433043/,{ "Label": "Send Interval for Reporting Group 1", "Value": 3, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 1 is sent.", "ValueIDKey": 31243722626433043, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/31525197603143699/,{ "Label": "Send Interval for Reporting Group 2", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 112, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 2 is sent.", "ValueIDKey": 31525197603143699, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/31806672579854355/,{ "Label": "Send Interval for Reporting Group 3", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 113, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 3 is sent.", "ValueIDKey": 31806672579854355, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/56294995553681428/,{ "Label": "Partner ID", "Value": { "List": [ { "Value": 0, "Label": "Aeon Labs Standard (Default)" }, { "Value": 1, "Label": "Others" } ], "Selected": "Aeon Labs Standard (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 200, "Node": 12, "Genre": "Config", "Help": "Partner ID", "ValueIDKey": 56294995553681428, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/70931694342635540/,{ "Label": "Configuration Locked", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 12, "Genre": "Config", "Help": "Enable/disable Configuration Locked", "ValueIDKey": 70931694342635540, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160} -OpenZWave/1/node/12/instance/1/commandclass/112/value/71494644296056854/,{ "Label": "Device tag", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 254, "Node": 12, "Genre": "Config", "Help": "Device tag.", "ValueIDKey": 71494644296056854, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629161} -OpenZWave/1/node/12/instance/1/commandclass/112/value/71776119272767512/,{ "Label": "Reset device", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 12, "Genre": "Config", "Help": "Reset to the default configuration.", "ValueIDKey": 71776119272767512, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/1407375098085395/,{ "Label": "Instance 1: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085395, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630293} -OpenZWave/1/node/12/instance/1/commandclass/38/value/206143505/,{ "Label": "Instance 1: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143505, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630090} -OpenZWave/1/node/12/instance/1/commandclass/38/value/281475182854168/,{ "Label": "Instance 1: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854168, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/562950159564824/,{ "Label": "Instance 1: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564824, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/844425144664080/,{ "Label": "Instance 1: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664080, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/38/value/1125900121374737/,{ "Label": "Instance 1: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374737, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/39/value/214548500/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 214548500, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} -OpenZWave/1/node/12/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/51/value/562950168166419/,{ "Label": "Color Channels", "Value": 28, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 12, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950168166419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/51/value/206356503/,{ "Label": "Color", "Value": "#000000", "Units": "#RRGGBB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 12, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 206356503, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/51/value/281475183067156/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Off", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 12, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475183067156, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/94/value/215449617/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449617, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/94/value/281475192160278/,{ "Label": "Instance 1: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160278, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/94/value/562950168870934/,{ "Label": "Instance 1: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870934, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/215777299/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 12, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 215777299, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/281475192487955/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 12, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475192487955, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/562950169198611/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 12, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950169198611, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/114/value/1125900122619927/,{ "Label": "Serial Number", "Value": "0a000100010106040700000108010000000000", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 12, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900122619927, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/215793684/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 215793684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} -OpenZWave/1/node/12/instance/1/commandclass/115/value/281475192504337/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 12, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475192504337, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055} -OpenZWave/1/node/12/instance/1/commandclass/115/value/562950169215000/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 12, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950169215000, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/844425145925649/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425145925649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1125900122636308/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900122636308, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1407375099346966/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375099346966, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1688850076057624/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 12, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850076057624, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/1970325052768280/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 12, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325052768280, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/2251800029478932/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 12, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800029478932, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/115/value/2533275006189590/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 12, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275006189590, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/129/value/207634452/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Tuesday", "Selected_id": 2 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 12, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 207634452, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/129/value/281475184345105/,{ "Label": "Hour", "Value": 11, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 12, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475184345105, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/129/value/562950161055761/,{ "Label": "Minute", "Value": 21, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 12, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950161055761, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630092} -OpenZWave/1/node/12/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/134/value/216104983/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 12, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 216104983, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/134/value/281475192815639/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 12, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475192815639, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/134/value/562950169526295/,{ "Label": "Application Version", "Value": "1.12", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 12, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950169526295, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/50/value/206340114/,{ "Label": "Electric - kWh", "Value": 17.562999725341798, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206340114, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/562950159761426/,{ "Label": "Electric - W", "Value": 9.6899995803833, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 562950159761426, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/1125900113182738/,{ "Label": "Electric - V", "Value": 123.04900360107422, "Units": "V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1125900113182738, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/1407375089893394/,{ "Label": "Electric - A", "Value": 0.08299999684095383, "Units": "A", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1407375089893394, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/72057594244268048/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 72057594244268048, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091} -OpenZWave/1/node/12/instance/1/commandclass/50/value/72339069229367320/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 12, "Genre": "System", "Help": "", "ValueIDKey": 72339069229367320, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/43/value/206225427/,{ "Label": "Scene", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206225427, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/43/value/281475182936083/,{ "Label": "Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 281475182936083, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/1/commandclass/152/value/216399888/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399888, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/,{ "Instance": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/1407375098085411/,{ "Label": "Instance 2: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085411, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630295} -OpenZWave/1/node/12/instance/2/commandclass/38/value/206143521/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": true, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143521, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630132} -OpenZWave/1/node/12/instance/2/commandclass/38/value/281475182854184/,{ "Label": "Instance 2: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854184, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/562950159564840/,{ "Label": "Instance 2: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564840, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/844425144664096/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664096, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/38/value/1125900121374753/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374753, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/value/215449633/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449633, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/value/281475192160294/,{ "Label": "Instance 2: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160294, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/94/value/562950168870950/,{ "Label": "Instance 2: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870950, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028} -OpenZWave/1/node/12/instance/2/commandclass/152/value/216399904/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399904, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028} -OpenZWave/1/node/12/association/1/,{ "Name": "LifeLine", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1605629028} -OpenZWave/1/node/12/association/2/,{ "Name": "Retransmit Switch CC", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1605629035} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv b/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv deleted file mode 100644 index df810f64102..00000000000 --- a/tests/components/ozw/fixtures/light_new_ozw_network_dump.csv +++ /dev/null @@ -1,55 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1214", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_no_cw_network_dump.csv b/tests/components/ozw/fixtures/light_no_cw_network_dump.csv deleted file mode 100644 index 4120bc34dce..00000000000 --- a/tests/components/ozw/fixtures/light_no_cw_network_dump.csv +++ /dev/null @@ -1,54 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 29, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#0000000000", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_no_rgb.json b/tests/components/ozw/fixtures/light_no_rgb.json deleted file mode 100644 index 85226b8a71a..00000000000 --- a/tests/components/ozw/fixtures/light_no_rgb.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/2/instance/1/commandclass/38/value/38371345/", - "payload": { - "Label": "Level", - "Value": 0, - "Units": "", - "Min": 0, - "Max": 255, - "Type": "Byte", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", - "Index": 0, - "Node": 2, - "Genre": "User", - "Help": "The Current Level of the Device", - "ValueIDKey": 38371345, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/light_no_ww_network_dump.csv b/tests/components/ozw/fixtures/light_no_ww_network_dump.csv deleted file mode 100644 index c001750973d..00000000000 --- a/tests/components/ozw/fixtures/light_no_ww_network_dump.csv +++ /dev/null @@ -1,54 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 30, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#0000000000", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/light_pure_rgb.json b/tests/components/ozw/fixtures/light_pure_rgb.json deleted file mode 100644 index 4e66e8459e7..00000000000 --- a/tests/components/ozw/fixtures/light_pure_rgb.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/7/instance/1/commandclass/51/value/122470423/", - "payload": { - "Label": "Color", - "Value": "#ff00000000", - "Units": "#RRGGBBWW", - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Min": 0, - "Max": 0, - "Type": "String", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_COLOR", - "Index": 0, - "Node": 7, - "Genre": "User", - "Help": "Color (in RGB format)", - "ValueIDKey": 122470423, - "ReadOnly": false, - "WriteOnly": false, - "Event": "valueAdded", - "TimeStamp": 1597142799 - } -} diff --git a/tests/components/ozw/fixtures/light_rgb.json b/tests/components/ozw/fixtures/light_rgb.json deleted file mode 100644 index 0945b77db2d..00000000000 --- a/tests/components/ozw/fixtures/light_rgb.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/", - "payload": { - "Label": "Color", - "Value": "#000000FF00", - "Units": "#RRGGBBWWCW", - "Min": 0, - "Max": 0, - "Type": "String", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_COLOR", - "Index": 0, - "Node": 39, - "Genre": "User", - "Help": "Color (in RGB format)", - "ValueIDKey": 659341335, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/light_wc_network_dump.csv b/tests/components/ozw/fixtures/light_wc_network_dump.csv deleted file mode 100644 index 7af15f9926a..00000000000 --- a/tests/components/ozw/fixtures/light_wc_network_dump.csv +++ /dev/null @@ -1,54 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1214", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} -OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#0000000000", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} -OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} -OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} -OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/lock.json b/tests/components/ozw/fixtures/lock.json deleted file mode 100644 index 1ec2187abcb..00000000000 --- a/tests/components/ozw/fixtures/lock.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/10/instance/1/commandclass/98/value/173572112/", - "payload": { - "Label": "Lock", - "Value": false, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "Bool", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_DOOR_LOCK", - "Index": 0, - "Node": 10, - "Genre": "User", - "Help": "Lock / Unlock Device", - "ValueIDKey": 173572112, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/fixtures/lock_network_dump.csv b/tests/components/ozw/fixtures/lock_network_dump.csv deleted file mode 100644 index fdb4ce7353e..00000000000 --- a/tests/components/ozw/fixtures/lock_network_dump.csv +++ /dev/null @@ -1,79 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1131", "OZWDaemon_Version": "0.1.101", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueriedSomeDead", "TimeStamp": 1590178891, "ManufacturerSpecificDBReady": true, "homeID": 4075923038, "getControllerNodeId": 1, "getSUCNodeId": 0, "isPrimaryController": false, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.05", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/10/,{ "NodeID": 10, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "", "ZWAProductURL": "", "ProductPic": "", "Description": "", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "", "ProductPicBase64": "" }, "Event": "nodeQueriesComplete", "TimeStamp": 1590178891, "NodeManufacturerName": "Poly-control", "NodeProductName": "Danalock V3 BTZE", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Keypad Door Lock", "NodeSpecific": 3, "NodeManufacturerID": "0x010e", "NodeProductType": "0x0009", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeName": "", "NodeLocation": "", "NodeGroups": 1, "NodeDeviceTypeString": "Door Lock Keypad", "NodeDeviceType": 768, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 5, 9 ]} -OpenZWave/1/node/10/instance/1/,{ "Instance": 1, "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/281475154706452/,{ "Label": "Twist Assist", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 281475154706452, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/562950131417107/,{ "Label": "Hold and Release", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 562950131417107, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/844425108127764/,{ "Label": "Block to Block", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 844425108127764, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1125900084838419/,{ "Label": "BLE Temporary Allowed", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 1125900084838419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1407375061549076/,{ "Label": "BLE Always Allowed", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 1407375061549076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} -OpenZWave/1/node/10/instance/1/commandclass/112/value/1688850038259731/,{ "Label": "Autolock", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 1688850038259731, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/value/181895185/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 10, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 181895185, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/value/281475158605846/,{ "Label": "InstallerIcon", "Value": 768, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 10, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475158605846, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/94/value/562950135316502/,{ "Label": "UserIcon", "Value": 768, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 10, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950135316502, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/,{ "Instance": 1, "CommandClassId": 98, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/value/173572112/,{ "Label": "Locked", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 0, "Node": 10, "Genre": "User", "Help": "State of the Lock", "ValueIDKey": 173572112, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345913} -OpenZWave/1/node/10/instance/1/commandclass/98/value/281475150282772/,{ "Label": "Locked (Advanced)", "Value": { "List": [ { "Value": 0, "Label": "Unsecure" }, { "Value": 1, "Label": "Unsecured with Timeout" }, { "Value": 2, "Label": "Inside Handle Unsecured" }, { "Value": 3, "Label": "Inside Handle Unsecured with Timeout" }, { "Value": 4, "Label": "Outside Handle Unsecured" }, { "Value": 5, "Label": "Outside Handle Unsecured with Timeout" }, { "Value": 6, "Label": "Secured" }, { "Value": 255, "Label": "Invalid" } ], "Selected": "Unsecure", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 1, "Node": 10, "Genre": "User", "Help": "State of the Lock (Advanced)", "ValueIDKey": 281475150282772, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345913} -OpenZWave/1/node/10/instance/1/commandclass/98/value/562950135382036/,{ "Label": "Timeout Mode", "Value": { "List": [ { "Value": 1, "Label": "No Timeout" }, { "Value": 2, "Label": "Secure Lock after Timeout" } ], "Selected": "No Timeout", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 2, "Node": 10, "Genre": "System", "Help": "Timeout Mode for Reverting Lock State", "ValueIDKey": 562950135382036, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/value/1407375065514001/,{ "Label": "Outside Handle Control", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 5, "Node": 10, "Genre": "System", "Help": "State of the Exterior Handle Control", "ValueIDKey": 1407375065514001, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/98/value/1688850042224657/,{ "Label": "Inside Handle Control", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 6, "Node": 10, "Genre": "System", "Help": "State of the Interior Handle Control", "ValueIDKey": 1688850042224657, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/,{ "Instance": 1, "CommandClassId": 99, "CommandClass": "COMMAND_CLASS_USER_CODE", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/72339069196615702/,{ "Label": "Code Count", "Value": 20, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 257, "Node": 10, "Genre": "System", "Help": "Number of User Codes supported by the Device", "ValueIDKey": 72339069196615702, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/71776119243194392/,{ "Label": "Refresh All UserCodes", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 255, "Node": 10, "Genre": "System", "Help": "Refresh All UserCodes Stored on Device", "ValueIDKey": 71776119243194392, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/72057594219905046/,{ "Label": "Remove User Code", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 256, "Node": 10, "Genre": "System", "Help": "Remove A UserCode at the Specified Index", "ValueIDKey": 72057594219905046, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/173588503/,{ "Label": "Enrollment Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 0, "Node": 10, "Genre": "User", "Help": "Enrollment Code", "ValueIDKey": 173588503, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/281475150299159/,{ "Label": "Code 1:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 1, "Node": 10, "Genre": "User", "Help": "UserCode 1", "ValueIDKey": 281475150299159, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/562950127009815/,{ "Label": "Code 2:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 2, "Node": 10, "Genre": "User", "Help": "UserCode 2", "ValueIDKey": 562950127009815, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/844425103720471/,{ "Label": "Code 3:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 3, "Node": 10, "Genre": "User", "Help": "UserCode 3", "ValueIDKey": 844425103720471, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1125900080431127/,{ "Label": "Code 4:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 4, "Node": 10, "Genre": "User", "Help": "UserCode 4", "ValueIDKey": 1125900080431127, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1407375057141783/,{ "Label": "Code 5:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 5, "Node": 10, "Genre": "User", "Help": "UserCode 5", "ValueIDKey": 1407375057141783, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1688850033852439/,{ "Label": "Code 6:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 6, "Node": 10, "Genre": "User", "Help": "UserCode 6", "ValueIDKey": 1688850033852439, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/1970325010563095/,{ "Label": "Code 7:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 7, "Node": 10, "Genre": "User", "Help": "UserCode 7", "ValueIDKey": 1970325010563095, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/2251799987273751/,{ "Label": "Code 8:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 8, "Node": 10, "Genre": "User", "Help": "UserCode 8", "ValueIDKey": 2251799987273751, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/2533274963984407/,{ "Label": "Code 9:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 9, "Node": 10, "Genre": "User", "Help": "UserCode 9", "ValueIDKey": 2533274963984407, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/2814749940695063/,{ "Label": "Code 10:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 10, "Node": 10, "Genre": "User", "Help": "UserCode 10", "ValueIDKey": 2814749940695063, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3096224917405719/,{ "Label": "Code 11:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 11, "Node": 10, "Genre": "User", "Help": "UserCode 11", "ValueIDKey": 3096224917405719, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3377699894116375/,{ "Label": "Code 12:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 12, "Node": 10, "Genre": "User", "Help": "UserCode 12", "ValueIDKey": 3377699894116375, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3659174870827031/,{ "Label": "Code 13:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 13, "Node": 10, "Genre": "User", "Help": "UserCode 13", "ValueIDKey": 3659174870827031, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/3940649847537687/,{ "Label": "Code 14:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 14, "Node": 10, "Genre": "User", "Help": "UserCode 14", "ValueIDKey": 3940649847537687, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/4222124824248343/,{ "Label": "Code 15:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 15, "Node": 10, "Genre": "User", "Help": "UserCode 15", "ValueIDKey": 4222124824248343, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/4503599800958999/,{ "Label": "Code 16:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 16, "Node": 10, "Genre": "User", "Help": "UserCode 16", "ValueIDKey": 4503599800958999, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/4785074777669655/,{ "Label": "Code 17:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 17, "Node": 10, "Genre": "User", "Help": "UserCode 17", "ValueIDKey": 4785074777669655, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/5066549754380311/,{ "Label": "Code 18:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 18, "Node": 10, "Genre": "User", "Help": "UserCode 18", "ValueIDKey": 5066549754380311, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/5348024731090967/,{ "Label": "Code 19:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 19, "Node": 10, "Genre": "User", "Help": "UserCode 19", "ValueIDKey": 5348024731090967, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/99/value/5629499707801623/,{ "Label": "Code 20:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 20, "Node": 10, "Genre": "User", "Help": "UserCode 20", "ValueIDKey": 5629499707801623, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/182222867/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 10, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 182222867, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/281475158933523/,{ "Label": "Config File Revision", "Value": 15, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 10, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475158933523, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/562950135644179/,{ "Label": "Latest Available Config File Revision", "Value": 15, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 10, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950135644179, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/844425112354839/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 10, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425112354839, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/114/value/1125900089065495/,{ "Label": "Serial Number", "Value": "3b548b972bf8", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 10, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900089065495, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/182239252/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 182239252, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/281475158949905/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 10, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475158949905, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/562950135660568/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 10, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950135660568, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/844425112371217/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425112371217, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1125900089081876/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900089081876, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1407375065792534/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 10, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375065792534, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1688850042503192/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 10, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850042503192, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/1970325019213848/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 10, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325019213848, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2251799995924500/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 10, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799995924500, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/115/value/2533274972635158/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 10, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274972635158, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/128/value/174063633/,{ "Label": "Battery Level", "Value": 94, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 10, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 174063633, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178891} -OpenZWave/1/node/10/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/134/value/182550551/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 10, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 182550551, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/134/value/281475159261207/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 10, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475159261207, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/134/value/562950135971863/,{ "Label": "Application Version", "Value": "1.02", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 10, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950135971863, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/139/,{ "Instance": 1, "CommandClassId": 139, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/139/value/182632471/,{ "Label": "Date", "Value": "22/05/2020", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 0, "Node": 10, "Genre": "System", "Help": "Current Date", "ValueIDKey": 182632471, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178858} -OpenZWave/1/node/10/instance/1/commandclass/139/value/281475159343127/,{ "Label": "Time", "Value": "20:20:57", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 1, "Node": 10, "Genre": "System", "Help": "Current Time", "ValueIDKey": 281475159343127, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178858} -OpenZWave/1/node/10/instance/1/commandclass/139/value/562950136053784/,{ "Label": "Set Date/Time", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 2, "Node": 10, "Genre": "System", "Help": "Set the Date/Time", "ValueIDKey": 562950136053784, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/139/value/844425112764440/,{ "Label": "Refresh Date/Time", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 3, "Node": 10, "Genre": "System", "Help": "Refresh the Date/Time", "ValueIDKey": 844425112764440, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/152/value/182845456/,{ "Label": "Secured", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 10, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 182845456, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} -OpenZWave/1/node/10/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/113/value/72057594211745809/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 10, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594211745809, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178857} -OpenZWave/1/node/10/instance/1/commandclass/113/value/1688850034081812/,{ "Label": "Access Control", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 1, "Label": "Manual Lock Operation" }, { "Value": 2, "Label": "Manual Unlock Operation" }, { "Value": 3, "Label": "Wireless Lock Operation" }, { "Value": 4, "Label": "Wireless Unlock Operation" }, { "Value": 9, "Label": "Auto Lock" }, { "Value": 11, "Label": "Lock Jammed" } ], "Selected": "Wireless Unlock Operation", "Selected_id": 4 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 6, "Node": 10, "Genre": "User", "Help": "Access Control Alerts", "ValueIDKey": 1688850034081812, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345911} -OpenZWave/1/node/10/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1590178858} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/migration_fixture.csv b/tests/components/ozw/fixtures/migration_fixture.csv deleted file mode 100644 index 92b68f448f6..00000000000 --- a/tests/components/ozw/fixtures/migration_fixture.csv +++ /dev/null @@ -1,9 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "fixture description", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "iVBORggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} -OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} -OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} -OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "fixture description", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "kSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} -OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} -OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} diff --git a/tests/components/ozw/fixtures/sensor.json b/tests/components/ozw/fixtures/sensor.json deleted file mode 100644 index 17b86f90809..00000000000 --- a/tests/components/ozw/fixtures/sensor.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "topic": "OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/", - "payload": { - "Label": "Instance 1: Water", - "Value": { - "List": [ - { - "Value": 0, - "Label": "Clear" - }, - { - "Value": 2, - "Label": "Water Leak at Unknown Location" - } - ], - "Selected": "Clear", - "Selected_id": 0 - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_NOTIFICATION", - "Index": 5, - "Node": 36, - "Genre": "User", - "Help": "Water Alerts", - "ValueIDKey": 1407375493578772, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv b/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv deleted file mode 100644 index 071d92da0d0..00000000000 --- a/tests/components/ozw/fixtures/sensor_string_value_network_dump.csv +++ /dev/null @@ -1,5 +0,0 @@ -OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1240", "OZWDaemon_Version": "0.1.170", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.9", "Status": "driverAllNodesQueried", "TimeStamp": 1598022319, "ManufacturerSpecificDBReady": true, "homeID": 3389163831, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} -OpenZWave/1/node/49/,{ "NodeID": 49, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0373:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2780/", "ProductPic": "images/idlock/idlock150.png", "Description": "A module enabling your ID Lock digital door lock to a Z-Wave Plus enabled digital door Lock. The module is compatible with ID Lock 101 and ID Lock 150. It enables your ID Lock to operate in a Z-Wave network with numerous access control funtions and notifications.", "ProductManualURL": "https://idlock.no/wp-content/uploads/2019/08/IDLock150_ZWave_UserManual_v3.02.pdf", "ProductPageURL": "https://idlock.no/z-wave/", "InclusionHelp": "Inclusion – (Puts your device in inclusion mode) • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on touch panel. • Press digit \"2\" for settings followed by * on touch panel. • Press digit “5” on touch panel. Inclusion mode starts immediately. LED indicator below logo signals this by flashing blue.", "ExclusionHelp": "Exclusion – (Puts your device in exclusion mode) • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on touch panel. • Press digit \"2\" for settings followed by * on touch panel. • Press digit “5” on touch panel. Exclusion mode starts immediately. LED indicator below logo signals this by flashing blue.", "ResetHelp": "Device reset – (This will reset RF interface module to factory default settings) Warning: Please do only proceed with the following reset procedure, if primary network controller is missing or otherwise inoperable. RESET Z-WAVE MODULE: • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on keypad. • Press digit \"2\" for settings followed by * on keypad. • Press digit “0” on keypad. If the Z-wave module is not included in a Z-wave network the door lock will also return to factory settings when following the above procedure. FACTORY RESET DOOR LOCK FIRMWARE: • Push and hold inside lock/unlock button while inserting the fourth battery. • Receive reset sound. • Release button. • Receive confirmation sound.", "WakeupHelp": "Activate by touching the touch panel with finger(s), the palm of the hand on the outside unit or by pushing the key button on the inside unit.", "ProductSupportURL": "https://idlock.no/kundesenter/", "Frequency": "CEPT (Europe)", "Name": "ID Lock 150 Z-Wave module", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1598022340, "NodeManufacturerName": "ID Lock AS", "NodeProductName": "ID-150 Z-Wave Module", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Keypad Door Lock", "NodeSpecific": 3, "NodeManufacturerID": "0x0373", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Door Lock Keypad", "NodeDeviceType": 768, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 46, 48, 50 ], "Neighbors": [ 1, 46, 48, 50 ]} -OpenZWave/1/node/49/instance/1/,{ "Instance": 1, "TimeStamp": 1598022021} -OpenZWave/1/node/49/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 4, "TimeStamp": 1598022021} -OpenZWave/1/node/49/instance/1/commandclass/113/value/73464969749610519/,{ "Label": "User Code", "Value": "asdfgh", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 261, "Node": 49, "Genre": "User", "Help": "User Code that was used", "ValueIDKey": 73464969749610519, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1598022021} \ No newline at end of file diff --git a/tests/components/ozw/fixtures/switch.json b/tests/components/ozw/fixtures/switch.json deleted file mode 100644 index 0d3fc37e9b2..00000000000 --- a/tests/components/ozw/fixtures/switch.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "topic": "OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/", - "payload": { - "Label": "Switch", - "Value": false, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "Bool", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", - "Index": 0, - "Node": 32, - "Genre": "User", - "Help": "Turn On/Off Device", - "ValueIDKey": 541671440, - "ReadOnly": false, - "WriteOnly": false, - "ValueSet": false, - "ValuePolled": false, - "ChangeVerified": false, - "Event": "valueAdded", - "TimeStamp": 1579566891 - } -} diff --git a/tests/components/ozw/test_binary_sensor.py b/tests/components/ozw/test_binary_sensor.py deleted file mode 100644 index d0852a5caf0..00000000000 --- a/tests/components/ozw/test_binary_sensor.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Test Z-Wave Sensors.""" -from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR_DOMAIN, - BinarySensorDeviceClass, -) -from homeassistant.components.ozw.const import DOMAIN -from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.helpers import entity_registry as er - -from .common import setup_ozw - - -async def test_binary_sensor(hass, generic_data, binary_sensor_msg): - """Test setting up config entry.""" - receive_msg = await setup_ozw(hass, fixture=generic_data) - - # Test Legacy sensor (disabled by default) - registry = er.async_get(hass) - entity_id = "binary_sensor.trisensor_sensor" - state = hass.states.get(entity_id) - assert state is None - entry = registry.async_get(entity_id) - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - # Test enabling legacy entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entry - assert updated_entry.disabled is False - - # Test Sensor for Notification CC - state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") - assert state - assert state.state == "off" - assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION - - # Test incoming state change - receive_msg(binary_sensor_msg) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.trisensor_home_security_motion_detected") - assert state.state == "on" - - -async def test_sensor_enabled(hass, generic_data, binary_sensor_alt_msg): - """Test enabling a legacy binary_sensor.""" - - registry = er.async_get(hass) - - entry = registry.async_get_or_create( - BINARY_SENSOR_DOMAIN, - DOMAIN, - "1-37-625737744", - suggested_object_id="trisensor_sensor_instance_1_sensor", - disabled_by=None, - ) - assert entry.disabled is False - - receive_msg = await setup_ozw(hass, fixture=generic_data) - receive_msg(binary_sensor_alt_msg) - await hass.async_block_till_done() - - state = hass.states.get(entry.entity_id) - assert state is not None - assert state.state == "on" diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py deleted file mode 100644 index 3414e6c4832..00000000000 --- a/tests/components/ozw/test_climate.py +++ /dev/null @@ -1,327 +0,0 @@ -"""Test Z-Wave Multi-setpoint Climate entities.""" -from homeassistant.components.climate import ATTR_TEMPERATURE -from homeassistant.components.climate.const import ( - ATTR_CURRENT_TEMPERATURE, - ATTR_FAN_MODE, - ATTR_FAN_MODES, - ATTR_HVAC_ACTION, - ATTR_HVAC_MODES, - ATTR_PRESET_MODE, - ATTR_PRESET_MODES, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_IDLE, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, -) - -from .common import setup_ozw - - -async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=climate_data) - - # Test multi-setpoint thermostat (node 7 in dump) - # mode is heat, this should be single setpoint - state = hass.states.get("climate.ct32_thermostat_mode") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - ] - assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 - assert state.attributes[ATTR_TEMPERATURE] == 21.1 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes[ATTR_FAN_MODE] == "Auto Low" - assert state.attributes[ATTR_FAN_MODES] == ["Auto Low", "On Low"] - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - {"entity_id": "climate.ct32_thermostat_mode", "temperature": 26.1}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 78.98 - assert msg["payload"]["ValueIDKey"] == 281475099443218 - - # Test hvac_mode with set_temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.ct32_thermostat_mode", - "temperature": 24.1, - "hvac_mode": "cool", - }, - blocking=True, - ) - assert len(sent_messages) == 3 # 2 messages - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - # Celsius is converted to Fahrenheit here! - assert round(msg["payload"]["Value"], 2) == 75.38 - assert msg["payload"]["ValueIDKey"] == 281475099443218 - - # Test set mode - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_HEAT_COOL}, - blocking=True, - ) - assert len(sent_messages) == 4 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 3, "ValueIDKey": 122683412} - - # Test set missing mode - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": "fan_only"}, - blocking=True, - ) - assert len(sent_messages) == 4 - assert "Received an invalid hvac mode: fan_only" in caplog.text - - # Test set fan mode - await hass.services.async_call( - "climate", - "set_fan_mode", - {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "On Low"}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 1, "ValueIDKey": 122748948} - - # Test set invalid fan mode - await hass.services.async_call( - "climate", - "set_fan_mode", - {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "invalid fan mode"}, - blocking=True, - ) - assert len(sent_messages) == 5 - assert "Received an invalid fan mode: invalid fan mode" in caplog.text - - # Test incoming mode change to auto, - # resulting in multiple setpoints - receive_message(climate_msg) - await hass.async_block_till_done() - state = hass.states.get("climate.ct32_thermostat_mode") - assert state is not None - assert state.state == HVAC_MODE_HEAT_COOL - assert state.attributes.get(ATTR_TEMPERATURE) is None - assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 - assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 - - # Test setting high/low temp on multiple setpoints - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.ct32_thermostat_mode", - "target_temp_low": 20, - "target_temp_high": 25, - }, - blocking=True, - ) - assert len(sent_messages) == 7 # 2 messages ! - msg = sent_messages[-2] # low setpoint - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 68.0 - assert msg["payload"]["ValueIDKey"] == 281475099443218 - msg = sent_messages[-1] # high setpoint - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert round(msg["payload"]["Value"], 2) == 77.0 - assert msg["payload"]["ValueIDKey"] == 562950076153874 - - # Test basic/single-setpoint thermostat (node 16 in dump) - state = hass.states.get("climate.komforthaus_spirit_z_wave_plus_mode") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - ] - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 17.3 - assert round(state.attributes[ATTR_TEMPERATURE], 0) == 19 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes[ATTR_PRESET_MODES] == [ - "none", - "Heat Eco", - "Full Power", - "Manufacturer Specific", - ] - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "temperature": 28.0, - }, - blocking=True, - ) - assert len(sent_messages) == 8 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 28.0, - "ValueIDKey": 281475250438162, - } - - # Test set preset mode - await hass.services.async_call( - "climate", - "set_preset_mode", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "preset_mode": "Heat Eco", - }, - blocking=True, - ) - assert len(sent_messages) == 9 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 11, - "ValueIDKey": 273678356, - } - - # Test set preset mode None - # This preset should set and return to current hvac mode - await hass.services.async_call( - "climate", - "set_preset_mode", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "preset_mode": "none", - }, - blocking=True, - ) - assert len(sent_messages) == 10 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 1, - "ValueIDKey": 273678356, - } - - # Test set invalid preset mode - await hass.services.async_call( - "climate", - "set_preset_mode", - { - "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", - "preset_mode": "invalid preset mode", - }, - blocking=True, - ) - assert len(sent_messages) == 10 - assert "Received an invalid preset mode: invalid preset mode" in caplog.text - - # test thermostat device without a mode commandclass - state = hass.states.get("climate.danfoss_living_connect_z_v1_06_014g0013_heating_1") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_HEAT, - ] - assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) is None - assert round(state.attributes[ATTR_TEMPERATURE], 0) == 21 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes.get(ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_PRESET_MODES) is None - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.danfoss_living_connect_z_v1_06_014g0013_heating_1", - "temperature": 28.0, - }, - blocking=True, - ) - assert len(sent_messages) == 11 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 28.0, - "ValueIDKey": 281475116220434, - } - - await hass.services.async_call( - "climate", - "set_hvac_mode", - { - "entity_id": "climate.danfoss_living_connect_z_v1_06_014g0013_heating_1", - "hvac_mode": HVAC_MODE_HEAT, - }, - blocking=True, - ) - assert len(sent_messages) == 11 - assert "does not support setting a mode" in caplog.text - - # test thermostat device without a mode commandclass - state = hass.states.get("climate.secure_srt321_zwave_stat_tx_heating_1") - assert state is not None - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_HEAT, - ] - assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 29.0 - assert round(state.attributes[ATTR_TEMPERATURE], 0) == 16 - assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None - assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None - assert state.attributes.get(ATTR_PRESET_MODE) is None - assert state.attributes.get(ATTR_PRESET_MODES) is None - - # Test set target temperature - await hass.services.async_call( - "climate", - "set_temperature", - { - "entity_id": "climate.secure_srt321_zwave_stat_tx_heating_1", - "temperature": 28.0, - }, - blocking=True, - ) - assert len(sent_messages) == 12 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 28.0, - "ValueIDKey": 281475267215378, - } - - await hass.services.async_call( - "climate", - "set_hvac_mode", - { - "entity_id": "climate.secure_srt321_zwave_stat_tx_heating_1", - "hvac_mode": HVAC_MODE_HEAT, - }, - blocking=True, - ) - assert len(sent_messages) == 12 - assert "does not support setting a mode" in caplog.text diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py deleted file mode 100644 index 9c65372ca98..00000000000 --- a/tests/components/ozw/test_config_flow.py +++ /dev/null @@ -1,502 +0,0 @@ -"""Test the Z-Wave over MQTT config flow.""" -from unittest.mock import patch - -import pytest - -from homeassistant import config_entries -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.components.ozw.config_flow import TITLE -from homeassistant.components.ozw.const import DOMAIN - -from tests.common import MockConfigEntry - -ADDON_DISCOVERY_INFO = HassioServiceInfo( - config={ - "addon": "OpenZWave", - "host": "host1", - "port": 1234, - "username": "name1", - "password": "pass1", - } -) - - -@pytest.fixture(name="supervisor") -def mock_supervisor_fixture(): - """Mock Supervisor.""" - with patch("homeassistant.components.hassio.is_hassio", return_value=True): - yield - - -@pytest.fixture(name="addon_info") -def mock_addon_info(): - """Mock Supervisor add-on info.""" - with patch("homeassistant.components.hassio.async_get_addon_info") as addon_info: - addon_info.return_value = {} - yield addon_info - - -@pytest.fixture(name="addon_running") -def mock_addon_running(addon_info): - """Mock add-on already running.""" - addon_info.return_value["state"] = "started" - return addon_info - - -@pytest.fixture(name="addon_installed") -def mock_addon_installed(addon_info): - """Mock add-on already installed but not running.""" - addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" - return addon_info - - -@pytest.fixture(name="addon_options") -def mock_addon_options(addon_info): - """Mock add-on options.""" - addon_info.return_value["options"] = {} - return addon_info.return_value["options"] - - -@pytest.fixture(name="set_addon_options") -def mock_set_addon_options(): - """Mock set add-on options.""" - with patch( - "homeassistant.components.hassio.async_set_addon_options" - ) as set_options: - yield set_options - - -@pytest.fixture(name="install_addon") -def mock_install_addon(): - """Mock install add-on.""" - with patch("homeassistant.components.hassio.async_install_addon") as install_addon: - yield install_addon - - -@pytest.fixture(name="start_addon") -def mock_start_addon(): - """Mock start add-on.""" - with patch("homeassistant.components.hassio.async_start_addon") as start_addon: - yield start_addon - - -async def test_user_not_supervisor_create_entry(hass, mqtt): - """Test the user step creates an entry not on Supervisor.""" - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": None, - "network_key": None, - "use_addon": False, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_mqtt_not_setup(hass): - """Test that mqtt is required.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "abort" - assert result["reason"] == "mqtt_required" - - -async def test_one_instance_allowed(hass): - """Test that only one instance is allowed.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" - - -async def test_not_addon(hass, supervisor, mqtt): - """Test opting out of add-on on Supervisor.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": False} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": None, - "network_key": None, - "use_addon": False, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_addon_running(hass, supervisor, addon_running, addon_options): - """Test add-on already running on Supervisor.""" - addon_options["device"] = "/test" - addon_options["network_key"] = "abc123" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_addon_info_failure(hass, supervisor, addon_info): - """Test add-on info failure.""" - addon_info.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - assert result["type"] == "abort" - assert result["reason"] == "addon_info_failed" - - -async def test_addon_installed( - hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon -): - """Test add-on already installed but not running on Supervisor.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_set_addon_config_failure( - hass, supervisor, addon_installed, addon_options, set_addon_options -): - """Test add-on set config failure.""" - set_addon_options.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - - assert result["type"] == "abort" - assert result["reason"] == "addon_set_config_failed" - - -async def test_start_addon_failure( - hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon -): - """Test add-on start failure.""" - start_addon.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "addon_start_failed"} - - -async def test_addon_not_installed( - hass, - supervisor, - addon_installed, - install_addon, - addon_options, - set_addon_options, - start_addon, -): - """Test add-on not installed.""" - addon_installed.return_value["version"] = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - assert result["type"] == "progress" - - # Make sure the flow continues when the progress task is done. - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] == "form" - assert result["step_id"] == "start_addon" - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): - """Test add-on install failure.""" - addon_installed.return_value["version"] = None - install_addon.side_effect = HassioAPIError() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": True} - ) - - assert result["type"] == "progress" - - # Make sure the flow continues when the progress task is done. - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] == "abort" - assert result["reason"] == "addon_install_failed" - - -async def test_supervisor_discovery(hass, supervisor, addon_running, addon_options): - """Test flow started from Supervisor discovery.""" - - addon_options["device"] = "/test" - addon_options["network_key"] = "abc123" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": "/test", - "network_key": "abc123", - "use_addon": True, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_clean_discovery_on_user_create( - hass, supervisor, addon_running, addon_options -): - """Test discovery flow is cleaned up when a user flow is finished.""" - - addon_options["device"] = "/test" - addon_options["network_key"] = "abc123" - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ozw.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"use_addon": False} - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.flow.async_progress()) == 0 - assert result["type"] == "create_entry" - assert result["title"] == TITLE - assert result["data"] == { - "usb_path": None, - "network_key": None, - "use_addon": False, - "integration_created_addon": False, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_abort_discovery_with_user_flow( - hass, supervisor, addon_running, addon_options -): - """Test discovery flow is aborted if a user flow is in progress.""" - - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_in_progress" - assert len(hass.config_entries.flow.async_progress()) == 1 - - -async def test_abort_discovery_with_existing_entry( - hass, supervisor, addon_running, addon_options -): - """Test discovery flow is aborted if an entry already exists.""" - - entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=DOMAIN) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_discovery_addon_not_running( - hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon -): - """Test discovery with add-on already installed but not running.""" - addon_options["device"] = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["step_id"] == "start_addon" - assert result["type"] == "form" - - -async def test_discovery_addon_not_installed( - hass, supervisor, addon_installed, install_addon, addon_options -): - """Test discovery with add-on not installed.""" - addon_installed.return_value["version"] = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=ADDON_DISCOVERY_INFO, - ) - - assert result["step_id"] == "hassio_confirm" - assert result["type"] == "form" - - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["step_id"] == "install_addon" - assert result["type"] == "progress" - - await hass.async_block_till_done() - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["type"] == "form" - assert result["step_id"] == "start_addon" diff --git a/tests/components/ozw/test_cover.py b/tests/components/ozw/test_cover.py deleted file mode 100644 index 2b3b1e06862..00000000000 --- a/tests/components/ozw/test_cover.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Test Z-Wave Covers.""" -from homeassistant.components.cover import ATTR_CURRENT_POSITION -from homeassistant.components.ozw.cover import VALUE_SELECTED_ID - -from .common import setup_ozw - -VALUE_ID = "Value" - - -async def test_cover(hass, cover_data, sent_messages, cover_msg): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=cover_data) - # Test loaded - state = hass.states.get("cover.roller_shutter_3_instance_1_level") - assert state is not None - assert state.state == "closed" - assert state.attributes[ATTR_CURRENT_POSITION] == 0 - - # Test setting position - await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} - - # Feedback on state - cover_msg.decode() - cover_msg.payload["Value"] = 50 - cover_msg.encode() - receive_message(cover_msg) - await hass.async_block_till_done() - - # Test opening - await hass.services.async_call( - "cover", - "open_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 281475602284568} - - # Test stopping after opening - await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 4 - msg = sent_messages[2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - - msg = sent_messages[3] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} - - # Test closing - await hass.services.async_call( - "cover", - "close_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[4] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 562950578995224} - - # Test stopping after closing - await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - assert len(sent_messages) == 7 - msg = sent_messages[5] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - - msg = sent_messages[6] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} - - # Test stopping after no open/close - await hass.services.async_call( - "cover", - "stop_cover", - {"entity_id": "cover.roller_shutter_3_instance_1_level"}, - blocking=True, - ) - # both stop open/close messages sent - assert len(sent_messages) == 9 - msg = sent_messages[7] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - - msg = sent_messages[8] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} - - # Test converting position to zwave range for position > 0 - await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 100}, - blocking=True, - ) - assert len(sent_messages) == 10 - msg = sent_messages[9] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} - - # Test converting position to zwave range for position = 0 - await hass.services.async_call( - "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 0}, - blocking=True, - ) - assert len(sent_messages) == 11 - msg = sent_messages[10] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} - - -async def test_barrier(hass, cover_gdo_data, sent_messages, cover_gdo_msg): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=cover_gdo_data) - # Test loaded - state = hass.states.get("cover.gd00z_4_barrier_state") - assert state is not None - assert state.state == "closed" - - # Test opening - await hass.services.async_call( - "cover", - "open_cover", - {"entity_id": "cover.gd00z_4_barrier_state"}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 4, "ValueIDKey": 281475083239444} - - # Feedback on state - cover_gdo_msg.decode() - cover_gdo_msg.payload[VALUE_ID][VALUE_SELECTED_ID] = 4 - cover_gdo_msg.encode() - receive_message(cover_gdo_msg) - await hass.async_block_till_done() - - state = hass.states.get("cover.gd00z_4_barrier_state") - assert state is not None - assert state.state == "open" - - # Test closing - await hass.services.async_call( - "cover", - "close_cover", - {"entity_id": "cover.gd00z_4_barrier_state"}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475083239444} diff --git a/tests/components/ozw/test_fan.py b/tests/components/ozw/test_fan.py deleted file mode 100644 index 9bf0cbf7093..00000000000 --- a/tests/components/ozw/test_fan.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test Z-Wave Fans.""" - -from .common import setup_ozw - - -async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): - """Test fan.""" - receive_message = await setup_ozw(hass, fixture=fan_data) - - # Test loaded - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "on" - - # Test turning off - await hass.services.async_call( - "fan", - "turn_off", - {"entity_id": "fan.in_wall_smart_fan_control_level"}, - blocking=True, - ) - - assert len(sent_messages) == 1 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 172589073} - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 0 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "off" - - # Test turning on - await hass.services.async_call( - "fan", - "turn_on", - {"entity_id": "fan.in_wall_smart_fan_control_level", "percentage": 66}, - blocking=True, - ) - - assert len(sent_messages) == 2 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 66, - "ValueIDKey": 172589073, - } - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 66 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "on" - assert state.attributes["percentage"] == 66 - - # Test turn on without speed - await hass.services.async_call( - "fan", - "turn_on", - {"entity_id": "fan.in_wall_smart_fan_control_level"}, - blocking=True, - ) - - assert len(sent_messages) == 3 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 255, - "ValueIDKey": 172589073, - } - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 99 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "on" - assert state.attributes["percentage"] == 100 - - # Test set percentage to 0 - await hass.services.async_call( - "fan", - "set_percentage", - {"entity_id": "fan.in_wall_smart_fan_control_level", "percentage": 0}, - blocking=True, - ) - - assert len(sent_messages) == 4 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 0, - "ValueIDKey": 172589073, - } - - # Feedback on state - fan_msg.decode() - fan_msg.payload["Value"] = 0 - fan_msg.encode() - receive_message(fan_msg) - await hass.async_block_till_done() - - state = hass.states.get("fan.in_wall_smart_fan_control_level") - assert state is not None - assert state.state == "off" diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py deleted file mode 100644 index 9719c483800..00000000000 --- a/tests/components/ozw/test_init.py +++ /dev/null @@ -1,238 +0,0 @@ -"""Test integration initialization.""" -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.components.ozw import DOMAIN, PLATFORMS, const -from homeassistant.const import ATTR_RESTORED, STATE_UNAVAILABLE - -from .common import setup_ozw - -from tests.common import MockConfigEntry - - -async def test_init_entry(hass, generic_data): - """Test setting up config entry.""" - await setup_ozw(hass, fixture=generic_data) - - # Verify integration + platform loaded. - assert "ozw" in hass.config.components - for platform in PLATFORMS: - assert platform in hass.config.components, platform - assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" - - # Verify services registered - assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) - assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) - - -async def test_setup_entry_without_mqtt(hass): - """Test setting up config entry without mqtt integration setup.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - ) - entry.add_to_hass(hass) - - assert not await hass.config_entries.async_setup(entry.entry_id) - - -async def test_publish_without_mqtt(hass, caplog): - """Test publish without mqtt integration setup.""" - with patch("homeassistant.components.ozw.OZWOptions") as ozw_options: - await setup_ozw(hass) - - send_message = ozw_options.call_args[1]["send_message"] - - mqtt_entries = hass.config_entries.async_entries("mqtt") - mqtt_entry = mqtt_entries[0] - await hass.config_entries.async_remove(mqtt_entry.entry_id) - await hass.async_block_till_done() - - assert not hass.config_entries.async_entries("mqtt") - - # Sending a message should not error with the MQTT integration not set up. - send_message("test_topic", "test_payload") - await hass.async_block_till_done() - - assert "MQTT integration is not set up" in caplog.text - - -async def test_unload_entry(hass, generic_data, switch_msg, caplog): - """Test unload the config entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - ) - entry.add_to_hass(hass) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - - receive_message = await setup_ozw(hass, entry=entry, fixture=generic_data) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(hass.states.async_entity_ids("switch")) == 1 - - await hass.config_entries.async_unload(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - entities = hass.states.async_entity_ids("switch") - assert len(entities) == 1 - for entity in entities: - assert hass.states.get(entity).state == STATE_UNAVAILABLE - assert hass.states.get(entity).attributes.get(ATTR_RESTORED) - - # Send a message for a switch from the broker to check that - # all entity topic subscribers are unsubscribed. - receive_message(switch_msg) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids("switch")) == 1 - for entity in entities: - assert hass.states.get(entity).state == STATE_UNAVAILABLE - assert hass.states.get(entity).attributes.get(ATTR_RESTORED) - - # Load the integration again and check that there are no errors when - # adding the entities. - # This asserts that we have unsubscribed the entity addition signals - # when unloading the integration previously. - await setup_ozw(hass, entry=entry, fixture=generic_data) - await hass.async_block_till_done() - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(hass.states.async_entity_ids("switch")) == 1 - for record in caplog.records: - assert record.levelname != "ERROR" - - -async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): - """Test remove the config entry.""" - # test successful remove without created add-on - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - data={"integration_created_addon": False}, - ) - entry.add_to_hass(hass) - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - await hass.config_entries.async_remove(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - - # test successful remove with created add-on - entry = MockConfigEntry( - domain=DOMAIN, - title="Z-Wave", - data={"integration_created_addon": True}, - ) - entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - await hass.config_entries.async_remove(entry.entry_id) - - assert stop_addon.call_count == 1 - assert uninstall_addon.call_count == 1 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - stop_addon.reset_mock() - uninstall_addon.reset_mock() - - # test add-on stop failure - entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - stop_addon.side_effect = HassioAPIError() - - await hass.config_entries.async_remove(entry.entry_id) - - assert stop_addon.call_count == 1 - assert uninstall_addon.call_count == 0 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to stop the OpenZWave add-on" in caplog.text - stop_addon.side_effect = None - stop_addon.reset_mock() - uninstall_addon.reset_mock() - - # test add-on uninstall failure - entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - uninstall_addon.side_effect = HassioAPIError() - - await hass.config_entries.async_remove(entry.entry_id) - - assert stop_addon.call_count == 1 - assert uninstall_addon.call_count == 1 - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - assert "Failed to uninstall the OpenZWave add-on" in caplog.text - - -async def test_setup_entry_with_addon(hass, get_addon_discovery_info): - """Test set up entry using OpenZWave add-on.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - data={"use_addon": True}, - ) - entry.add_to_hass(hass) - - with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_client.return_value.start_client.call_count == 1 - - # Verify integration + platform loaded. - assert "ozw" in hass.config.components - for platform in PLATFORMS: - assert platform in hass.config.components, platform - assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" - - # Verify services registered - assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) - assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) - - -async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): - """Test set up entry using OpenZWave add-on but missing discovery info.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - data={"use_addon": True}, - ) - entry.add_to_hass(hass) - - get_addon_discovery_info.return_value = None - - with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: - assert not await hass.config_entries.async_setup(entry.entry_id) - - assert mock_client.return_value.start_client.call_count == 0 - assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY - - -async def test_unload_entry_with_addon( - hass, get_addon_discovery_info, generic_data, switch_msg, caplog -): - """Test unload the config entry using the OpenZWave add-on.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="OpenZWave", - data={"use_addon": True}, - ) - entry.add_to_hass(hass) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED - - with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_client.return_value.start_client.call_count == 1 - assert entry.state is config_entries.ConfigEntryState.LOADED - - await hass.config_entries.async_unload(entry.entry_id) - - assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py deleted file mode 100644 index a8ed4352f9a..00000000000 --- a/tests/components/ozw/test_light.py +++ /dev/null @@ -1,683 +0,0 @@ -"""Test Z-Wave Lights.""" -from homeassistant.components.light import SUPPORT_TRANSITION -from homeassistant.components.ozw.light import byte_to_zwave_brightness - -from .common import setup_ozw - - -async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_data) - - # Test loaded - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == SUPPORT_TRANSITION - assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] - - # Test turning on - # Beware that due to rounding, a roundtrip conversion does not always work - new_brightness = 44 - new_transition = 0 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "brightness": new_brightness, - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 2 - - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 1407375551070225} - - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": byte_to_zwave_brightness(new_brightness), - "ValueIDKey": 659128337, - } - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["brightness"] == new_brightness - assert state.attributes["color_mode"] == "color_temp" - - # Test turning off - new_transition = 6553 - await hass.services.async_call( - "light", - "turn_off", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 4 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 237, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 0 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - - # Test turn on without brightness - new_transition = 127.0 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 6 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 127, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": 255, - "ValueIDKey": 659128337, - } - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["brightness"] == new_brightness - assert state.attributes["color_mode"] == "color_temp" - - # Test set brightness to 0 - new_brightness = 0 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "brightness": new_brightness, - }, - blocking=True, - ) - assert len(sent_messages) == 7 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": byte_to_zwave_brightness(new_brightness), - "ValueIDKey": 659128337, - } - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - - # Test setting color_name - new_color = "blue" - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_name": new_color}, - blocking=True, - ) - assert len(sent_messages) == 9 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#0000ff0000", "ValueIDKey": 659341335} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#0000ff0000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["rgb_color"] == (0, 0, 255) - assert state.attributes["color_mode"] == "hs" - - # Test setting hs_color - new_color = [300, 70] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "hs_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 11 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff4cff0000", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#ff4cff0000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["hs_color"] == (300.0, 70.196) - assert state.attributes["color_mode"] == "hs" - - # Test setting rgb_color - new_color = [255, 154, 0] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "rgb_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 13 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff99000000", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#ff99000000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["rgb_color"] == (255, 153, 0) - assert state.attributes["color_mode"] == "hs" - - # Test setting xy_color - new_color = [0.52, 0.43] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "xy_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 15 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ffbb370000", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#ffbb370000" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["xy_color"] == (0.519, 0.429) - assert state.attributes["color_mode"] == "hs" - - # Test setting color temp - new_color = 200 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_temp": new_color}, - blocking=True, - ) - assert len(sent_messages) == 17 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#00000037c8", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#00000037c8" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_temp"] == 200 - assert state.attributes["color_mode"] == "color_temp" - - # Test setting invalid color temp - new_color = 120 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_temp": new_color}, - blocking=True, - ) - assert len(sent_messages) == 19 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#00000000ff", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#00000000ff" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_temp"] == 153 - assert state.attributes["color_mode"] == "color_temp" - - -async def test_pure_rgb_dimmer_light( - hass, light_data, light_pure_rgb_msg, sent_messages -): - """Test light with no color channels command class.""" - receive_message = await setup_ozw(hass, fixture=light_data) - - # Test loaded - state = hass.states.get("light.kitchen_rgb_strip_level") - assert state is not None - assert state.state == "on" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["hs"] - assert state.attributes["color_mode"] == "hs" - - # Test setting hs_color - new_color = [300, 70] - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.kitchen_rgb_strip_level", "hs_color": new_color}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 122257425} - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff4cff00", "ValueIDKey": 122470423} - - # Feedback on state - light_pure_rgb_msg.decode() - light_pure_rgb_msg.payload["Value"] = "#ff4cff00" - light_pure_rgb_msg.encode() - receive_message(light_pure_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.kitchen_rgb_strip_level") - assert state is not None - assert state.state == "on" - assert state.attributes["hs_color"] == (300.0, 70.196) - assert state.attributes["color_mode"] == "hs" - - -async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_data) - - # Test loaded no RGBW support (dimmer only) - state = hass.states.get("light.master_bedroom_l_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["brightness"] - - # Turn on the light - new_brightness = 44 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.master_bedroom_l_level", "brightness": new_brightness}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == { - "Value": byte_to_zwave_brightness(new_brightness), - "ValueIDKey": 38371345, - } - - # Feedback on state - - light_no_rgb_msg.decode() - light_no_rgb_msg.payload["Value"] = byte_to_zwave_brightness(new_brightness) - light_no_rgb_msg.encode() - receive_message(light_no_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.master_bedroom_l_level") - assert state is not None - assert state.state == "on" - assert state.attributes["brightness"] == new_brightness - assert state.attributes["color_mode"] == "brightness" - - -async def test_no_ww_light( - hass, light_no_ww_data, light_msg, light_rgb_msg, sent_messages -): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_no_ww_data) - - # Test loaded no ww support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["rgbw"] - - # Turn on the light - white_color = 190 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "rgbw_color": [0, 0, 0, white_color], - }, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#00000000be", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#00000000be" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_mode"] == "rgbw" - assert state.attributes["rgbw_color"] == (0, 0, 0, 190) - - -async def test_no_cw_light( - hass, light_no_cw_data, light_msg, light_rgb_msg, sent_messages -): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_no_cw_data) - - # Test loaded no cw support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["rgbw"] - - # Turn on the light - white_color = 190 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "rgbw_color": [0, 0, 0, white_color], - }, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#000000be", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#000000be" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_mode"] == "rgbw" - assert state.attributes["rgbw_color"] == (0, 0, 0, 190) - - -async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_wc_data) - - # Test loaded only white LED support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == 0 - assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] - - assert state.attributes["min_mireds"] == 153 - assert state.attributes["max_mireds"] == 370 - - # Turn on the light - new_color = 190 - await hass.services.async_call( - "light", - "turn_on", - {"entity_id": "light.led_bulb_6_multi_colour_level", "color_temp": new_color}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#0000002bd4", "ValueIDKey": 659341335} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = byte_to_zwave_brightness(255) - light_msg.encode() - light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#0000002bd4" - light_rgb_msg.encode() - receive_message(light_msg) - receive_message(light_rgb_msg) - await hass.async_block_till_done() - - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "on" - assert state.attributes["color_temp"] == 190 - assert state.attributes["color_mode"] == "color_temp" - - -async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=light_new_ozw_data) - - # Test loaded only white LED support - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state is not None - assert state.state == "off" - assert state.attributes["supported_features"] == SUPPORT_TRANSITION - assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] - - # Test turning on with new duration (newer openzwave) - new_transition = 4180 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 2 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 4180, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 255 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state.attributes["color_mode"] == "color_temp" - - # Test turning off with new duration (newer openzwave)(new max) - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.led_bulb_6_multi_colour_level"}, - blocking=True, - ) - assert len(sent_messages) == 4 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 7621, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 0 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - - # Test turning on with new duration (newer openzwave)(factory default) - new_transition = 8000 - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.led_bulb_6_multi_colour_level", - "transition": new_transition, - }, - blocking=True, - ) - assert len(sent_messages) == 6 - - msg = sent_messages[-2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 6553, "ValueIDKey": 1407375551070225} - - msg = sent_messages[-1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 255, "ValueIDKey": 659128337} - - # Feedback on state - light_msg.decode() - light_msg.payload["Value"] = 255 - light_msg.encode() - receive_message(light_msg) - await hass.async_block_till_done() - state = hass.states.get("light.led_bulb_6_multi_colour_level") - assert state.attributes["color_mode"] == "color_temp" diff --git a/tests/components/ozw/test_lock.py b/tests/components/ozw/test_lock.py deleted file mode 100644 index f32d073c562..00000000000 --- a/tests/components/ozw/test_lock.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Test Z-Wave Locks.""" -from .common import setup_ozw - - -async def test_lock(hass, lock_data, sent_messages, lock_msg, caplog): - """Test lock.""" - receive_message = await setup_ozw(hass, fixture=lock_data) - - # Test loaded - state = hass.states.get("lock.danalock_v3_btze_locked") - assert state is not None - assert state.state == "unlocked" - - # Test locking - await hass.services.async_call( - "lock", "lock", {"entity_id": "lock.danalock_v3_btze_locked"}, blocking=True - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 173572112} - - # Feedback on state - lock_msg.decode() - lock_msg.payload["Value"] = True - lock_msg.encode() - receive_message(lock_msg) - await hass.async_block_till_done() - - state = hass.states.get("lock.danalock_v3_btze_locked") - assert state is not None - assert state.state == "locked" - - # Test unlocking - await hass.services.async_call( - "lock", "unlock", {"entity_id": "lock.danalock_v3_btze_locked"}, blocking=True - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 173572112} - - # Test set_usercode - await hass.services.async_call( - "ozw", - "set_usercode", - { - "entity_id": "lock.danalock_v3_btze_locked", - "usercode": 123456, - "code_slot": 1, - }, - blocking=True, - ) - assert len(sent_messages) == 3 - msg = sent_messages[2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "123456", "ValueIDKey": 281475150299159} - - # Test clear_usercode - await hass.services.async_call( - "ozw", - "clear_usercode", - {"entity_id": "lock.danalock_v3_btze_locked", "code_slot": 1}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[4] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 1, "ValueIDKey": 72057594219905046} - - # Test set_usercode invalid length - await hass.services.async_call( - "ozw", - "set_usercode", - { - "entity_id": "lock.danalock_v3_btze_locked", - "usercode": "123", - "code_slot": 1, - }, - blocking=True, - ) - assert len(sent_messages) == 5 - assert "User code must be at least 4 digits" in caplog.text diff --git a/tests/components/ozw/test_scenes.py b/tests/components/ozw/test_scenes.py deleted file mode 100644 index 1c510d58a3c..00000000000 --- a/tests/components/ozw/test_scenes.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Test Z-Wave (central) Scenes.""" -from .common import MQTTMessage, setup_ozw - -from tests.common import async_capture_events - - -async def test_scenes(hass, generic_data, sent_messages): - """Test setting up config entry.""" - - receive_message = await setup_ozw(hass, fixture=generic_data) - events = async_capture_events(hass, "ozw.scene_activated") - - # Publish fake scene event on mqtt - message = MQTTMessage( - topic="OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/", - payload={ - "Label": "Scene", - "Value": 16, - "Units": "", - "Min": -2147483648, - "Max": 2147483647, - "Type": "Int", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", - "Index": 0, - "Node": 7, - "Genre": "User", - "Help": "", - "ValueIDKey": 122339347, - "ReadOnly": False, - "WriteOnly": False, - "ValueSet": False, - "ValuePolled": False, - "ChangeVerified": False, - "Event": "valueChanged", - "TimeStamp": 1579630367, - }, - ) - message.encode() - receive_message(message) - # wait for the event - await hass.async_block_till_done() - assert len(events) == 1 - assert events[0].data["scene_value_id"] == 16 - - # Publish fake central scene event on mqtt - message = MQTTMessage( - topic="OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/", - payload={ - "Label": "Scene 1", - "Value": { - "List": [ - {"Value": 0, "Label": "Inactive"}, - {"Value": 1, "Label": "Pressed 1 Time"}, - {"Value": 2, "Label": "Key Released"}, - {"Value": 3, "Label": "Key Held down"}, - ], - "Selected": "Pressed 1 Time", - "Selected_id": 1, - }, - "Units": "", - "Min": 0, - "Max": 0, - "Type": "List", - "Instance": 1, - "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", - "Index": 1, - "Node": 61, - "Genre": "User", - "Help": "", - "ValueIDKey": 281476005806100, - "ReadOnly": False, - "WriteOnly": False, - "ValueSet": False, - "ValuePolled": False, - "ChangeVerified": False, - "Event": "valueChanged", - "TimeStamp": 1579640710, - }, - ) - message.encode() - receive_message(message) - # wait for the event - await hass.async_block_till_done() - assert len(events) == 2 - assert events[1].data["scene_id"] == 1 - assert events[1].data["scene_label"] == "Scene 1" - assert events[1].data["scene_value_label"] == "Pressed 1 Time" - assert events[1].data["instance_id"] == 1 diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py deleted file mode 100644 index 2eddb9722e5..00000000000 --- a/tests/components/ozw/test_sensor.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Test Z-Wave Sensors.""" -from homeassistant.components.ozw.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass -from homeassistant.const import ATTR_DEVICE_CLASS -from homeassistant.helpers import entity_registry as er - -from .common import setup_ozw - - -async def test_sensor(hass, generic_data): - """Test setting up config entry.""" - await setup_ozw(hass, fixture=generic_data) - - # Test standard sensor - state = hass.states.get("sensor.smart_plug_electric_v") - assert state is not None - assert state.state == "123.9" - assert state.attributes["unit_of_measurement"] == "V" - - # Test device classes - state = hass.states.get("sensor.trisensor_relative_humidity") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.HUMIDITY - state = hass.states.get("sensor.trisensor_pressure") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.PRESSURE - state = hass.states.get("sensor.trisensor_fake_power") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - state = hass.states.get("sensor.trisensor_fake_energy") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - state = hass.states.get("sensor.trisensor_fake_electric") - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - - # Test ZWaveListSensor disabled by default - registry = er.async_get(hass) - entity_id = "sensor.water_sensor_6_instance_1_water" - state = hass.states.get(entity_id) - assert state is None - - entry = registry.async_get(entity_id) - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION - - # Test enabling entity - updated_entry = registry.async_update_entity( - entry.entity_id, **{"disabled_by": None} - ) - assert updated_entry != entry - assert updated_entry.disabled is False - - -async def test_sensor_enabled(hass, generic_data, sensor_msg): - """Test enabling an advanced sensor.""" - - registry = er.async_get(hass) - - entry = registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "1-36-1407375493578772", - suggested_object_id="water_sensor_6_instance_1_water", - disabled_by=None, - ) - assert entry.disabled is False - - receive_msg = await setup_ozw(hass, fixture=generic_data) - receive_msg(sensor_msg) - await hass.async_block_till_done() - - state = hass.states.get(entry.entity_id) - assert state is not None - assert state.state == "0" - assert state.attributes["label"] == "Clear" - - -async def test_string_sensor(hass, string_sensor_data): - """Test so the returned type is a string sensor.""" - - registry = er.async_get(hass) - - entry = registry.async_get_or_create( - SENSOR_DOMAIN, - DOMAIN, - "1-49-73464969749610519", - suggested_object_id="id_150_z_wave_module_user_code", - disabled_by=None, - ) - - await setup_ozw(hass, fixture=string_sensor_data) - await hass.async_block_till_done() - - state = hass.states.get(entry.entity_id) - assert state is not None - assert state.state == "asdfgh" diff --git a/tests/components/ozw/test_services.py b/tests/components/ozw/test_services.py deleted file mode 100644 index 7c71b234242..00000000000 --- a/tests/components/ozw/test_services.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Test Z-Wave Services.""" -from openzwavemqtt.const import ATTR_POSITION, ATTR_VALUE -from openzwavemqtt.exceptions import InvalidValueError, NotFoundError, WrongTypeError -import pytest - -from .common import setup_ozw - - -async def test_services(hass, light_data, sent_messages): - """Test services on lock.""" - await setup_ozw(hass, fixture=light_data) - - # Test set_config_parameter list by label - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": "Disable"}, - blocking=True, - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475641245716} - - # Test set_config_parameter list by index int - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": 1}, - blocking=True, - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 1, "ValueIDKey": 281475641245716} - - # Test set_config_parameter int - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 3, "value": 55}, - blocking=True, - ) - assert len(sent_messages) == 3 - msg = sent_messages[2] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 55, "ValueIDKey": 844425594667027} - - # Test set_config_parameter invalid list int - with pytest.raises(NotFoundError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": 12}, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter invalid list value - with pytest.raises(NotFoundError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 1, "value": "Blah"}, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter invalid list value type - with pytest.raises(WrongTypeError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - { - "node_id": 39, - "parameter": 1, - "value": {ATTR_VALUE: True, ATTR_POSITION: 1}, - }, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter int out of range - with pytest.raises(InvalidValueError): - assert await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 3, "value": 2147483657}, - blocking=True, - ) - assert len(sent_messages) == 3 - - # Test set_config_parameter short - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 81, "value": 3000}, - blocking=True, - ) - assert len(sent_messages) == 4 - msg = sent_messages[3] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 3000, "ValueIDKey": 22799473778098198} - - # Test set_config_parameter byte - await hass.services.async_call( - "ozw", - "set_config_parameter", - {"node_id": 39, "parameter": 16, "value": 20}, - blocking=True, - ) - assert len(sent_messages) == 5 - msg = sent_messages[4] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 20, "ValueIDKey": 4503600291905553} diff --git a/tests/components/ozw/test_switch.py b/tests/components/ozw/test_switch.py deleted file mode 100644 index 7af331b3e0f..00000000000 --- a/tests/components/ozw/test_switch.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Test Z-Wave Switches.""" -from .common import setup_ozw - - -async def test_switch(hass, generic_data, sent_messages, switch_msg): - """Test setting up config entry.""" - receive_message = await setup_ozw(hass, fixture=generic_data) - - # Test loaded - state = hass.states.get("switch.smart_plug_switch") - assert state is not None - assert state.state == "off" - - # Test turning on - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.smart_plug_switch"}, blocking=True - ) - assert len(sent_messages) == 1 - msg = sent_messages[0] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": True, "ValueIDKey": 541671440} - - # Feedback on state - switch_msg.decode() - switch_msg.payload["Value"] = True - switch_msg.encode() - receive_message(switch_msg) - await hass.async_block_till_done() - - state = hass.states.get("switch.smart_plug_switch") - assert state is not None - assert state.state == "on" - - # Test turning off - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.smart_plug_switch"}, blocking=True - ) - assert len(sent_messages) == 2 - msg = sent_messages[1] - assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": False, "ValueIDKey": 541671440} diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py deleted file mode 100644 index 2cbe69f1c98..00000000000 --- a/tests/components/ozw/test_websocket_api.py +++ /dev/null @@ -1,387 +0,0 @@ -"""Test OpenZWave Websocket API.""" -from unittest.mock import patch - -from openzwavemqtt.const import ( - ATTR_CODE_SLOT, - ATTR_LABEL, - ATTR_OPTIONS, - ATTR_POSITION, - ATTR_VALUE, - ValueType, -) - -from homeassistant.components.ozw.const import ATTR_CONFIG_PARAMETER -from homeassistant.components.ozw.lock import ATTR_USERCODE -from homeassistant.components.ozw.websocket_api import ( - ATTR_IS_AWAKE, - ATTR_IS_BEAMING, - ATTR_IS_FAILED, - ATTR_IS_FLIRS, - ATTR_IS_ROUTING, - ATTR_IS_SECURITYV1, - ATTR_IS_ZWAVE_PLUS, - ATTR_NEIGHBORS, - ATTR_NODE_BASIC_STRING, - ATTR_NODE_BAUD_RATE, - ATTR_NODE_GENERIC_STRING, - ATTR_NODE_QUERY_STAGE, - ATTR_NODE_SPECIFIC_STRING, - ID, - NODE_ID, - OZW_INSTANCE, - PARAMETER, - SCHEMA, - TYPE, - VALUE, -) -from homeassistant.components.websocket_api.const import ( - ERR_INVALID_FORMAT, - ERR_NOT_FOUND, - ERR_NOT_SUPPORTED, -) - -from .common import MQTTMessage, setup_ozw - - -async def test_websocket_api(hass, generic_data, hass_ws_client, mqtt_mock): - """Test the ozw websocket api.""" - await setup_ozw(hass, fixture=generic_data) - client = await hass_ws_client(hass) - - # Test instance list - await client.send_json({ID: 4, TYPE: "ozw/get_instances"}) - msg = await client.receive_json() - assert len(msg["result"]) == 1 - result = msg["result"][0] - assert result[OZW_INSTANCE] == 1 - assert result["Status"] == "driverAllNodesQueried" - assert result["OpenZWave_Version"] == "1.6.1008" - - # Test network status - await client.send_json({ID: 5, TYPE: "ozw/network_status"}) - msg = await client.receive_json() - result = msg["result"] - - assert result["Status"] == "driverAllNodesQueried" - assert result[OZW_INSTANCE] == 1 - - # Test node status - await client.send_json({ID: 6, TYPE: "ozw/node_status", NODE_ID: 32}) - msg = await client.receive_json() - result = msg["result"] - - assert result[OZW_INSTANCE] == 1 - assert result[NODE_ID] == 32 - assert result[ATTR_NODE_QUERY_STAGE] == "Complete" - assert result[ATTR_IS_ZWAVE_PLUS] - assert result[ATTR_IS_AWAKE] - assert not result[ATTR_IS_FAILED] - assert result[ATTR_NODE_BAUD_RATE] == 100000 - assert result[ATTR_IS_BEAMING] - assert not result[ATTR_IS_FLIRS] - assert result[ATTR_IS_ROUTING] - assert not result[ATTR_IS_SECURITYV1] - assert result[ATTR_NODE_BASIC_STRING] == "Routing Slave" - assert result[ATTR_NODE_GENERIC_STRING] == "Binary Switch" - assert result[ATTR_NODE_SPECIFIC_STRING] == "Binary Power Switch" - assert result[ATTR_NEIGHBORS] == [1, 33, 36, 37, 39] - - await client.send_json({ID: 7, TYPE: "ozw/node_status", NODE_ID: 999}) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test node statistics - await client.send_json({ID: 8, TYPE: "ozw/node_statistics", NODE_ID: 39}) - msg = await client.receive_json() - result = msg["result"] - - assert result[OZW_INSTANCE] == 1 - assert result[NODE_ID] == 39 - assert result["send_count"] == 57 - assert result["sent_failed"] == 0 - assert result["retries"] == 1 - assert result["last_request_rtt"] == 26 - assert result["last_response_rtt"] == 38 - assert result["average_request_rtt"] == 29 - assert result["average_response_rtt"] == 37 - assert result["received_packets"] == 3594 - assert result["received_dup_packets"] == 12 - assert result["received_unsolicited"] == 3546 - - # Test node metadata - await client.send_json({ID: 9, TYPE: "ozw/node_metadata", NODE_ID: 39}) - msg = await client.receive_json() - result = msg["result"] - assert result["metadata"]["ProductPic"] == "images/aeotec/zwa002.png" - - await client.send_json({ID: 10, TYPE: "ozw/node_metadata", NODE_ID: 999}) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test network statistics - await client.send_json({ID: 11, TYPE: "ozw/network_statistics"}) - msg = await client.receive_json() - result = msg["result"] - assert result["readCnt"] == 92220 - assert result[OZW_INSTANCE] == 1 - assert result["node_count"] == 5 - - # Test get nodes - await client.send_json({ID: 12, TYPE: "ozw/get_nodes"}) - msg = await client.receive_json() - result = msg["result"] - assert len(result) == 5 - assert result[2][ATTR_IS_AWAKE] - assert not result[1][ATTR_IS_FAILED] - - # Test get config parameters - await client.send_json({ID: 13, TYPE: "ozw/get_config_parameters", NODE_ID: 39}) - msg = await client.receive_json() - result = msg["result"] - assert len(result) == 8 - for config_param in result: - assert config_param["type"] in ( - ValueType.LIST.value, - ValueType.BOOL.value, - ValueType.INT.value, - ValueType.BYTE.value, - ValueType.SHORT.value, - ValueType.BITSET.value, - ) - - # Test set config parameter - config_param = result[0] - current_val = config_param[ATTR_VALUE] - new_val = next( - option[0] - for option in config_param[SCHEMA][0][ATTR_OPTIONS] - if option[0] != current_val - ) - new_label = next( - option[1] - for option in config_param[SCHEMA][0][ATTR_OPTIONS] - if option[1] != current_val and option[0] != new_val - ) - await client.send_json( - { - ID: 14, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: config_param[ATTR_CONFIG_PARAMETER], - VALUE: new_val, - } - ) - msg = await client.receive_json() - assert msg["success"] - await client.send_json( - { - ID: 15, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: config_param[ATTR_CONFIG_PARAMETER], - VALUE: new_label, - } - ) - msg = await client.receive_json() - assert msg["success"] - - # Test OZW Instance not found error - await client.send_json( - {ID: 16, TYPE: "ozw/get_config_parameters", OZW_INSTANCE: 999, NODE_ID: 1} - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test OZW Node not found error - await client.send_json( - { - ID: 18, - TYPE: "ozw/set_config_parameter", - NODE_ID: 999, - PARAMETER: 0, - VALUE: "test", - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test parameter not found - await client.send_json( - { - ID: 19, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 45, - VALUE: "test", - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test list value not found - await client.send_json( - { - ID: 20, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: config_param[ATTR_CONFIG_PARAMETER], - VALUE: "test", - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - # Test value type invalid - await client.send_json( - { - ID: 21, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 3, - VALUE: 0, - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_SUPPORTED - - # Test invalid bitset format - await client.send_json( - { - ID: 22, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 3, - VALUE: {ATTR_POSITION: 1, ATTR_VALUE: True, ATTR_LABEL: "test"}, - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_INVALID_FORMAT - - # Test valid bitset format passes validation - await client.send_json( - { - ID: 23, - TYPE: "ozw/set_config_parameter", - NODE_ID: 39, - PARAMETER: 10000, - VALUE: {ATTR_POSITION: 1, ATTR_VALUE: True}, - } - ) - msg = await client.receive_json() - result = msg["error"] - assert result["code"] == ERR_NOT_FOUND - - -async def test_ws_locks(hass, lock_data, hass_ws_client, mqtt_mock): - """Test lock websocket apis.""" - await setup_ozw(hass, fixture=lock_data) - client = await hass_ws_client(hass) - - await client.send_json( - { - ID: 1, - TYPE: "ozw/get_code_slots", - NODE_ID: 10, - } - ) - msg = await client.receive_json() - assert msg["success"] - - await client.send_json( - { - ID: 2, - TYPE: "ozw/set_usercode", - NODE_ID: 10, - ATTR_CODE_SLOT: 1, - ATTR_USERCODE: "1234", - } - ) - msg = await client.receive_json() - assert msg["success"] - - await client.send_json( - { - ID: 3, - TYPE: "ozw/clear_usercode", - NODE_ID: 10, - ATTR_CODE_SLOT: 1, - } - ) - msg = await client.receive_json() - assert msg["success"] - - -async def test_refresh_node( - hass, generic_data, sent_messages, hass_ws_client, mqtt_mock -): - """Test the ozw refresh node api.""" - receive_message = await setup_ozw(hass, fixture=generic_data) - client = await hass_ws_client(hass) - - # Send the refresh_node_info command - await client.send_json({ID: 9, TYPE: "ozw/refresh_node_info", NODE_ID: 39}) - msg = await client.receive_json() - - assert len(sent_messages) == 1 - assert msg["success"] - - # Receive a mock status update from OZW - message = MQTTMessage( - topic="OpenZWave/1/node/39/", - payload={"NodeID": 39, "NodeQueryStage": "initializing"}, - ) - message.encode() - receive_message(message) - - # Verify we got expected data on the websocket - msg = await client.receive_json() - result = msg["event"] - assert result["type"] == "node_updated" - assert result["node_query_stage"] == "initializing" - - # Send another mock status update from OZW - message = MQTTMessage( - topic="OpenZWave/1/node/39/", - payload={"NodeID": 39, "NodeQueryStage": "versions"}, - ) - message.encode() - receive_message(message) - - # Send a mock status update for a different node - message = MQTTMessage( - topic="OpenZWave/1/node/35/", - payload={"NodeID": 35, "NodeQueryStage": "fake_shouldnt_be_received"}, - ) - message.encode() - receive_message(message) - - # Verify we received the message for node 39 but not for node 35 - msg = await client.receive_json() - result = msg["event"] - assert result["type"] == "node_updated" - assert result["node_query_stage"] == "versions" - - -async def test_refresh_node_unsubscribe(hass, generic_data, hass_ws_client, mqtt_mock): - """Test unsubscribing the ozw refresh node api.""" - await setup_ozw(hass, fixture=generic_data) - client = await hass_ws_client(hass) - - with patch("openzwavemqtt.OZWOptions.listen") as mock_listen: - # Send the refresh_node_info command - await client.send_json({ID: 9, TYPE: "ozw/refresh_node_info", NODE_ID: 39}) - await client.receive_json() - - # Send the unsubscribe command - await client.send_json({ID: 10, TYPE: "unsubscribe_events", "subscription": 9}) - await client.receive_json() - - assert mock_listen.return_value.called From 38306417ad75460637f048e18396f92f7536a754 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Mon, 14 Mar 2022 12:36:16 -0400 Subject: [PATCH 0429/1054] Bump amcrest version to 1.9.7 (#68055) --- homeassistant/components/amcrest/__init__.py | 3 +-- homeassistant/components/amcrest/camera.py | 16 +++++++++++----- homeassistant/components/amcrest/const.py | 3 +++ homeassistant/components/amcrest/manifest.json | 2 +- requirements_all.txt | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 166d2e74ffd..f2472575259 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -52,6 +52,7 @@ from .const import ( DATA_AMCREST, DEVICES, DOMAIN, + RESOLUTION_LIST, SERVICE_EVENT, SERVICE_UPDATE, ) @@ -76,8 +77,6 @@ RECHECK_INTERVAL = timedelta(minutes=1) NOTIFICATION_ID = "amcrest_notification" NOTIFICATION_TITLE = "Amcrest Camera Setup" -RESOLUTION_LIST = {"high": 0, "low": 1} - SCAN_INTERVAL = timedelta(seconds=10) AUTHENTICATION_LIST = {"basic": "basic"} diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 49d5cfd7afd..61b97b041cd 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -35,6 +35,7 @@ from .const import ( DATA_AMCREST, DEVICES, DOMAIN, + RESOLUTION_TO_STREAM, SERVICE_UPDATE, SNAPSHOT_TIMEOUT, ) @@ -533,13 +534,14 @@ class AmcrestCam(Camera): return async def _async_get_video(self) -> bool: - stream = {0: "Main", 1: "Extra"} return await self._api.async_is_video_enabled( - channel=0, stream=stream[self._resolution] + channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] ) async def _async_set_video(self, enable: bool) -> None: - await self._api.async_set_video_enabled(enable, channel=0) + await self._api.async_set_video_enabled( + enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] + ) async def _async_enable_video(self, enable: bool) -> None: """Enable or disable camera video stream.""" @@ -585,10 +587,14 @@ class AmcrestCam(Camera): ) async def _async_get_audio(self) -> bool: - return await self._api.async_audio_enabled + return await self._api.async_is_audio_enabled( + channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] + ) async def _async_set_audio(self, enable: bool) -> None: - await self._api.async_set_audio_enabled(enable) + await self._api.async_set_audio_enabled( + enable, channel=0, stream=RESOLUTION_TO_STREAM[self._resolution] + ) async def _async_enable_audio(self, enable: bool) -> None: """Enable or disable audio stream.""" diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 89cde63a08a..6c2fe431d43 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -13,3 +13,6 @@ SNAPSHOT_TIMEOUT = 20 SERVICE_EVENT = "event" SERVICE_UPDATE = "update" + +RESOLUTION_LIST = {"high": 0, "low": 1} +RESOLUTION_TO_STREAM = {0: "Main", 1: "Extra"} diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 5a7bec89e31..b4646be5e66 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.9.4"], + "requirements": ["amcrest==1.9.7"], "dependencies": ["ffmpeg"], "codeowners": ["@flacjacket"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 17c4452ff00..7e382f092fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -286,7 +286,7 @@ amberelectric==1.0.3 ambiclimate==0.2.1 # homeassistant.components.amcrest -amcrest==1.9.4 +amcrest==1.9.7 # homeassistant.components.androidtv androidtv[async]==0.0.63 From 362191a0e63e7b3d59ba97866805e8f3d28a00b7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 Mar 2022 17:37:48 +0100 Subject: [PATCH 0430/1054] Add typing of deCONZ device_trigger (#67496) --- .../components/deconz/device_trigger.py | 54 ++++++--- .../components/deconz/test_device_trigger.py | 108 ++++++++++++++++-- 2 files changed, 134 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index ae539ee5d48..c76aaf481bf 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -2,8 +2,14 @@ from __future__ import annotations +from typing import Any + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -17,11 +23,13 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType from . import DOMAIN from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, DeconzAlarmEvent, DeconzEvent +from .gateway import DeconzGateway CONF_SUBTYPE = "subtype" @@ -622,7 +630,8 @@ def _get_deconz_event_from_device( device: dr.DeviceEntry, ) -> DeconzAlarmEvent | DeconzEvent: """Resolve deconz event from device.""" - for gateway in hass.data.get(DOMAIN, {}).values(): + gateways: dict[str, DeconzGateway] = hass.data.get(DOMAIN, {}) + for gateway in gateways.values(): for deconz_event in gateway.events: if device.id == deconz_event.device_id: return deconz_event @@ -632,7 +641,10 @@ def _get_deconz_event_from_device( ) -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, + config: dict[str, Any], +) -> vol.Schema: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -656,32 +668,42 @@ async def async_validate_trigger_config(hass, config): return config -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" + event_data: dict[str, int | str] = {} + device_registry = dr.async_get(hass) - device = device_registry.async_get(config[CONF_DEVICE_ID]) - - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - - trigger = REMOTES[device.model][trigger] + device = device_registry.devices[config[CONF_DEVICE_ID]] deconz_event = _get_deconz_event_from_device(hass, device) + if event_id := deconz_event.serial: + event_data[CONF_UNIQUE_ID] = event_id - event_id = deconz_event.serial + if device_model := device.model: + config_trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + event_data |= REMOTES[device_model][config_trigger] - event_config = { + raw_event_config = { event_trigger.CONF_PLATFORM: "event", event_trigger.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, - event_trigger.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, **trigger}, + event_trigger.CONF_EVENT_DATA: event_data, } - event_config = event_trigger.TRIGGER_SCHEMA(event_config) + event_config = event_trigger.TRIGGER_SCHEMA(raw_event_config) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, + device_id: str, +) -> list | None: """List device triggers. Make sure device is a supported remote model. @@ -689,10 +711,10 @@ async def async_get_triggers(hass, device_id): Generate device trigger list. """ device_registry = dr.async_get(hass) - device = device_registry.async_get(device_id) + device = device_registry.devices[device_id] if device.model not in REMOTES: - return + return None triggers = [] for trigger, subtype in REMOTES[device.model].keys(): diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 4ae8fd32e45..91d8e0e1ea2 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -5,6 +5,13 @@ from unittest.mock import Mock, patch import pytest from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.binary_sensor.device_trigger import ( + CONF_BAT_LOW, + CONF_NOT_BAT_LOW, + CONF_NOT_TAMPERED, + CONF_TAMPERED, +) from homeassistant.components.deconz import device_trigger from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.device_trigger import CONF_SUBTYPE @@ -129,6 +136,91 @@ async def test_get_triggers(hass, aioclient_mock): assert_lists_same(triggers, expected_triggers) +async def test_get_triggers_for_alarm_event(hass, aioclient_mock): + """Test triggers work.""" + data = { + "sensors": { + "1": { + "config": { + "battery": 95, + "enrolled": 1, + "on": True, + "pending": [], + "reachable": True, + }, + "ep": 1, + "etag": "5aaa1c6bae8501f59929539c6e8f44d6", + "lastseen": "2021-07-25T18:07Z", + "manufacturername": "lk", + "modelid": "ZB-KeypadGeneric-D0002", + "name": "Keypad", + "state": { + "action": "armed_stay", + "lastupdated": "2021-07-25T18:02:51.172", + "lowbattery": False, + "panel": "exit_delay", + "seconds_remaining": 55, + "tampered": False, + }, + "swversion": "3.13", + "type": "ZHAAncillaryControl", + "uniqueid": "00:00:00:00:00:00:00:00-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:00")} + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + + expected_triggers = [ + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_low_battery", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_BAT_LOW, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_low_battery", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_NOT_BAT_LOW, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_tampered", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_TAMPERED, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: BINARY_SENSOR_DOMAIN, + ATTR_ENTITY_ID: "binary_sensor.keypad_tampered", + CONF_PLATFORM: "device", + CONF_TYPE: CONF_NOT_TAMPERED, + }, + { + CONF_DEVICE_ID: device.id, + CONF_DOMAIN: SENSOR_DOMAIN, + ATTR_ENTITY_ID: "sensor.keypad_battery", + CONF_PLATFORM: "device", + CONF_TYPE: ATTR_BATTERY_LEVEL, + }, + ] + + assert_lists_same(triggers, expected_triggers) + + async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock): """Verify no triggers for an unsupported remote.""" data = { @@ -244,9 +336,7 @@ async def test_functional_device_trigger( assert automation_calls[0].data["some"] == "test_trigger_button_press" -async def test_validate_trigger_unknown_device( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_validate_trigger_unknown_device(hass, aioclient_mock): """Test unknown device does not return a trigger config.""" await setup_deconz_integration(hass, aioclient_mock) @@ -276,9 +366,7 @@ async def test_validate_trigger_unknown_device( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_validate_trigger_unsupported_device( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_validate_trigger_unsupported_device(hass, aioclient_mock): """Test unsupported device doesn't return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) @@ -315,9 +403,7 @@ async def test_validate_trigger_unsupported_device( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_validate_trigger_unsupported_trigger( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_validate_trigger_unsupported_trigger(hass, aioclient_mock): """Test unsupported trigger does not return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) @@ -356,9 +442,7 @@ async def test_validate_trigger_unsupported_trigger( assert len(hass.states.async_entity_ids(AUTOMATION_DOMAIN)) == 0 -async def test_attach_trigger_no_matching_event( - hass, aioclient_mock, mock_deconz_websocket -): +async def test_attach_trigger_no_matching_event(hass, aioclient_mock): """Test no matching event for device doesn't return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) From 1bb0f5281499b366d16e60244d1bc27fb5362460 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Mon, 14 Mar 2022 12:38:40 -0400 Subject: [PATCH 0431/1054] Set isolated_build = True for tox (#67238) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index b47a6c94ac8..441960fbbf5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = py39, lint, pylint, typing, cov skip_missing_interpreters = True ignore_basepython_conflict = True +isolated_build = True [testenv] basepython = {env:PYTHON3_PATH:python3} From 4506d453455ab1efb7b75e0540fcbf59998c5797 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 Mar 2022 17:40:58 +0100 Subject: [PATCH 0432/1054] Add typing of deconz_event (#67497) --- .../components/deconz/deconz_event.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 300aef3f82a..c0b4f763c37 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,5 +1,7 @@ """Representation of a deCONZ remote or keypad.""" +from __future__ import annotations + from pydeconz.sensor import ( ANCILLARY_CONTROL_EMERGENCY, ANCILLARY_CONTROL_FIRE, @@ -23,6 +25,7 @@ from homeassistant.util import slugify from .const import CONF_ANGLE, CONF_GESTURE, LOGGER from .deconz_device import DeconzBase +from .gateway import DeconzGateway CONF_DECONZ_EVENT = "deconz_event" CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" @@ -35,11 +38,13 @@ SUPPORTED_DECONZ_ALARM_EVENTS = { } -async def async_setup_events(gateway) -> None: +async def async_setup_events(gateway: DeconzGateway) -> None: """Set up the deCONZ events.""" @callback - def async_add_sensor(sensors=gateway.api.sensors.values()): + def async_add_sensor( + sensors: AncillaryControl | Switch = gateway.api.sensors.values(), + ) -> None: """Create DeconzEvent.""" new_events = [] known_events = {event.unique_id for event in gateway.events} @@ -74,7 +79,7 @@ async def async_setup_events(gateway) -> None: @callback -def async_unload_events(gateway) -> None: +def async_unload_events(gateway: DeconzGateway) -> None: """Unload all deCONZ events.""" for event in gateway.events: event.async_will_remove_from_hass() @@ -89,18 +94,22 @@ class DeconzEvent(DeconzBase): instead of a sensor entity in hass. """ - def __init__(self, device, gateway): + def __init__( + self, + device: AncillaryControl | Switch, + gateway: DeconzGateway, + ) -> None: """Register callback that will be used for signals.""" super().__init__(device, gateway) self._device.register_callback(self.async_update_callback) - self.device_id = None + self.device_id: str | None = None self.event_id = slugify(self._device.name) LOGGER.debug("deCONZ event created: %s", self.event_id) @property - def device(self): + def device(self) -> AncillaryControl | Switch: """Return Event device.""" return self._device @@ -110,7 +119,7 @@ class DeconzEvent(DeconzBase): self._device.remove_callback(self.async_update_callback) @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Fire the event if reason is that state is updated.""" if ( self.gateway.ignore_state_updates @@ -154,8 +163,10 @@ class DeconzEvent(DeconzBase): class DeconzAlarmEvent(DeconzEvent): """Alarm control panel companion event when user interacts with a keypad.""" + _device: AncillaryControl + @callback - def async_update_callback(self): + def async_update_callback(self) -> None: """Fire the event if reason is new action is updated.""" if ( self.gateway.ignore_state_updates From ed6466f706f0b59a5e3406ba7b98ad0f00a3331a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 18:45:45 +0100 Subject: [PATCH 0433/1054] Add Lock platform to Switch as X (#68123) Co-authored-by: Erik Montnemery --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/fan.py | 2 +- homeassistant/components/switch_as_x/lock.py | 83 +++++++++++++ tests/components/switch_as_x/test_init.py | 6 + tests/components/switch_as_x/test_lock.py | 110 ++++++++++++++++++ 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switch_as_x/lock.py create mode 100644 tests/components/switch_as_x/test_lock.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 6247d55b1fa..5c3c68e9353 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -25,6 +25,7 @@ CONFIG_FLOW = { {"value": Platform.COVER, "label": "Cover"}, {"value": Platform.FAN, "label": "Fan"}, {"value": Platform.LIGHT, "label": "Light"}, + {"value": Platform.LOCK, "label": "Lock"}, {"value": Platform.SIREN, "label": "Siren"}, ] } diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index 546e22b3fc1..e87f49b1b7b 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -44,7 +44,7 @@ class FanSwitch(BaseToggleEntity, FanEntity): """Return true if the entity is on. Fan logic uses speed percentage or preset mode to determine - its it on or off, however, when using a wrapped switch, we + if it's on or off, however, when using a wrapped switch, we just use the wrapped switch's state. """ return self._attr_is_on diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py new file mode 100644 index 00000000000..0eaabb03770 --- /dev/null +++ b/homeassistant/components/switch_as_x/lock.py @@ -0,0 +1,83 @@ +"""Lock support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Lock Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + LockSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class LockSwitch(BaseEntity, LockEntity): + """Represents a Switch as a Lock.""" + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + # Logic is the same as the lock device class for binary sensors + # on means open (unlocked), off means closed (locked) + self._attr_is_locked = state.state != STATE_ON diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 7a7002de094..b36ce64e593 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -17,6 +17,7 @@ from tests.common import MockConfigEntry Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -114,6 +115,7 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -180,6 +182,7 @@ async def test_device_registry_config_entry_1( Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -239,6 +242,7 @@ async def test_device_registry_config_entry_2( Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -282,6 +286,7 @@ async def test_config_entry_entity_id( Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -314,6 +319,7 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) diff --git a/tests/components/switch_as_x/test_lock.py b/tests/components/switch_as_x/test_lock.py new file mode 100644 index 00000000000..de4c729e492 --- /dev/null +++ b/tests/components/switch_as_x/test_lock.py @@ -0,0 +1,110 @@ +"""Tests for the Switch as X Lock platform.""" +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_LOCK, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_UNLOCK, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_UNLOCKED, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test lock switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.LOCK, + }, + title="candy_jar", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("lock.candy_jar") + assert state is not None + assert state.state == "unavailable" + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as lock entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.LOCK, + }, + title="candy_jar", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("lock.candy_jar").state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {CONF_ENTITY_ID: "lock.candy_jar"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.candy_jar").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {CONF_ENTITY_ID: "lock.candy_jar"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("lock.candy_jar").state == STATE_UNLOCKED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.candy_jar").state == STATE_LOCKED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("lock.candy_jar").state == STATE_UNLOCKED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.candy_jar").state == STATE_LOCKED From 2a538f6ae1bcfd6b7b46a0720f4a32324160a344 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 14 Mar 2022 20:14:45 +0200 Subject: [PATCH 0434/1054] Downgrade SSDP failed to setup listener warning to debug (#68129) --- homeassistant/components/ssdp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 80d9a716df0..cdc5fe3242e 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -449,7 +449,7 @@ class Scanner: failed_listeners = [] for idx, result in enumerate(results): if isinstance(result, Exception): - _LOGGER.warning( + _LOGGER.debug( "Failed to setup listener for %s: %s", self._ssdp_listeners[idx].source, result, From bff91b170f3bbb979ca7d4ee7c07b12b405e1914 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 Mar 2022 19:34:05 +0100 Subject: [PATCH 0435/1054] Complete typing of some platforms of deCONZ integration (#67494) --- .../components/deconz/binary_sensor.py | 28 +++++++++---------- homeassistant/components/deconz/light.py | 14 +++++++--- homeassistant/components/deconz/logbook.py | 3 +- homeassistant/components/deconz/number.py | 4 +-- homeassistant/components/deconz/sensor.py | 8 +++--- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index fd674dc1cba..1a37236fae4 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -70,7 +70,7 @@ ENTITY_DESCRIPTIONS = { Alarm: [ DeconzBinarySensorDescription( key="alarm", - value_fn=lambda device: device.alarm, + value_fn=lambda device: device.alarm, # type: ignore[no-any-return] suffix="", update_key="alarm", device_class=BinarySensorDeviceClass.SAFETY, @@ -79,7 +79,7 @@ ENTITY_DESCRIPTIONS = { CarbonMonoxide: [ DeconzBinarySensorDescription( key="carbon_monoxide", - value_fn=lambda device: device.carbon_monoxide, + value_fn=lambda device: device.carbon_monoxide, # type: ignore[no-any-return] suffix="", update_key="carbonmonoxide", device_class=BinarySensorDeviceClass.CO, @@ -88,14 +88,14 @@ ENTITY_DESCRIPTIONS = { Fire: [ DeconzBinarySensorDescription( key="fire", - value_fn=lambda device: device.fire, + value_fn=lambda device: device.fire, # type: ignore[no-any-return] suffix="", update_key="fire", device_class=BinarySensorDeviceClass.SMOKE, ), DeconzBinarySensorDescription( key="in_test_mode", - value_fn=lambda device: device.in_test_mode, + value_fn=lambda device: device.in_test_mode, # type: ignore[no-any-return] suffix="Test Mode", update_key="test", device_class=BinarySensorDeviceClass.SMOKE, @@ -105,7 +105,7 @@ ENTITY_DESCRIPTIONS = { GenericFlag: [ DeconzBinarySensorDescription( key="flag", - value_fn=lambda device: device.flag, + value_fn=lambda device: device.flag, # type: ignore[no-any-return] suffix="", update_key="flag", ) @@ -113,7 +113,7 @@ ENTITY_DESCRIPTIONS = { OpenClose: [ DeconzBinarySensorDescription( key="open", - value_fn=lambda device: device.open, + value_fn=lambda device: device.open, # type: ignore[no-any-return] suffix="", update_key="open", device_class=BinarySensorDeviceClass.OPENING, @@ -122,7 +122,7 @@ ENTITY_DESCRIPTIONS = { Presence: [ DeconzBinarySensorDescription( key="presence", - value_fn=lambda device: device.presence, + value_fn=lambda device: device.presence, # type: ignore[no-any-return] suffix="", update_key="presence", device_class=BinarySensorDeviceClass.MOTION, @@ -131,7 +131,7 @@ ENTITY_DESCRIPTIONS = { Vibration: [ DeconzBinarySensorDescription( key="vibration", - value_fn=lambda device: device.vibration, + value_fn=lambda device: device.vibration, # type: ignore[no-any-return] suffix="", update_key="vibration", device_class=BinarySensorDeviceClass.VIBRATION, @@ -140,7 +140,7 @@ ENTITY_DESCRIPTIONS = { Water: [ DeconzBinarySensorDescription( key="water", - value_fn=lambda device: device.water, + value_fn=lambda device: device.water, # type: ignore[no-any-return] suffix="", update_key="water", device_class=BinarySensorDeviceClass.MOISTURE, @@ -151,7 +151,7 @@ ENTITY_DESCRIPTIONS = { BINARY_SENSOR_DESCRIPTIONS = [ DeconzBinarySensorDescription( key="tampered", - value_fn=lambda device: device.tampered, + value_fn=lambda device: device.tampered, # type: ignore[no-any-return] suffix="Tampered", update_key="tampered", device_class=BinarySensorDeviceClass.TAMPER, @@ -159,7 +159,7 @@ BINARY_SENSOR_DESCRIPTIONS = [ ), DeconzBinarySensorDescription( key="low_battery", - value_fn=lambda device: device.low_battery, + value_fn=lambda device: device.low_battery, # type: ignore[no-any-return] suffix="Low Battery", update_key="lowbattery", device_class=BinarySensorDeviceClass.BATTERY, @@ -266,11 +266,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): @property def extra_state_attributes(self) -> dict[str, bool | float | int | list | None]: """Return the state attributes of the sensor.""" - if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: - return - attr: dict[str, bool | float | int | list | None] = {} + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: + return attr + if self._device.on is not None: attr[ATTR_ON] = self._device.on diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e3cf6442079..4fb401ffab2 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import ValuesView -from typing import Any +from typing import Any, cast from pydeconz.group import Group from pydeconz.light import ( @@ -220,11 +220,15 @@ class DeconzBaseLight(DeconzDevice, LightEntity): elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + if ( + alert := FLASH_TO_DECONZ.get(cast(str, kwargs.get(ATTR_FLASH))) + ) is not None: data["alert"] = alert del data["on"] - if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: + if ( + effect := EFFECT_TO_DECONZ.get(cast(str, kwargs.get(ATTR_EFFECT))) + ) is not None: data["effect"] = effect await self._device.set_state(**data) @@ -240,7 +244,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["brightness"] = 0 data["transition_time"] = int(attr_transition * 10) - if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + if ( + alert := FLASH_TO_DECONZ.get(cast(str, kwargs.get(ATTR_FLASH))) + ) is not None: data["alert"] = alert del data["on"] diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 1c41feda7da..3dedeb4bfac 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -4,9 +4,8 @@ from __future__ import annotations from collections.abc import Callable from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.event import Event from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index bf138aaef63..532cb92ebdf 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -28,7 +28,7 @@ class DeconzNumberDescriptionMixin: suffix: str update_key: str - value_fn: Callable[[PydeconzSensor], bool | None] + value_fn: Callable[[PydeconzSensor], float | None] @dataclass @@ -40,7 +40,7 @@ ENTITY_DESCRIPTIONS = { Presence: [ DeconzNumberDescription( key="delay", - value_fn=lambda device: device.delay, + value_fn=lambda device: device.delay, # type: ignore[no-any-return] suffix="Delay", update_key=PRESENCE_DELAY, max_value=65535, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index b0df644f1bd..95db9625139 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -75,7 +75,7 @@ class DeconzSensorDescriptionMixin: """Required values when describing secondary sensor attributes.""" update_key: str - value_fn: Callable[[PydeconzSensor], float | int | None] + value_fn: Callable[[PydeconzSensor], float | int | str | None] @dataclass @@ -334,14 +334,14 @@ class DeconzSensor(DeconzDevice, SensorEntity): """Return the state of the sensor.""" if self.entity_description.device_class is SensorDeviceClass.TIMESTAMP: return dt_util.parse_datetime( - self.entity_description.value_fn(self._device) + self.entity_description.value_fn(self._device) # type: ignore[arg-type] ) return self.entity_description.value_fn(self._device) @property - def extra_state_attributes(self) -> dict[str, bool | float | int | None]: + def extra_state_attributes(self) -> dict[str, bool | float | int | str | None]: """Return the state attributes of the sensor.""" - attr: dict[str, bool | float | int | None] = {} + attr: dict[str, bool | float | int | str | None] = {} if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: return attr From 0e602dd39083c8754a82922cf3789a7504207451 Mon Sep 17 00:00:00 2001 From: Gido Date: Mon, 14 Mar 2022 19:37:34 +0100 Subject: [PATCH 0436/1054] Bump Rova to 0.3.0 (#67688) --- homeassistant/components/rova/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 01f2e2703e8..d1500d33610 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -2,7 +2,7 @@ "domain": "rova", "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", - "requirements": ["rova==0.2.1"], + "requirements": ["rova==0.3.0"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["rova"] diff --git a/requirements_all.txt b/requirements_all.txt index 7e382f092fc..e40750fd1a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2066,7 +2066,7 @@ roombapy==1.6.5 roonapi==0.0.38 # homeassistant.components.rova -rova==0.2.1 +rova==0.3.0 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 8a8d7741d5a3a52df5ba8032944a8aee9ec160db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Mar 2022 08:38:54 -1000 Subject: [PATCH 0437/1054] Fix Magic Home devices with multiple network interfaces (#68029) --- homeassistant/components/flux_led/__init__.py | 32 +++++--- .../components/flux_led/config_flow.py | 57 +++++++++---- .../components/flux_led/discovery.py | 21 +++-- homeassistant/components/flux_led/util.py | 12 +++ tests/components/flux_led/__init__.py | 2 + tests/components/flux_led/test_config_flow.py | 42 ++++++++++ tests/components/flux_led/test_init.py | 82 ++++++++++++++++++- 7 files changed, 215 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 997f053aa3c..56f0f371d60 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -41,6 +41,7 @@ from .discovery import ( async_trigger_discovery, async_update_entry_from_discovery, ) +from .util import mac_matches_by_one _LOGGER = logging.getLogger(__name__) @@ -90,18 +91,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: """Migrate entities when the mac address gets discovered.""" - if not (unique_id := entry.unique_id): - return - entry_id = entry.entry_id @callback def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: - # Old format {entry_id}..... - # New format {unique_id}.... - entity_unique_id = entity_entry.unique_id - if not entity_unique_id.startswith(entry_id): + if not (unique_id := entry.unique_id): + return None + entry_id = entry.entry_id + entity_unique_id = entity_entry.unique_id + entity_mac = entity_unique_id[: len(unique_id)] + new_unique_id = None + if entity_unique_id.startswith(entry_id): + # Old format {entry_id}....., New format {unique_id}.... + new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + elif ( + ":" in entity_mac + and entity_mac != unique_id + and mac_matches_by_one(entity_mac, unique_id) + ): + # Old format {dhcp_mac}....., New format {discovery_mac}.... + new_unique_id = f"{unique_id}{entity_unique_id[len(unique_id):]}" + else: return None - new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" _LOGGER.info( "Migrating unique_id from [%s] to [%s]", entity_unique_id, @@ -148,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id and discovery.get(ATTR_ID): mac = dr.format_mac(cast(str, discovery[ATTR_ID])) - if mac != entry.unique_id: + if not mac_matches_by_one(mac, entry.unique_id): # The device is offline and another flux_led device is now using the ip address raise ConfigEntryNotReady( f"Unexpected device found at {host}; Expected {entry.unique_id}, found {mac}" @@ -157,7 +167,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not discovery_cached: # Only update the entry once we have verified the unique id # is either missing or we have verified it matches - async_update_entry_from_discovery(hass, entry, discovery, device.model_num) + async_update_entry_from_discovery( + hass, entry, discovery, device.model_num, True + ) await _async_migrate_unique_ids(hass, entry) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 5bdd18d1dbd..dfb6ff4a174 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -19,7 +19,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType @@ -43,7 +43,7 @@ from .discovery import ( async_populate_data_from_discovery, async_update_entry_from_discovery, ) -from .util import format_as_flux_mac +from .util import format_as_flux_mac, mac_matches_by_one CONF_DEVICE: Final = "device" @@ -57,6 +57,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._discovered_devices: dict[str, FluxLEDDiscovery] = {} self._discovered_device: FluxLEDDiscovery | None = None + self._allow_update_mac = False @staticmethod @callback @@ -85,37 +86,65 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle integration discovery.""" + self._allow_update_mac = True self._discovered_device = cast(FluxLEDDiscovery, discovery_info) return await self._async_handle_discovery() + async def _async_set_discovered_mac( + self, device: FluxLEDDiscovery, allow_update_mac: bool + ) -> None: + """Set the discovered mac. + + We only allow it to be updated if it comes from udp + discovery since the dhcp mac can be one digit off from + the udp discovery mac for devices with multiple network interfaces + """ + mac_address = device[ATTR_ID] + assert mac_address is not None + mac = dr.format_mac(mac_address) + await self.async_set_unique_id(mac) + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] == device[ATTR_IPADDR] or ( + entry.unique_id + and ":" in entry.unique_id + and mac_matches_by_one(entry.unique_id, mac) + ): + if async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise AbortFlow("already_configured") + async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" device = self._discovered_device assert device is not None - mac_address = device[ATTR_ID] - assert mac_address is not None - mac = dr.format_mac(mac_address) + await self._async_set_discovered_mac(device, self._allow_update_mac) host = device[ATTR_IPADDR] - await self.async_set_unique_id(mac) - for entry in self._async_current_entries(include_ignore=False): - if entry.unique_id == mac or entry.data[CONF_HOST] == host: - if async_update_entry_from_discovery(self.hass, entry, device, None): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == host: return self.async_abort(reason="already_in_progress") if not device[ATTR_MODEL_DESCRIPTION]: + mac_address = device[ATTR_ID] + assert mac_address is not None + mac = dr.format_mac(mac_address) try: device = await self._async_try_connect(host, device) except FLUX_LED_EXCEPTIONS: return self.async_abort(reason="cannot_connect") else: - if device[ATTR_MODEL_DESCRIPTION]: + discovered_mac = device[ATTR_ID] + if device[ATTR_MODEL_DESCRIPTION] or ( + discovered_mac is not None + and (formatted_discovered_mac := dr.format_mac(discovered_mac)) + and formatted_discovered_mac != mac + and mac_matches_by_one(discovered_mac, mac) + ): self._discovered_device = device + await self._async_set_discovered_mac(device, True) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index a522d133827..34e8b4b0667 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -19,6 +19,7 @@ from flux_led.const import ( ATTR_REMOTE_ACCESS_PORT, ATTR_VERSION_NUM, ) +from flux_led.models_db import get_model_description from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries @@ -42,7 +43,7 @@ from .const import ( DOMAIN, FLUX_LED_DISCOVERY, ) -from .util import format_as_flux_mac +from .util import format_as_flux_mac, mac_matches_by_one _LOGGER = logging.getLogger(__name__) @@ -80,13 +81,17 @@ def async_build_cached_discovery(entry: ConfigEntry) -> FluxLEDDiscovery: @callback -def async_name_from_discovery(device: FluxLEDDiscovery) -> str: +def async_name_from_discovery( + device: FluxLEDDiscovery, model_num: int | None = None +) -> str: """Convert a flux_led discovery to a human readable name.""" if (mac_address := device[ATTR_ID]) is None: return device[ATTR_IPADDR] short_mac = mac_address[-6:] if device[ATTR_MODEL_DESCRIPTION]: return f"{device[ATTR_MODEL_DESCRIPTION]} {short_mac}" + if model_num is not None: + return f"{get_model_description(model_num, None)} {short_mac}" return f"{device[ATTR_MODEL]} {short_mac}" @@ -113,19 +118,25 @@ def async_update_entry_from_discovery( entry: config_entries.ConfigEntry, device: FluxLEDDiscovery, model_num: int | None, + allow_update_mac: bool, ) -> bool: """Update a config entry from a flux_led discovery.""" data_updates: dict[str, Any] = {} mac_address = device[ATTR_ID] assert mac_address is not None updates: dict[str, Any] = {} - if not entry.unique_id: - updates["unique_id"] = dr.format_mac(mac_address) + formatted_mac = dr.format_mac(mac_address) + if not entry.unique_id or ( + allow_update_mac + and entry.unique_id != formatted_mac + and mac_matches_by_one(formatted_mac, entry.unique_id) + ): + updates["unique_id"] = formatted_mac if model_num and entry.data.get(CONF_MODEL_NUM) != model_num: data_updates[CONF_MODEL_NUM] = model_num async_populate_data_from_discovery(entry.data, data_updates, device) if is_ip_address(entry.title): - updates["title"] = async_name_from_discovery(device) + updates["title"] = async_name_from_discovery(device, model_num) title_matches_name = entry.title == entry.data.get(CONF_NAME) if data_updates or title_matches_name: updates["data"] = {**entry.data, **data_updates} diff --git a/homeassistant/components/flux_led/util.py b/homeassistant/components/flux_led/util.py index 70983433e18..9adbacb4273 100644 --- a/homeassistant/components/flux_led/util.py +++ b/homeassistant/components/flux_led/util.py @@ -28,6 +28,18 @@ def _human_readable_option(const_option: str) -> str: return const_option.replace("_", " ").title() +def mac_matches_by_one(formatted_mac_1: str, formatted_mac_2: str) -> bool: + """Check if a mac address is only one digit off. + + Some of the devices have two mac addresses which are + one off from each other. We need to treat them as the same + since its the same device. + """ + mac_int_1 = int(formatted_mac_1.replace(":", ""), 16) + mac_int_2 = int(formatted_mac_2.replace(":", ""), 16) + return abs(mac_int_1 - mac_int_2) < 2 + + def _flux_color_mode_to_hass( flux_color_mode: str | None, flux_color_modes: set[str] ) -> str: diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 65fb704e4c7..2f4e5d62dcc 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -39,6 +39,8 @@ MODEL_NUM = 0x35 MODEL = "AK001-ZJ2149" MODEL_DESCRIPTION = "Bulb RGBCW" MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +MAC_ADDRESS_ONE_OFF = "aa:bb:cc:dd:ee:fe" + FLUX_MAC_ADDRESS = "AABBCCDDEEFF" SHORT_MAC_ADDRESS = "DDEEFF" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index b858f6d995a..7c810688053 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -34,6 +34,7 @@ from . import ( FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, + MAC_ADDRESS_ONE_OFF, MODEL, MODEL_DESCRIPTION, MODEL_NUM, @@ -546,6 +547,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): CONF_MODEL_NUM: MODEL_NUM, CONF_MODEL_DESCRIPTION: MODEL_DESCRIPTION, } + assert result2["title"] == "Bulb RGBCW DDEEFF" assert mock_async_setup.called assert mock_async_setup_entry.called @@ -589,6 +591,46 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( assert config_entry.unique_id == MAC_ADDRESS +async def test_mac_address_off_by_one_updated_via_discovery(hass): + """Test the mac address is updated when its off by one from integration discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_ONE_OFF + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=FLUX_DISCOVERY, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS + + +async def test_mac_address_off_by_one_not_updated_from_dhcp(hass): + """Test the mac address is NOT updated when its off by one from dhcp discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS_ONE_OFF + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF + + @pytest.mark.parametrize( "source, data", [ diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 489f6c932c2..89c5aee73d9 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -26,6 +26,7 @@ from . import ( FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, MAC_ADDRESS, + MAC_ADDRESS_ONE_OFF, _mocked_bulb, _patch_discovery, _patch_wifibulb, @@ -82,7 +83,9 @@ async def test_configuring_flux_led_causes_discovery_multiple_addresses( async def test_config_entry_reload(hass: HomeAssistant) -> None: """Test that a config entry can be reloaded.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + ) config_entry.add_to_hass(hass) with _patch_discovery(), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) @@ -126,11 +129,11 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( # Only return discovery results when doing directed discovery nonlocal last_address last_address = address - return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + return [discovery] if address == IP_ADDRESS else [] def _mock_getBulbInfo(*args, **kwargs): nonlocal last_address - return [FLUX_DISCOVERY] if last_address == IP_ADDRESS else [] + return [discovery] if last_address == IP_ADDRESS else [] with patch( "homeassistant.components.flux_led.discovery.AIOBulbScanner.async_scan", @@ -149,7 +152,9 @@ async def test_config_entry_fills_unique_id_with_directed_discovery( async def test_time_sync_startup_and_next_day(hass: HomeAssistant) -> None: """Test that time is synced on startup and next day.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() with _patch_discovery(), _patch_wifibulb(device=bulb): @@ -204,3 +209,72 @@ async def test_unique_id_migrate_when_mac_discovered(hass: HomeAssistant) -> Non entity_registry.async_get("switch.bulb_rgbcw_ddeeff_remote_access").unique_id == f"{config_entry.unique_id}_remote_access" ) + + +async def test_unique_id_migrate_when_mac_discovered_via_discovery( + hass: HomeAssistant, +) -> None: + """Test unique id migrated when mac discovered via discovery and the mac address from dhcp was one off.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + unique_id=MAC_ADDRESS_ONE_OFF, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get("light.bulb_rgbcw_ddeeff").unique_id + == MAC_ADDRESS_ONE_OFF + ) + assert ( + entity_registry.async_get("switch.bulb_rgbcw_ddeeff_remote_access").unique_id + == f"{MAC_ADDRESS_ONE_OFF}_remote_access" + ) + + for _ in range(2): + with _patch_discovery(), _patch_wifibulb(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get("light.bulb_rgbcw_ddeeff").unique_id + == config_entry.unique_id + ) + assert ( + entity_registry.async_get( + "switch.bulb_rgbcw_ddeeff_remote_access" + ).unique_id + == f"{config_entry.unique_id}_remote_access" + ) + + +async def test_name_removed_when_it_matches_entry_title(hass: HomeAssistant) -> None: + """Test name is removed when it matches the entry title.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + }, + title=DEFAULT_ENTRY_TITLE, + ) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + assert CONF_NAME not in config_entry.data From 314175135fbab619d3c542b2d1022a9f150e8d88 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Mar 2022 19:39:07 +0100 Subject: [PATCH 0438/1054] Hide switch_as_x tracked entity (#67949) * Hide switch_as_x tracked entity * Hide wrapped switch during config flow * Allow setting/getting entity disabled by via WS * Adjust tests * Improve test coverage * Improve tests --- .../components/config/entity_registry.py | 13 ++- .../components/switch_as_x/__init__.py | 17 ++++ .../components/switch_as_x/config_flow.py | 16 +++- homeassistant/helpers/entity_registry.py | 28 ++++++- .../components/config/test_entity_registry.py | 38 ++++++++- .../switch_as_x/test_config_flow.py | 61 ++++++++++++++ tests/components/switch_as_x/test_init.py | 80 +++++++++++++++++++ 7 files changed, 244 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index f5ffc574b86..719e028cab1 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -78,6 +78,14 @@ def websocket_get_entity(hass, connection, msg): er.RegistryEntryDisabler.USER.value, ), ), + # We only allow setting hidden_by user via API. + vol.Optional("hidden_by"): vol.Any( + None, + vol.All( + vol.Coerce(er.RegistryEntryHider), + er.RegistryEntryHider.USER.value, + ), + ), } ) @callback @@ -96,7 +104,7 @@ def websocket_update_entity(hass, connection, msg): changes = {} - for key in ("area_id", "device_class", "disabled_by", "icon", "name"): + for key in ("area_id", "device_class", "disabled_by", "hidden_by", "icon", "name"): if key in msg: changes[key] = msg[key] @@ -113,6 +121,7 @@ def websocket_update_entity(hass, connection, msg): return if "disabled_by" in msg and msg["disabled_by"] is None: + # Don't allow enabling an entity of a disabled device entity = registry.entities[msg["entity_id"]] if entity.device_id: device_registry = dr.async_get(hass) @@ -135,6 +144,7 @@ def websocket_update_entity(hass, connection, msg): return result = {"entity_entry": _entry_ext_dict(entry)} if "disabled_by" in changes and changes["disabled_by"] is None: + # Enabling an entity requires a config entry reload, or HA restart config_entry = hass.config_entries.async_get_entry(entry.config_entry_id) if config_entry and not config_entry.supports_unload: result["require_restart"] = True @@ -178,6 +188,7 @@ def _entry_dict(entry): "disabled_by": entry.disabled_by, "entity_category": entry.entity_category, "entity_id": entry.entity_id, + "hidden_by": entry.hidden_by, "icon": entry.icon, "name": entry.name, "platform": entry.platform, diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 1da70f8029f..1adeace7a96 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -101,3 +101,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms( entry, (entry.options[CONF_TARGET_DOMAIN],) ) + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Unload a config entry.""" + # Unhide the wrapped entry if registered + registry = er.async_get(hass) + try: + entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + except vol.Invalid: + # The source entity has been removed from the entity registry + return + + if not (entity_entry := registry.async_get(entity_id)): + return + + if entity_entry.hidden_by == er.RegistryEntryHider.INTEGRATION: + registry.async_update_entity(entity_id, hidden_by=None) diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 5c3c68e9353..800e056cd26 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -7,7 +7,11 @@ from typing import Any import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID, Platform -from homeassistant.helpers import helper_config_entry_flow, selector +from homeassistant.helpers import ( + entity_registry as er, + helper_config_entry_flow, + selector, +) from .const import CONF_TARGET_DOMAIN, DOMAIN @@ -45,7 +49,15 @@ class SwitchAsXConfigFlowHandler( config_flow = CONFIG_FLOW def async_config_entry_title(self, options: Mapping[str, Any]) -> str: - """Return config entry title.""" + """Return config entry title and hide the wrapped entity if registered.""" + # Hide the wrapped entry if registered + registry = er.async_get(self.hass) + entity_entry = registry.async_get(options[CONF_ENTITY_ID]) + if entity_entry is not None and not entity_entry.hidden: + registry.async_update_entity( + options[CONF_ENTITY_ID], hidden_by=er.RegistryEntryHider.INTEGRATION + ) + return helper_config_entry_flow.wrapped_entity_config_entry_title( self.hass, options[CONF_ENTITY_ID] ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7c86bfaa501..5f91911044e 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -59,7 +59,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 5 +STORAGE_VERSION_MINOR = 6 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -85,6 +85,13 @@ class RegistryEntryDisabler(StrEnum): USER = "user" +class RegistryEntryHider(StrEnum): + """What hid a registry entry.""" + + INTEGRATION = "integration" + USER = "user" + + # DISABLED_* are deprecated, to be removed in 2022.3 DISABLED_CONFIG_ENTRY = RegistryEntryDisabler.CONFIG_ENTRY.value DISABLED_DEVICE = RegistryEntryDisabler.DEVICE.value @@ -120,6 +127,7 @@ class RegistryEntry: entity_category: EntityCategory | None = attr.ib( default=None, converter=_convert_to_entity_category ) + hidden_by: RegistryEntryHider | None = attr.ib(default=None) icon: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) name: str | None = attr.ib(default=None) @@ -143,6 +151,11 @@ class RegistryEntry: """Return if entry is disabled.""" return self.disabled_by is not None + @property + def hidden(self) -> bool: + """Return if entry is hidden.""" + return self.hidden_by is not None + @callback def write_unavailable_state(self, hass: HomeAssistant) -> None: """Write the unavailable state to the state machine.""" @@ -327,8 +340,9 @@ class EntityRegistry: # To influence entity ID generation known_object_ids: Iterable[str] | None = None, suggested_object_id: str | None = None, - # To disable an entity if it gets created + # To disable or hide an entity if it gets created disabled_by: RegistryEntryDisabler | None = None, + hidden_by: RegistryEntryHider | None = None, # Data that we want entry to have area_id: str | None = None, capabilities: Mapping[str, Any] | None = None, @@ -400,6 +414,7 @@ class EntityRegistry: disabled_by=disabled_by, entity_category=_convert_to_entity_category(entity_category), entity_id=entity_id, + hidden_by=hidden_by, original_device_class=original_device_class, original_icon=original_icon, original_name=original_name, @@ -505,6 +520,7 @@ class EntityRegistry: disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED, # Type str (ENTITY_CATEG*) is deprecated as of 2021.12, use EntityCategory entity_category: EntityCategory | str | None | UndefinedType = UNDEFINED, + hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, @@ -540,6 +556,7 @@ class EntityRegistry: ("device_id", device_id), ("disabled_by", disabled_by), ("entity_category", entity_category), + ("hidden_by", hidden_by), ("icon", icon), ("name", name), ("original_device_class", original_device_class), @@ -651,6 +668,7 @@ class EntityRegistry: entity["entity_category"], raise_report=False ), entity_id=entity["entity_id"], + hidden_by=entity["hidden_by"], icon=entity["icon"], id=entity["id"], name=entity["name"], @@ -686,6 +704,7 @@ class EntityRegistry: "disabled_by": entry.disabled_by, "entity_category": entry.entity_category, "entity_id": entry.entity_id, + "hidden_by": entry.hidden_by, "icon": entry.icon, "id": entry.id, "name": entry.name, @@ -846,6 +865,11 @@ async def _async_migrate( for entity in data["entities"]: entity["options"] = {} + if old_major_version == 1 and old_minor_version < 6: + # Version 1.6 adds hidden_by + for entity in data["entities"]: + entity["hidden_by"] = None + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index b4065d855ff..fae4c1bbc6f 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -4,7 +4,11 @@ import pytest from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler -from homeassistant.helpers.entity_registry import RegistryEntry, RegistryEntryDisabler +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + RegistryEntryDisabler, + RegistryEntryHider, +) from tests.common import ( MockConfigEntry, @@ -57,6 +61,7 @@ async def test_list_entities(hass, client): "area_id": None, "disabled_by": None, "entity_id": "test_domain.name", + "hidden_by": None, "name": "Hello World", "icon": None, "platform": "test_platform", @@ -68,6 +73,7 @@ async def test_list_entities(hass, client): "area_id": None, "disabled_by": None, "entity_id": "test_domain.no_name", + "hidden_by": None, "name": None, "icon": None, "platform": "test_platform", @@ -109,6 +115,7 @@ async def test_get_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.name", + "hidden_by": None, "icon": None, "name": "Hello World", "original_device_class": None, @@ -136,6 +143,7 @@ async def test_get_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.no_name", + "hidden_by": None, "icon": None, "name": None, "original_device_class": None, @@ -170,7 +178,7 @@ async def test_update_entity(hass, client): assert state.name == "before update" assert state.attributes[ATTR_ICON] == "icon:before update" - # UPDATE AREA, DEVICE_CLASS, ICON AND NAME + # UPDATE AREA, DEVICE_CLASS, HIDDEN_BY, ICON AND NAME await client.send_json( { "id": 6, @@ -178,6 +186,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "area_id": "mock-area-id", "device_class": "custom_device_class", + "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "name": "after update", } @@ -195,6 +204,7 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "name": "after update", "original_device_class": None, @@ -209,17 +219,33 @@ async def test_update_entity(hass, client): assert state.name == "after update" assert state.attributes[ATTR_ICON] == "icon:after update" - # UPDATE DISABLED_BY TO USER + # UPDATE HIDDEN_BY TO ILLEGAL VALUE await client.send_json( { "id": 7, "type": "config/entity_registry/update", "entity_id": "test_domain.world", + "hidden_by": "ivy", + } + ) + + msg = await client.receive_json() + assert not msg["success"] + + assert registry.entities["test_domain.world"].hidden_by is RegistryEntryHider.USER + + # UPDATE DISABLED_BY TO USER + await client.send_json( + { + "id": 8, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", "disabled_by": RegistryEntryDisabler.USER, } ) msg = await client.receive_json() + assert msg["success"] assert hass.states.get("test_domain.world") is None assert ( @@ -229,7 +255,7 @@ async def test_update_entity(hass, client): # UPDATE DISABLED_BY TO NONE await client.send_json( { - "id": 8, + "id": 9, "type": "config/entity_registry/update", "entity_id": "test_domain.world", "disabled_by": None, @@ -248,6 +274,7 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "name": "after update", "original_device_class": None, @@ -306,6 +333,7 @@ async def test_update_entity_require_restart(hass, client): "entity_category": None, "entity_id": "test_domain.world", "icon": None, + "hidden_by": None, "name": None, "original_device_class": None, "original_icon": None, @@ -409,6 +437,7 @@ async def test_update_entity_no_changes(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "hidden_by": None, "icon": None, "name": "name of entity", "original_device_class": None, @@ -492,6 +521,7 @@ async def test_update_entity_id(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.planet", + "hidden_by": None, "icon": None, "name": None, "original_device_class": None, diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index b2a05faa998..6859dda9073 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Switch as X config flow.""" +from __future__ import annotations + from unittest.mock import AsyncMock import pytest @@ -8,6 +10,7 @@ from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAI from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.helpers import entity_registry as er @pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) @@ -49,6 +52,64 @@ async def test_config_flow( } +@pytest.mark.parametrize( + "hidden_by_before,hidden_by_after", + ( + (er.RegistryEntryHider.USER.value, er.RegistryEntryHider.USER.value), + (None, er.RegistryEntryHider.INTEGRATION.value), + ), +) +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_config_flow_registered_entity( + hass: HomeAssistant, + target_domain: Platform, + mock_setup_entry: AsyncMock, + hidden_by_before: er.RegistryEntryHider | None, + hidden_by_after: er.RegistryEntryHider, +) -> None: + """Test the config flow hides a registered entity.""" + registry = er.async_get(hass) + switch_entity_entry = registry.async_get_or_create( + "switch", "test", "unique", suggested_object_id="ceiling" + ) + assert switch_entity_entry.entity_id == "switch.ceiling" + registry.async_update_entity("switch.ceiling", hidden_by=hidden_by_before) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ceiling" + assert result["data"] == {} + assert result["options"] == { + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + CONF_ENTITY_ID: "switch.ceiling", + CONF_TARGET_DOMAIN: target_domain, + } + + switch_entity_entry = registry.async_get("switch.ceiling") + assert switch_entity_entry.hidden_by == hidden_by_after + + @pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) async def test_options( hass: HomeAssistant, diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index b36ce64e593..f5c3e7c5653 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -1,4 +1,6 @@ """Tests for the Switch as X.""" +from __future__ import annotations + from unittest.mock import patch import pytest @@ -356,3 +358,81 @@ async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: entity_entry = entity_registry.async_get(f"{target_domain}.abc") assert entity_entry assert entity_entry.device_id == switch_entity_entry.device_id + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + target_domain: Platform, +) -> None: + """Test removing a config entry.""" + registry = er.async_get(hass) + + # Setup the config entry + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + switch_as_x_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are present + assert hass.states.get(f"{target_domain}.abc") is not None + assert registry.async_get(f"{target_domain}.abc") is not None + + # Remove the config entry + assert await hass.config_entries.async_remove(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(f"{target_domain}.my_min_max") is None + assert registry.async_get(f"{target_domain}.my_min_max") is None + + +@pytest.mark.parametrize( + "hidden_by_before,hidden_by_after", + ( + (er.RegistryEntryHider.USER.value, er.RegistryEntryHider.USER.value), + (er.RegistryEntryHider.INTEGRATION.value, None), + ), +) +@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +async def test_reset_hidden_by( + hass: HomeAssistant, + target_domain: Platform, + hidden_by_before: er.RegistryEntryHider | None, + hidden_by_after: er.RegistryEntryHider, +) -> None: + """Test removing a config entry resets hidden by.""" + registry = er.async_get(hass) + + switch_entity_entry = registry.async_get_or_create("switch", "test", "unique") + registry.async_update_entity( + switch_entity_entry.entity_id, hidden_by=hidden_by_before + ) + + # Add the config entry + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + switch_as_x_config_entry.add_to_hass(hass) + + # Remove the config entry + assert await hass.config_entries.async_remove(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + # Check hidden by is reset + switch_entity_entry = registry.async_get(switch_entity_entry.entity_id) + assert switch_entity_entry.hidden_by == hidden_by_after From a109889f13a201537227e3a4d16cd3cb39e43840 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Mar 2022 12:14:21 -0700 Subject: [PATCH 0439/1054] Add media source support to openhome (#67566) --- .../components/openhome/media_player.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 1bda8b077a9..21c3c465775 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -10,9 +10,14 @@ from async_upnp_client.client import UpnpError from openhomedevice.device import Device import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -149,7 +154,10 @@ class OpenhomeDevice(MediaPlayerEntity): if self._source["type"] == "Radio": self._supported_features |= ( - SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + SUPPORT_STOP + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) if self._source["type"] in ("Playlist", "Spotify"): self._supported_features |= ( @@ -158,6 +166,7 @@ class OpenhomeDevice(MediaPlayerEntity): | SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) if self._in_standby: @@ -189,6 +198,11 @@ class OpenhomeDevice(MediaPlayerEntity): @catch_request_errors() async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + if media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", @@ -196,6 +210,9 @@ class OpenhomeDevice(MediaPlayerEntity): MEDIA_TYPE_MUSIC, ) return + + media_id = async_process_play_media_url(self.hass, media_id) + track_details = {"title": "Home Assistant", "uri": media_id} await self._device.play_media(track_details) @@ -320,3 +337,11 @@ class OpenhomeDevice(MediaPlayerEntity): async def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" await self._device.set_mute(mute) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From 86abb85efa605a1b7dc5660f7a6dc0ee33e15860 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 20:17:36 +0100 Subject: [PATCH 0440/1054] Update pytest-xdist to 2.5.0 (#68135) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 944b8dbed00..9e16676092a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -23,7 +23,7 @@ pytest-socket==0.4.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==2.1.0 -pytest-xdist==2.4.0 +pytest-xdist==2.5.0 pytest==7.1.0 requests_mock==1.9.2 respx==0.19.0 From 7fc0ffd5c591429cef805aca707acdda0ca304e6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Mar 2022 20:28:55 +0100 Subject: [PATCH 0441/1054] Restore state of trigger based template binary sensor (#67538) --- .../components/template/binary_sensor.py | 111 +++++++++-- .../components/template/test_binary_sensor.py | 175 +++++++++++++++++- 2 files changed, 273 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index f416454f388..53267b29341 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,7 +1,8 @@ """Support for exposing a templated binary sensor.""" from __future__ import annotations -from datetime import timedelta +from dataclasses import dataclass +from datetime import datetime, timedelta from functools import partial import logging from typing import Any @@ -40,9 +41,10 @@ from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.event import async_call_later, async_track_point_in_utc_time +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import dt as dt_util from . import TriggerUpdateCoordinator from .const import ( @@ -291,7 +293,7 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): return self._device_class -class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): +class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity): """Sensor entity based on trigger data.""" domain = BINARY_SENSOR_DOMAIN @@ -312,9 +314,36 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): self._parse_result.add(key) self._delay_cancel: CALLBACK_TYPE | None = None - self._auto_off_cancel = None + self._auto_off_cancel: CALLBACK_TYPE | None = None + self._auto_off_time: datetime | None = None self._state: bool | None = None + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + (last_state := await self.async_get_last_state()) is not None + and (extra_data := await self.async_get_last_binary_sensor_data()) + is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + # The trigger might have fired already while we waited for stored data, + # then we should not restore state + and self._state is None + ): + self._state = last_state.state == STATE_ON + + if CONF_AUTO_OFF not in self._config: + return + + if ( + auto_off_time := extra_data.auto_off_time + ) is not None and auto_off_time <= dt_util.utcnow(): + # It's already past the saved auto off time + self._state = False + + if self._state and auto_off_time is not None: + self._set_auto_off(auto_off_time) + @property def is_on(self) -> bool | None: """Return state of the sensor.""" @@ -332,6 +361,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): if self._auto_off_cancel: self._auto_off_cancel() self._auto_off_cancel = None + self._auto_off_time = None if not self.available: self.async_write_ha_state() @@ -372,28 +402,85 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity): if not state: return - auto_off_time = self._rendered.get(CONF_AUTO_OFF) or self._config.get( + auto_off_delay = self._rendered.get(CONF_AUTO_OFF) or self._config.get( CONF_AUTO_OFF ) - if auto_off_time is None: + if auto_off_delay is None: return - if not isinstance(auto_off_time, timedelta): + if not isinstance(auto_off_delay, timedelta): try: - auto_off_time = cv.positive_time_period(auto_off_time) + auto_off_delay = cv.positive_time_period(auto_off_delay) except vol.Invalid as err: logging.getLogger(__name__).warning( "Error rendering %s template: %s", CONF_AUTO_OFF, err ) return + auto_off_time = dt_util.utcnow() + auto_off_delay + self._set_auto_off(auto_off_time) + + def _set_auto_off(self, auto_off_time: datetime) -> None: @callback def _auto_off(_): - """Set state of template binary sensor.""" + """Reset state of template binary sensor.""" self._state = False self.async_write_ha_state() - self._auto_off_cancel = async_call_later( - self.hass, auto_off_time.total_seconds(), _auto_off + self._auto_off_time = auto_off_time + self._auto_off_cancel = async_track_point_in_utc_time( + self.hass, _auto_off, self._auto_off_time ) + + @property + def extra_restore_state_data(self) -> AutoOffExtraStoredData: + """Return specific state data to be restored.""" + return AutoOffExtraStoredData(self._auto_off_time) + + async def async_get_last_binary_sensor_data( + self, + ) -> AutoOffExtraStoredData | None: + """Restore auto_off_time.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + return AutoOffExtraStoredData.from_dict(restored_last_extra_data.as_dict()) + + +@dataclass +class AutoOffExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + auto_off_time: datetime | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of additional data.""" + auto_off_time: datetime | None | dict[str, str] = self.auto_off_time + if isinstance(auto_off_time, datetime): + auto_off_time = { + "__type": str(type(auto_off_time)), + "isoformat": auto_off_time.isoformat(), + } + return { + "auto_off_time": auto_off_time, + } + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> AutoOffExtraStoredData | None: + """Initialize a stored binary sensor state from a dict.""" + try: + auto_off_time = restored["auto_off_time"] + except KeyError: + return None + try: + type_ = auto_off_time["__type"] + if type_ == "": + auto_off_time = dt_util.parse_datetime(auto_off_time["isoformat"]) + except TypeError: + # native_value is not a dict + pass + except KeyError: + # native_value is a dict, but does not have all values + return None + + return cls(auto_off_time) diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c3773e8eb13..1d8d9473f3c 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,5 +1,5 @@ """The tests for the Template Binary sensor platform.""" -from datetime import timedelta +from datetime import datetime, timedelta, timezone import logging from unittest.mock import patch @@ -24,6 +24,7 @@ from tests.common import ( assert_setup_component, async_fire_time_changed, mock_restore_cache, + mock_restore_cache_with_extra_data, ) ON = "on" @@ -1112,6 +1113,11 @@ async def test_template_with_trigger_templated_delay_on(hass, start_ha): hass.bus.async_fire("test_event", {"beer": 2}, context=context) await hass.async_block_till_done() + # State should still be unknown + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNKNOWN + + # Now wait for the on delay future = dt_util.utcnow() + timedelta(seconds=3) async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -1126,3 +1132,170 @@ async def test_template_with_trigger_templated_delay_on(hass, start_ha): state = hass.states.get("binary_sensor.test") assert state.state == OFF + + +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer == 2 }}", + "device_class": "motion", + }, + }, + }, + ], +) +@pytest.mark.parametrize( + "restored_state, initial_state", + [ + (ON, ON), + (OFF, OFF), + (STATE_UNAVAILABLE, STATE_UNKNOWN), + (STATE_UNKNOWN, STATE_UNKNOWN), + ], +) +async def test_trigger_entity_restore_state( + hass, count, domain, config, restored_state, initial_state +): + """Test restoring trigger template binary sensor.""" + + fake_state = State( + "binary_sensor.test", + restored_state, + {}, + ) + fake_extra_data = { + "auto_off_time": None, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == initial_state + + +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer == 2 }}", + "device_class": "motion", + "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', + }, + }, + }, + ], +) +@pytest.mark.parametrize("restored_state", [ON, OFF]) +async def test_trigger_entity_restore_state_auto_off( + hass, count, domain, config, restored_state, freezer +): + """Test restoring trigger template binary sensor.""" + + freezer.move_to("2022-02-02 12:02:00+00:00") + fake_state = State( + "binary_sensor.test", + restored_state, + {}, + ) + fake_extra_data = { + "auto_off_time": { + "__type": "", + "isoformat": datetime( + 2022, 2, 2, 12, 2, 2, tzinfo=timezone.utc + ).isoformat(), + }, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == restored_state + + # Now wait for the auto-off + freezer.move_to("2022-02-02 12:02:03+00:00") + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == OFF + + +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "binary_sensor": { + "name": "test", + "state": "{{ trigger.event.data.beer == 2 }}", + "device_class": "motion", + "auto_off": '{{ ({ "seconds": 1 + 1 }) }}', + }, + }, + }, + ], +) +async def test_trigger_entity_restore_state_auto_off_expired( + hass, count, domain, config, freezer +): + """Test restoring trigger template binary sensor.""" + + freezer.move_to("2022-02-02 12:02:00+00:00") + fake_state = State( + "binary_sensor.test", + ON, + {}, + ) + fake_extra_data = { + "auto_off_time": { + "__type": "", + "isoformat": datetime( + 2022, 2, 2, 12, 2, 0, tzinfo=timezone.utc + ).isoformat(), + }, + } + mock_restore_cache_with_extra_data(hass, ((fake_state, fake_extra_data),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.test") + assert state.state == OFF From a9fd744247443e3d74abbae83aa63fdb64de0bfc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Mar 2022 13:16:22 -0700 Subject: [PATCH 0442/1054] Add media source support to unifiprotect (#67570) --- .../components/unifiprotect/media_player.py | 30 ++++++++++-- .../unifiprotect/test_media_player.py | 46 ++++++++++++++++--- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index a6ed8f65681..4de59c8252a 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -7,13 +7,19 @@ from typing import Any from pyunifiprotect.data import Camera from pyunifiprotect.exceptions import StreamError +from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityDescription, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_SET, @@ -74,7 +80,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self._attr_name = f"{self.device.name} Speaker" self._attr_supported_features = ( - SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | SUPPORT_STOP + SUPPORT_PLAY_MEDIA + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_STOP + | SUPPORT_BROWSE_MEDIA ) self._attr_media_content_type = MEDIA_TYPE_MUSIC @@ -112,16 +122,20 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + media_type = MEDIA_TYPE_MUSIC + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = async_process_play_media_url(self.hass, play_item.url) if media_type != MEDIA_TYPE_MUSIC: - raise ValueError("Only music media type is supported") + raise HomeAssistantError("Only music media type is supported") _LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name) await self.async_media_stop() try: await self.device.play_audio(media_id, blocking=False) except StreamError as err: - raise HomeAssistantError from err + raise HomeAssistantError(err) from err else: # update state after starting player self._async_updated_event() @@ -129,3 +143,13 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): await self.device.wait_until_audio_completes() self._async_updated_event() + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 4d83da3fabb..c4586eb7880 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations from copy import copy -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest from pyunifiprotect.data import Camera @@ -80,7 +80,7 @@ async def test_media_player_setup( assert state assert state.state == STATE_IDLE assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 5636 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 136708 assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == "music" assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == expected_volume @@ -166,7 +166,6 @@ async def test_media_player_play( camera: tuple[Camera, str], ): """Test media_player entity test play_media.""" - camera[0].__fields__["stop_audio"] = Mock() camera[0].__fields__["play_audio"] = Mock() camera[0].__fields__["wait_until_audio_completes"] = Mock() @@ -179,13 +178,48 @@ async def test_media_player_play( "play_media", { ATTR_ENTITY_ID: camera[1], - "media_content_id": "/test.mp3", + "media_content_id": "http://example.com/test.mp3", "media_content_type": "music", }, blocking=True, ) - camera[0].play_audio.assert_called_once_with("/test.mp3", blocking=False) + camera[0].play_audio.assert_called_once_with( + "http://example.com/test.mp3", blocking=False + ) + camera[0].wait_until_audio_completes.assert_called_once() + + +async def test_media_player_play_media_source( + hass: HomeAssistant, + camera: tuple[Camera, str], +): + """Test media_player entity test play_media.""" + camera[0].__fields__["stop_audio"] = Mock() + camera[0].__fields__["play_audio"] = Mock() + camera[0].__fields__["wait_until_audio_completes"] = Mock() + camera[0].stop_audio = AsyncMock() + camera[0].play_audio = AsyncMock() + camera[0].wait_until_audio_completes = AsyncMock() + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=Mock(url="http://example.com/test.mp3"), + ): + await hass.services.async_call( + "media_player", + "play_media", + { + ATTR_ENTITY_ID: camera[1], + "media_content_id": "media-source://some_source/some_id", + "media_content_type": "audio/mpeg", + }, + blocking=True, + ) + + camera[0].play_audio.assert_called_once_with( + "http://example.com/test.mp3", blocking=False + ) camera[0].wait_until_audio_completes.assert_called_once() @@ -198,7 +232,7 @@ async def test_media_player_play_invalid( camera[0].__fields__["play_audio"] = Mock() camera[0].play_audio = AsyncMock() - with pytest.raises(ValueError): + with pytest.raises(HomeAssistantError): await hass.services.async_call( "media_player", "play_media", From 59c4c7591529a2b23938b05e99987661720341df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Mar 2022 11:52:27 -1000 Subject: [PATCH 0443/1054] Ensure WiZ is reloaded on title change (#68147) --- homeassistant/components/wiz/__init__.py | 7 ++++++ tests/components/wiz/test_init.py | 30 +++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index ef6ffeb58b5..104ecb6f0c5 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -54,6 +54,11 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the wiz integration from a config entry.""" ip_address = entry.data[CONF_HOST] @@ -123,6 +128,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator=coordinator, bulb=bulb, scenes=scenes ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index 58afb5c944a..30340f78e49 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -3,7 +3,7 @@ import datetime from unittest.mock import AsyncMock from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_FRIENDLY_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -24,20 +24,20 @@ async def test_setup_retry(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.getMac = AsyncMock(side_effect=OSError) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_discovery(), _patch_wizlight(device=bulb): async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) await hass.async_block_till_done() - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED async def test_cleanup_on_shutdown(hass: HomeAssistant) -> None: """Test the socket is cleaned up on shutdown.""" bulb = _mocked_wizlight(None, None, FAKE_SOCKET) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() bulb.async_close.assert_called_once() @@ -48,7 +48,7 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.updateState = AsyncMock(side_effect=OSError) _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY bulb.async_close.assert_called_once() @@ -57,4 +57,22 @@ async def test_wrong_device_now_has_our_ip(hass: HomeAssistant) -> None: bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.mac = "dddddddddddd" _, entry = await async_setup_integration(hass, wizlight=bulb) - assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + + +async def test_reload_on_title_change(hass: HomeAssistant) -> None: + """Test the integration gets reloaded when the title is updated.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state is config_entries.ConfigEntryState.LOADED + await hass.async_block_till_done() + + with _patch_discovery(), _patch_wizlight(device=bulb): + hass.config_entries.async_update_entry(entry, title="Shop Switch") + assert entry.title == "Shop Switch" + await hass.async_block_till_done() + + assert ( + hass.states.get("switch.mock_title").attributes[ATTR_FRIENDLY_NAME] + == "Shop Switch" + ) From 9d6e1ab0e5d8776bf14f500e4637b037c27b8738 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Mar 2022 11:55:52 -1000 Subject: [PATCH 0444/1054] Ensure flux_led is reloaded when the title changes (#68146) --- homeassistant/components/flux_led/__init__.py | 11 ++++++ .../components/flux_led/coordinator.py | 1 + .../components/flux_led/discovery.py | 6 ++-- tests/components/flux_led/test_init.py | 34 ++++++++++++++++++- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 56f0f371d60..17dc28a5edf 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -122,6 +122,13 @@ async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + if entry.title != coordinator.title: + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flux LED/MagicLight from a config entry.""" host = entry.data[CONF_HOST] @@ -185,6 +192,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_sync_time() # set at startup entry.async_on_unload(async_track_time_change(hass, _async_sync_time, 2, 40, 30)) + # There must not be any awaits between here and the return + # to avoid a race condition where the add_update_listener is not + # in place in time for the check in async_update_entry_from_discovery + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index 0a285518c66..5f2c3c097c0 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -28,6 +28,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): ) -> None: """Initialize DataUpdateCoordinator to gather data for specific device.""" self.device = device + self.title = entry.title self.entry = entry super().__init__( hass, diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 34e8b4b0667..70ab8cb3fa5 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -24,7 +24,7 @@ from flux_led.scanner import FluxLEDDiscovery from homeassistant import config_entries from homeassistant.components import network -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -142,7 +142,9 @@ def async_update_entry_from_discovery( updates["data"] = {**entry.data, **data_updates} if title_matches_name: del updates["data"][CONF_NAME] - if updates: + # If the title has changed and the config entry is loaded, a listener is + # in place, and we should not reload + if updates and not ("title" in updates and entry.state is ConfigEntryState.LOADED): return hass.config_entries.async_update_entry(entry, **updates) return False diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 89c5aee73d9..b0a2c5dd33b 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -14,7 +14,12 @@ from homeassistant.components.flux_led.const import ( DOMAIN, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_HOST, + CONF_NAME, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -278,3 +283,30 @@ async def test_name_removed_when_it_matches_entry_title(hass: HomeAssistant) -> await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() assert CONF_NAME not in config_entry.data + + +async def test_entry_is_reloaded_when_title_changes(hass: HomeAssistant) -> None: + """Test the entry gets reloaded when the title changes.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_REMOTE_ACCESS_HOST: "any", + CONF_REMOTE_ACCESS_ENABLED: True, + CONF_REMOTE_ACCESS_PORT: 1234, + CONF_HOST: IP_ADDRESS, + }, + title=DEFAULT_ENTRY_TITLE, + ) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + hass.config_entries.async_update_entry(config_entry, title="Shop Light") + assert config_entry.title == "Shop Light" + await hass.async_block_till_done() + + assert ( + hass.states.get("light.bulb_rgbcw_ddeeff").attributes[ATTR_FRIENDLY_NAME] + == "Shop Light" + ) From f134219c7419e1eb0c7a088015b80e064340651d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Mar 2022 23:07:52 +0100 Subject: [PATCH 0445/1054] Minor tweak of config entity_registry test (#68141) --- tests/components/config/test_entity_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index fae4c1bbc6f..19a07b3f8f7 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -240,7 +240,7 @@ async def test_update_entity(hass, client): "id": 8, "type": "config/entity_registry/update", "entity_id": "test_domain.world", - "disabled_by": RegistryEntryDisabler.USER, + "disabled_by": "user", # We exchange strings over the WS API, not enums } ) From 4988c4683cf8d38b8b6fed909c8d2faa7180d2cc Mon Sep 17 00:00:00 2001 From: Benjamin <46243805+bbr111@users.noreply.github.com> Date: Tue, 15 Mar 2022 00:33:08 +0100 Subject: [PATCH 0446/1054] Bump volkszahler to 0.3.2 (#67958) Co-authored-by: Franck Nijhof --- homeassistant/components/volkszaehler/manifest.json | 2 +- homeassistant/components/volkszaehler/sensor.py | 4 +--- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 286e18b0b17..5212593da45 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,7 +2,7 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.2.1"], + "requirements": ["volkszaehler==0.3.2"], "codeowners": ["@fabaff"], "iot_class": "local_polling", "loggers": ["volkszaehler"] diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 02a136cc430..8694d8186fe 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -96,9 +96,7 @@ async def async_setup_platform( conditions = config[CONF_MONITORED_CONDITIONS] session = async_get_clientsession(hass) - vz_api = VolkszaehlerData( - Volkszaehler(hass.loop, session, uuid, host=host, port=port) - ) + vz_api = VolkszaehlerData(Volkszaehler(session, uuid, host=host, port=port)) await vz_api.async_update() diff --git a/requirements_all.txt b/requirements_all.txt index e40750fd1a3..fc930536b83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2365,7 +2365,7 @@ venstarcolortouch==0.15 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.2.1 +volkszaehler==0.3.2 # homeassistant.components.volvooncall volvooncall==0.10.0 From 7876ffe9e392b20da16f0d0c44c723f526f807e6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 14 Mar 2022 23:51:02 -0700 Subject: [PATCH 0447/1054] Update google calendar integration with a config flow (#68010) * Convert google calendar to config flow and async * Call correct exchange method * Fix async method and reduce unnecessary diffs * Wording improvements * Reduce unnecessary diffs * Run load/update config from executor * Update homeassistant/components/google/calendar.py Co-authored-by: Martin Hjelmare * Remove unnecessary updating of unexpected multiple config entries. * Remove unnecessary unique_id checks * Improve readability with comments about device code expiration * Update homeassistant/components/google/calendar.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/google/calendar.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/google/api.py Co-authored-by: Martin Hjelmare * Add comment for when code is none on timeout Co-authored-by: Martin Hjelmare --- homeassistant/components/google/__init__.py | 261 +++++-------- homeassistant/components/google/api.py | 239 +++++++++--- homeassistant/components/google/calendar.py | 54 ++- .../components/google/config_flow.py | 128 +++++++ homeassistant/components/google/const.py | 30 ++ homeassistant/components/google/manifest.json | 2 + homeassistant/components/google/strings.json | 31 ++ .../components/google/translations/en.json | 31 ++ homeassistant/generated/config_flows.py | 1 + tests/components/google/conftest.py | 21 ++ tests/components/google/test_calendar.py | 26 +- tests/components/google/test_config_flow.py | 350 ++++++++++++++++++ tests/components/google/test_init.py | 258 +++---------- 13 files changed, 989 insertions(+), 443 deletions(-) create mode 100644 homeassistant/components/google/config_flow.py create mode 100644 homeassistant/components/google/const.py create mode 100644 homeassistant/components/google/strings.json create mode 100644 homeassistant/components/google/translations/en.json create mode 100644 tests/components/google/test_config_flow.py diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 87d76b9f4c8..f158db884dc 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,23 +1,20 @@ """Support for Google - Calendar Event Devices.""" from __future__ import annotations +import asyncio from collections.abc import Mapping -from datetime import datetime, timedelta, timezone -from enum import Enum +from datetime import datetime, timedelta import logging from typing import Any -from oauth2client.client import ( - FlowExchangeError, - OAuth2DeviceCodeError, - OAuth2WebServerFlow, -) +from httplib2.error import ServerNotFoundError from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml -from homeassistant.components import persistent_notification +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -25,20 +22,28 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_NAME, CONF_OFFSET, - Platform, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, ServiceCall -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError +from homeassistant.helpers import config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.typing import ConfigType -from .api import GoogleCalendarService +from . import config_flow +from .api import DeviceAuth, GoogleCalendarService +from .const import ( + CONF_CALENDAR_ACCESS, + DATA_CONFIG, + DATA_SERVICE, + DISCOVER_CALENDAR, + DOMAIN, + FeatureAccess, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = "google" ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_TRACK_NEW = "track_new_calendar" @@ -48,7 +53,6 @@ CONF_TRACK = "track" CONF_SEARCH = "search" CONF_IGNORE_AVAILABILITY = "ignore_availability" CONF_MAX_RESULTS = "max_results" -CONF_CALENDAR_ACCESS = "calendar_access" DEFAULT_CONF_OFFSET = "!!" @@ -74,27 +78,11 @@ SERVICE_SCAN_CALENDARS = "scan_for_calendars" SERVICE_FOUND_CALENDARS = "found_calendar" SERVICE_ADD_EVENT = "add_event" -DATA_SERVICE = "service" - YAML_DEVICES = f"{DOMAIN}_calendars.yaml" TOKEN_FILE = f".{DOMAIN}.token" - -class FeatureAccess(Enum): - """Class to represent different access scopes.""" - - read_only = "https://www.googleapis.com/auth/calendar.readonly" - read_write = "https://www.googleapis.com/auth/calendar" - - def __init__(self, scope: str) -> None: - """Init instance.""" - self._scope = scope - - @property - def scope(self) -> str: - """Google calendar scope for the feature.""" - return self._scope +PLATFORMS = ["calendar"] CONFIG_SCHEMA = vol.Schema( @@ -159,131 +147,79 @@ ADD_EVENT_SERVICE_SCHEMA = vol.Schema( ) -def do_authentication( - hass: HomeAssistant, - hass_config: ConfigType, - config: ConfigType, - storage: Storage, -) -> bool: - """Notify user of actions and authenticate. - - Notify user of user_code and verification_url then poll - until we have an access token. - """ - oauth = OAuth2WebServerFlow( - client_id=config[CONF_CLIENT_ID], - client_secret=config[CONF_CLIENT_SECRET], - scope=config[CONF_CALENDAR_ACCESS].scope, - redirect_uri="Home-Assistant.io", - ) - try: - dev_flow = oauth.step1_get_device_and_user_codes() - except OAuth2DeviceCodeError as err: - persistent_notification.create( - hass, - f"Error: {err}
You will need to restart hass after fixing." "", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - - persistent_notification.create( +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Google component.""" + conf = config.get(DOMAIN, {}) + hass.data[DOMAIN] = {DATA_CONFIG: conf} + config_flow.OAuth2FlowHandler.async_register_implementation( hass, - ( - f"In order to authorize Home-Assistant to view your calendars " - f'you must visit: {dev_flow.verification_url} and enter ' - f"code: {dev_flow.user_code}" - ), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - listener: CALLBACK_TYPE | None = None - - def step2_exchange(now: datetime) -> None: - """Keep trying to validate the user_code until it expires.""" - _LOGGER.debug("Attempting to validate user code") - - # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime - # object without tzinfo. For the comparison below to work, it needs one. - user_code_expiry = dev_flow.user_code_expiry.replace(tzinfo=timezone.utc) - - if now >= user_code_expiry: - persistent_notification.create( - hass, - "Authentication code expired, please restart " - "Home-Assistant and try again", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - assert listener - listener() - return - - try: - credentials = oauth.step2_exchange(device_flow_info=dev_flow) - except FlowExchangeError: - # not ready yet, call again - return - - storage.put(credentials) - do_setup(hass, hass_config, config) - assert listener - listener() - persistent_notification.create( + DeviceAuth( hass, - ( - f"We are all setup now. Check {YAML_DEVICES} for calendars that have " - f"been found" - ), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - listener = track_utc_time_change( - hass, step2_exchange, second=range(1, 60, dev_flow.interval) + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + ), ) - return True - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Google platform.""" - - if not (conf := config.get(DOMAIN, {})): - # component is set up by tts platform - return True - + # Import credentials from the old token file into the new way as + # a ConfigEntry managed by home assistant. storage = Storage(hass.config.path(TOKEN_FILE)) - hass.data[DOMAIN] = { - DATA_SERVICE: GoogleCalendarService(hass, storage), - } - creds = storage.get() - if ( - not creds - or not creds.scopes - or conf[CONF_CALENDAR_ACCESS].scope not in creds.scopes - ): - do_authentication(hass, config, conf, storage) - else: - do_setup(hass, config, conf) + creds = await hass.async_add_executor_job(storage.get) + if creds and conf[CONF_CALENDAR_ACCESS].scope in creds.scopes: + _LOGGER.debug("Importing configuration entry with credentials") + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "creds": creds, + }, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Google from a config entry.""" + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + assert isinstance(implementation, DeviceAuth) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope + if required_scope not in session.token.get("scope", []): + raise ConfigEntryAuthFailed( + "Required scopes are not available, reauth required" + ) + calendar_service = GoogleCalendarService(hass, session) + hass.data[DOMAIN][DATA_SERVICE] = calendar_service + + await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -def setup_services( +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_setup_services( hass: HomeAssistant, - hass_config: ConfigType, config: ConfigType, calendar_service: GoogleCalendarService, ) -> None: """Set up the service listeners.""" created_calendars = set() - calendars = load_config(hass.config.path(YAML_DEVICES)) + calendars = await hass.async_add_executor_job( + load_config, hass.config.path(YAML_DEVICES) + ) - def _found_calendar(call: ServiceCall) -> None: - """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" + async def _found_calendar(call: ServiceCall) -> None: calendar = get_calendar_info(hass, call.data) calendar_id = calendar[CONF_CAL_ID] @@ -294,31 +230,33 @@ def setup_services( # Populate the yaml file with all discovered calendars if calendar_id not in calendars: calendars[calendar_id] = calendar - update_config(hass.config.path(YAML_DEVICES), calendar) + await hass.async_add_executor_job( + update_config, hass.config.path(YAML_DEVICES), calendar + ) else: # Prefer entity/name information from yaml, overriding api calendar = calendars[calendar_id] + async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar) - discovery.load_platform( - hass, - Platform.CALENDAR, - DOMAIN, - calendar, - hass_config, - ) + hass.services.async_register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) - hass.services.register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) - - def _scan_for_calendars(call: ServiceCall) -> None: + async def _scan_for_calendars(call: ServiceCall) -> None: """Scan for new calendars.""" - calendars = calendar_service.list_calendars() + try: + calendars = await calendar_service.async_list_calendars() + except ServerNotFoundError as err: + raise HomeAssistantError(str(err)) from err + tasks = [] for calendar in calendars: calendar[CONF_TRACK] = config[CONF_TRACK_NEW] - hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) + tasks.append( + hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) + ) + await asyncio.gather(*tasks) - hass.services.register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) + hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) - def _add_event(call: ServiceCall) -> None: + async def _add_event(call: ServiceCall) -> None: """Add a new event to calendar.""" start = {} end = {} @@ -354,7 +292,7 @@ def setup_services( start = {"dateTime": start_dt, "timeZone": str(hass.config.time_zone)} end = {"dateTime": end_dt, "timeZone": str(hass.config.time_zone)} - calendar_service.create_event( + await calendar_service.async_create_event( call.data[EVENT_CALENDAR_ID], { "summary": call.data[EVENT_SUMMARY], @@ -366,20 +304,11 @@ def setup_services( # Only expose the add event service if we have the correct permissions if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write: - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA ) -def do_setup(hass: HomeAssistant, hass_config: ConfigType, config: ConfigType) -> None: - """Run the setup after we have everything configured.""" - calendar_service = hass.data[DOMAIN][DATA_SERVICE] - setup_services(hass, hass_config, config, calendar_service) - - # Fetch calendars from the API - hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) - - def get_calendar_info( hass: HomeAssistant, calendar: Mapping[str, Any] ) -> dict[str, Any]: diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 1de6eb4e9aa..c279aefef88 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -2,20 +2,166 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable import datetime import logging from typing import Any from googleapiclient import discovery as google_discovery -from oauth2client.file import Storage +import oauth2client +from oauth2client.client import ( + Credentials, + DeviceFlowInfo, + FlowExchangeError, + OAuth2Credentials, + OAuth2DeviceCodeError, + OAuth2WebServerFlow, +) -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt +from .const import CONF_CALENDAR_ACCESS, DATA_CONFIG, DEVICE_AUTH_IMPL, DOMAIN + _LOGGER = logging.getLogger(__name__) - EVENT_PAGE_SIZE = 100 +EXCHANGE_TIMEOUT_SECONDS = 60 + + +class OAuthError(Exception): + """OAuth related error.""" + + +class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): + """OAuth implementation for Device Auth.""" + + def __init__(self, hass: HomeAssistant, client_id: str, client_secret: str) -> None: + """Initialize InstalledAppAuth.""" + super().__init__( + hass, + DEVICE_AUTH_IMPL, + client_id, + client_secret, + oauth2client.GOOGLE_AUTH_URI, + oauth2client.GOOGLE_TOKEN_URI, + ) + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve a Google API Credentials object to Home Assistant token.""" + creds: Credentials = external_data["creds"] + return { + "access_token": creds.access_token, + "refresh_token": creds.refresh_token, + "scope": " ".join(creds.scopes), + "token_type": "Bearer", + "expires_in": creds.token_expiry.timestamp(), + } + + +class DeviceFlow: + """OAuth2 device flow for exchanging a code for an access token.""" + + def __init__( + self, + hass: HomeAssistant, + oauth_flow: OAuth2WebServerFlow, + device_flow_info: DeviceFlowInfo, + ) -> None: + """Initialize DeviceFlow.""" + self._hass = hass + self._oauth_flow = oauth_flow + self._device_flow_info: DeviceFlowInfo = device_flow_info + self._exchange_task_unsub: CALLBACK_TYPE | None = None + + @property + def verification_url(self) -> str: + """Return the verification url that the user should visit to enter the code.""" + return self._device_flow_info.verification_url + + @property + def user_code(self) -> str: + """Return the code that the user should enter at the verification url.""" + return self._device_flow_info.user_code + + async def start_exchange_task( + self, finished_cb: Callable[[Credentials | None], Awaitable[None]] + ) -> None: + """Start the device auth exchange flow polling. + + The callback is invoked with the valid credentials or with None on timeout. + """ + _LOGGER.debug("Starting exchange flow") + assert not self._exchange_task_unsub + max_timeout = dt.utcnow() + datetime.timedelta(seconds=EXCHANGE_TIMEOUT_SECONDS) + # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime + # object without tzinfo. For the comparison below to work, it needs one. + user_code_expiry = self._device_flow_info.user_code_expiry.replace( + tzinfo=datetime.timezone.utc + ) + expiration_time = min(user_code_expiry, max_timeout) + + def _exchange() -> Credentials: + return self._oauth_flow.step2_exchange( + device_flow_info=self._device_flow_info + ) + + async def _poll_attempt(now: datetime.datetime) -> None: + assert self._exchange_task_unsub + _LOGGER.debug("Attempting OAuth code exchange") + # Note: The callback is invoked with None when the device code has expired + creds: Credentials | None = None + if now < expiration_time: + try: + creds = await self._hass.async_add_executor_job(_exchange) + except FlowExchangeError: + _LOGGER.debug("Token not yet ready; trying again later") + return + self._exchange_task_unsub() + self._exchange_task_unsub = None + await finished_cb(creds) + + self._exchange_task_unsub = async_track_time_interval( + self._hass, + _poll_attempt, + datetime.timedelta(seconds=self._device_flow_info.interval), + ) + + +async def async_create_device_flow(hass: HomeAssistant) -> DeviceFlow: + """Create a new Device flow.""" + conf = hass.data[DOMAIN][DATA_CONFIG] + oauth_flow = OAuth2WebServerFlow( + client_id=conf[CONF_CLIENT_ID], + client_secret=conf[CONF_CLIENT_SECRET], + scope=conf[CONF_CALENDAR_ACCESS].scope, + redirect_uri="", + ) + try: + device_flow_info = await hass.async_add_executor_job( + oauth_flow.step1_get_device_and_user_codes + ) + except OAuth2DeviceCodeError as err: + raise OAuthError(str(err)) from err + return DeviceFlow(hass, oauth_flow, device_flow_info) + + +def _async_google_creds(hass: HomeAssistant, token: dict[str, Any]) -> Credentials: + """Convert a Home Assistant token to a Google API Credentials object.""" + conf = hass.data[DOMAIN][DATA_CONFIG] + return OAuth2Credentials( + access_token=token["access_token"], + client_id=conf[CONF_CLIENT_ID], + client_secret=conf[CONF_CLIENT_SECRET], + refresh_token=token["refresh_token"], + token_expiry=token["expires_at"], + token_uri=oauth2client.GOOGLE_TOKEN_URI, + scopes=[conf[CONF_CALENDAR_ACCESS].scope], + user_agent=None, + ) def _api_time_format(time: datetime.datetime | None) -> str | None: @@ -26,26 +172,44 @@ def _api_time_format(time: datetime.datetime | None) -> str | None: class GoogleCalendarService: """Calendar service interface to Google.""" - def __init__(self, hass: HomeAssistant, storage: Storage) -> None: + def __init__( + self, hass: HomeAssistant, session: config_entry_oauth2_flow.OAuth2Session + ) -> None: """Init the Google Calendar service.""" self._hass = hass - self._storage = storage + self._session = session - def _get_service(self) -> google_discovery.Resource: - """Get the calendar service from the storage file token.""" + async def _async_get_service(self) -> google_discovery.Resource: + """Get the calendar service with valid credetnails.""" + await self._session.async_ensure_token_valid() + creds = _async_google_creds(self._hass, self._session.token) return google_discovery.build( - "calendar", "v3", credentials=self._storage.get(), cache_discovery=False + "calendar", "v3", credentials=creds, cache_discovery=False ) - def list_calendars(self) -> list[dict[str, Any]]: + async def async_list_calendars( + self, + ) -> list[dict[str, Any]]: """Return the list of calendars the user has added to their list.""" - cal_list = self._get_service().calendarList() # pylint: disable=no-member - return cal_list.list().execute()["items"] + service = await self._async_get_service() - def create_event(self, calendar_id: str, event: dict[str, Any]) -> dict[str, Any]: - """Create an event.""" - events = self._get_service().events() # pylint: disable=no-member - return events.insert(calendarId=calendar_id, body=event).execute() + def _list_calendars() -> list[dict[str, Any]]: + cal_list = service.calendarList() # pylint: disable=no-member + return cal_list.list().execute()["items"] + + return await self._hass.async_add_executor_job(_list_calendars) + + async def async_create_event( + self, calendar_id: str, event: dict[str, Any] + ) -> dict[str, Any]: + """Return the list of calendars the user has added to their list.""" + service = await self._async_get_service() + + def _create_event() -> dict[str, Any]: + events = service.events() # pylint: disable=no-member + return events.insert(calendarId=calendar_id, body=event).execute() + + return await self._hass.async_add_executor_job(_create_event) async def async_list_events( self, @@ -56,33 +220,20 @@ class GoogleCalendarService: page_token: str | None = None, ) -> tuple[list[dict[str, Any]], str | None]: """Return the list of events.""" - return await self._hass.async_add_executor_job( - self.list_events, - calendar_id, - start_time, - end_time, - search, - page_token, - ) + service = await self._async_get_service() - def list_events( - self, - calendar_id: str, - start_time: datetime.datetime | None = None, - end_time: datetime.datetime | None = None, - search: str | None = None, - page_token: str | None = None, - ) -> tuple[list[dict[str, Any]], str | None]: - """Return the list of events.""" - events = self._get_service().events() # pylint: disable=no-member - result = events.list( - calendarId=calendar_id, - timeMin=_api_time_format(start_time if start_time else dt.now()), - timeMax=_api_time_format(end_time), - q=search, - maxResults=EVENT_PAGE_SIZE, - pageToken=page_token, - singleEvents=True, # Flattens recurring events - orderBy="startTime", - ).execute() - return (result["items"], result.get("nextPageToken")) + def _list_events() -> tuple[list[dict[str, Any]], str | None]: + events = service.events() # pylint: disable=no-member + result = events.list( + calendarId=calendar_id, + timeMin=_api_time_format(start_time if start_time else dt.now()), + timeMax=_api_time_format(end_time), + q=search, + maxResults=EVENT_PAGE_SIZE, + pageToken=page_token, + singleEvents=True, # Flattens recurring events + orderBy="startTime", + ).execute() + return (result["items"], result.get("nextPageToken")) + + return await self._hass.async_add_executor_job(_list_events) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 282c80988e4..666096fd8c3 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -14,11 +14,13 @@ from homeassistant.components.calendar import ( calculate_offset, is_offset_reached, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from . import ( @@ -29,8 +31,10 @@ from . import ( DATA_SERVICE, DEFAULT_CONF_OFFSET, DOMAIN, + SERVICE_SCAN_CALENDARS, ) from .api import GoogleCalendarService +from .const import DISCOVER_CALENDAR _LOGGER = logging.getLogger(__name__) @@ -48,19 +52,39 @@ TRANSPARENCY = "transparency" OPAQUE = "opaque" -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - disc_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the calendar platform for event devices.""" - if disc_info is None: - return + """Set up the google calendar platform.""" - if not any(data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]): - return + @callback + def async_discover(discovery_info: dict[str, Any]) -> None: + _async_setup_entities( + hass, + entry, + async_add_entities, + discovery_info, + ) + entry.async_on_unload( + async_dispatcher_connect(hass, DISCOVER_CALENDAR, async_discover) + ) + + # Look for any new calendars + try: + await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, blocking=True) + except HomeAssistantError as err: + # This can happen if there's a connection error during setup. + raise PlatformNotReady(str(err)) from err + + +@callback +def _async_setup_entities( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + disc_info: dict[str, Any], +) -> None: calendar_service = hass.data[DOMAIN][DATA_SERVICE] entities = [] for data in disc_info[CONF_ENTITIES]: @@ -74,7 +98,7 @@ def setup_platform( ) entities.append(entity) - add_entities(entities, True) + async_add_entities(entities, True) class GoogleCalendarEventDevice(CalendarEventDevice): @@ -144,10 +168,10 @@ class GoogleCalendarEventDevice(CalendarEventDevice): return event_list @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest data.""" try: - items, _ = self._calendar_service.list_events( + items, _ = await self._calendar_service.async_list_events( self._calendar_id, search=self._search ) except ServerNotFoundError as err: diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py new file mode 100644 index 00000000000..c70dd83fcae --- /dev/null +++ b/homeassistant/components/google/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for Google integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from oauth2client.client import Credentials + +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .api import DeviceFlow, OAuthError, async_create_device_flow +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Google Calendars OAuth2 authentication.""" + + DOMAIN = DOMAIN + + def __init__(self) -> None: + """Set up instance.""" + super().__init__() + self._reauth = False + self._device_flow: DeviceFlow | None = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + async def async_step_import(self, info: dict[str, Any]) -> FlowResult: + """Import existing auth from Nest.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + implementations = await config_entry_oauth2_flow.async_get_implementations( + self.hass, self.DOMAIN + ) + assert len(implementations) == 1 + self.flow_impl = list(implementations.values())[0] + self.external_data = info + return await super().async_step_creation(info) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle external yaml configuration.""" + if not self._reauth and self._async_current_entries(): + return self.async_abort(reason="already_configured") + return await super().async_step_user(user_input) + + async def async_step_auth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Create an entry for auth.""" + # The default behavior from the parent class is to redirect the + # user with an external step. When using the device flow, we instead + # prompt the user to visit a URL and enter a code. The device flow + # background task will poll the exchange endpoint to get valid + # creds or until a timeout is complete. + if user_input is not None: + return self.async_show_progress_done(next_step_id="creation") + + if not self._device_flow: + _LOGGER.debug("Creating DeviceAuth flow") + try: + device_flow = await async_create_device_flow(self.hass) + except OAuthError as err: + _LOGGER.error("Error initializing device flow: %s", str(err)) + return self.async_abort(reason="oauth_error") + self._device_flow = device_flow + + async def _exchange_finished(creds: Credentials | None) -> None: + self.external_data = {"creds": creds} # is None on timeout/expiration + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure( + flow_id=self.flow_id, user_input={} + ) + ) + + await device_flow.start_exchange_task(_exchange_finished) + + return self.async_show_progress( + step_id="auth", + description_placeholders={ + "url": self._device_flow.verification_url, + "user_code": self._device_flow.user_code, + }, + progress_action="exchange", + ) + + async def async_step_creation( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle external yaml configuration.""" + if self.external_data.get("creds") is None: + return self.async_abort(reason="code_expired") + return await super().async_step_creation(user_input) + + async def async_oauth_create_entry(self, data: dict) -> FlowResult: + """Create an entry for the flow, or update existing entry.""" + existing_entries = self._async_current_entries() + if existing_entries: + assert len(existing_entries) == 1 + entry = existing_entries[0] + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth = True + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py new file mode 100644 index 00000000000..d5cdabb0638 --- /dev/null +++ b/homeassistant/components/google/const.py @@ -0,0 +1,30 @@ +"""Constants for google integration.""" +from __future__ import annotations + +from enum import Enum + +DOMAIN = "google" +DEVICE_AUTH_IMPL = "device_auth" + +CONF_CALENDAR_ACCESS = "calendar_access" +DATA_CALENDARS = "calendars" +DATA_SERVICE = "service" +DATA_CONFIG = "config" + +DISCOVER_CALENDAR = "google_discover_calendar" + + +class FeatureAccess(Enum): + """Class to represent different access scopes.""" + + read_only = "https://www.googleapis.com/auth/calendar.readonly" + read_write = "https://www.googleapis.com/auth/calendar" + + def __init__(self, scope: str) -> None: + """Init instance.""" + self._scope = scope + + @property + def scope(self) -> str: + """Google calendar scope for the feature.""" + return self._scope diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 53b85ef7aa2..589ecb25b21 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,6 +1,8 @@ { "domain": "google", "name": "Google Calendars", + "config_flow": true, + "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": [ "google-api-python-client==2.38.0", diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json new file mode 100644 index 00000000000..8208b7bebc9 --- /dev/null +++ b/homeassistant/components/google/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nest integration needs to re-authenticate your account" + }, + "auth": { + "title": "Link Google Account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "code_expired": "Authentication code expired or credential setup is invalid, please try again.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + }, + "progress": { + "exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}" + } + } +} diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json new file mode 100644 index 00000000000..51d1ad9aab8 --- /dev/null +++ b/homeassistant/components/google/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "code_expired": "Authentication code expired, please try again.", + "invalid_access_token": "Invalid access token", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth_error": "Received invalid token data.", + "reauth_successful": "Re-authentication was successful" + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "progress": { + "exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Nest integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 99e5cf7caf3..6d572efb0c7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -125,6 +125,7 @@ FLOWS = [ "goalzero", "gogogate2", "goodwe", + "google", "google_travel_time", "gpslogger", "gree", diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index f3d8f9a28b9..8efeac9983d 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -18,6 +18,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow +from tests.common import MockConfigEntry + ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE ApiResult = Callable[[dict[str, Any]], None] @@ -156,6 +158,25 @@ async def storage() -> YieldFixture[FakeStorage]: yield storage +@pytest.fixture +async def config_entry(token_scopes: list[str]) -> MockConfigEntry: + """Fixture to create a config entry for the integration.""" + token_expiry = utcnow() + datetime.timedelta(days=7) + return MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": " ".join(token_scopes), + "token_type": "Bearer", + "expires_at": token_expiry.timestamp(), + }, + }, + ) + + @pytest.fixture async def mock_token_read( hass: HomeAssistant, diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 34227ae02b1..baa09d0b88f 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -53,10 +53,15 @@ TEST_EVENT = { @pytest.fixture(autouse=True) def mock_test_setup( - mock_calendars_yaml, test_api_calendar, mock_calendars_list, mock_token_read + hass, + mock_calendars_yaml, + test_api_calendar, + mock_calendars_list, + config_entry, ): """Fixture that pulls in the default fixtures for tests in this file.""" mock_calendars_list({"items": [test_api_calendar]}) + config_entry.add_to_hass(hass) return @@ -300,12 +305,11 @@ async def test_update_error( assert state.name == TEST_ENTITY_NAME assert state.state == "on" - # Advance time to avoid throttling + # Advance time beyond update/throttle point now += datetime.timedelta(minutes=30) with patch( "homeassistant.components.google.api.google_discovery.build" ) as mock, patch("homeassistant.util.utcnow", return_value=now): - mock.return_value.events.return_value.list.return_value.execute.return_value = { "items": [ { @@ -417,3 +421,19 @@ async def test_opaque_event( assert response.status == HTTPStatus.OK events = await response.json() assert (len(events) > 0) == expect_visible_event + + +async def test_scan_calendar_error( + hass, + calendar_resource, + component_setup, + test_api_calendar, +): + """Test that the calendar update handles a server error.""" + with patch( + "homeassistant.components.google.api.google_discovery.build", + side_effect=httplib2.ServerNotFoundError("unit test"), + ): + assert await component_setup() + + assert not hass.states.get(TEST_ENTITY) diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py new file mode 100644 index 00000000000..a5467b95dcb --- /dev/null +++ b/tests/components/google/test_config_flow.py @@ -0,0 +1,350 @@ +"""Test the google config flow.""" + +import datetime +from unittest.mock import Mock, patch + +from oauth2client.client import ( + FlowExchangeError, + OAuth2Credentials, + OAuth2DeviceCodeError, +) +import pytest + +from homeassistant import config_entries +from homeassistant.components.google.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .conftest import ComponentSetup, YieldFixture + +from tests.common import MockConfigEntry, async_fire_time_changed + +CODE_CHECK_INTERVAL = 1 +CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) + + +@pytest.fixture(autouse=True) +async def request_setup(current_request_with_host) -> None: + """Request setup.""" + return + + +@pytest.fixture +async def code_expiration_delta() -> datetime.timedelta: + """Fixture for code expiration time, defaulting to the future.""" + return datetime.timedelta(minutes=3) + + +@pytest.fixture +async def mock_code_flow( + code_expiration_delta: datetime.timedelta, +) -> YieldFixture[Mock]: + """Fixture for initiating OAuth flow.""" + with patch( + "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + ) as mock_flow: + mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta + mock_flow.return_value.interval = CODE_CHECK_INTERVAL + yield mock_flow + + +@pytest.fixture +async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: + """Fixture for mocking out the exchange for credentials.""" + with patch( + "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds + ) as mock: + yield mock + + +async def fire_alarm(hass, point_in_time): + """Fire an alarm and wait for callbacks to run.""" + with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): + async_fire_time_changed(hass, point_in_time) + await hass.async_block_till_done() + + +async def test_full_flow( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "create_entry" + assert result.get("title") == "Configuration.yaml" + assert "data" in result + data = result["data"] + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + +async def test_code_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + with patch( + "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + side_effect=OAuth2DeviceCodeError("Test Failure"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "oauth_error" + + +@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) +async def test_expired_after_exchange( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "code_expired" + + +async def test_exchange_error( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test an error while exchanging the code for credentials.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + # Run one tick to invoke the credential exchange check + now = utcnow() + with patch( + "oauth2client.client.OAuth2WebServerFlow.step2_exchange", + side_effect=FlowExchangeError(), + ): + now += CODE_CHECK_ALARM_TIMEDELTA + await fire_alarm(hass, now) + await hass.async_block_till_done() + + # Status has not updated, will retry + result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + + # Run another tick, which attempts credential exchange again + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + now += CODE_CHECK_ALARM_TIMEDELTA + await fire_alarm(hass, now) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "create_entry" + assert result.get("title") == "Configuration.yaml" + assert "data" in result + data = result["data"] + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + +async def test_existing_config_entry( + hass: HomeAssistant, + config_entry: MockConfigEntry, + component_setup: ComponentSetup, +) -> None: + """Test can't configure when config entry already exists.""" + config_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" + + +async def test_missing_configuration( + hass: HomeAssistant, +) -> None: + """Test can't configure when config entry already exists.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "missing_configuration" + + +async def test_import_config_entry_from_existing_token( + hass: HomeAssistant, + mock_token_read: None, + component_setup: ComponentSetup, +) -> None: + """Test setup with an existing token file.""" + assert await component_setup() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + data = entries[0].data + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test can't configure when config entry already exists.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": "device_auth", + "token": {"access_token": "OLD_ACCESS_TOKEN"}, + }, + ) + config_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config_entry.data + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={}, + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "abort" + assert result.get("reason") == "reauth_successful" + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + data = entries[0].data + assert "token" in data + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "device_auth", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 0f02e20c735..6fc575ba03d 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -6,11 +6,6 @@ import datetime from typing import Any from unittest.mock import Mock, call, patch -from oauth2client.client import ( - FlowExchangeError, - OAuth2Credentials, - OAuth2DeviceCodeError, -) import pytest from homeassistant.components.google import ( @@ -18,6 +13,7 @@ from homeassistant.components.google import ( SERVICE_ADD_EVENT, SERVICE_SCAN_CALENDARS, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State from homeassistant.util.dt import utcnow @@ -30,73 +26,13 @@ from .conftest import ( TEST_YAML_ENTITY_NAME, ApiResult, ComponentSetup, - YieldFixture, ) -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry # Typing helpers HassApi = Callable[[], Awaitable[dict[str, Any]]] -CODE_CHECK_INTERVAL = 1 -CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) - - -@pytest.fixture -async def code_expiration_delta() -> datetime.timedelta: - """Fixture for code expiration time, defaulting to the future.""" - return datetime.timedelta(minutes=3) - - -@pytest.fixture -async def mock_code_flow( - code_expiration_delta: datetime.timedelta, -) -> YieldFixture[Mock]: - """Fixture for initiating OAuth flow.""" - with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", - ) as mock_flow: - mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta - mock_flow.return_value.interval = CODE_CHECK_INTERVAL - yield mock_flow - - -@pytest.fixture -async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: - """Fixture for mocking out the exchange for credentials.""" - with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds - ) as mock: - yield mock - - -@pytest.fixture -async def mock_notification() -> YieldFixture[Mock]: - """Fixture for capturing persistent notifications.""" - with patch("homeassistant.components.persistent_notification.create") as mock: - yield mock - - -async def fire_alarm(hass, point_in_time): - """Fire an alarm and wait for callbacks to run.""" - with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): - async_fire_time_changed(hass, point_in_time) - await hass.async_block_till_done() - - -@pytest.mark.parametrize("config", [{}]) -async def test_setup_config_empty( - hass: HomeAssistant, - component_setup: ComponentSetup, - mock_notification: Mock, -): - """Test setup component with an empty configuruation.""" - assert await component_setup() - - mock_notification.assert_not_called() - - assert not hass.states.get(TEST_YAML_ENTITY) - def assert_state(actual: State | None, expected: State | None) -> None: """Assert that the two states are equal.""" @@ -108,118 +44,29 @@ def assert_state(actual: State | None, expected: State | None) -> None: assert actual.attributes == expected.attributes -async def test_init_success( +@pytest.fixture +def setup_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> MockConfigEntry: + """Fixture to initialize the config entry.""" + config_entry.add_to_hass(hass) + + +async def test_unload_entry( hass: HomeAssistant, - mock_code_flow: Mock, - mock_exchange: Mock, - mock_notification: Mock, - mock_calendars_list: ApiResult, - test_api_calendar: dict[str, Any], - mock_calendars_yaml: None, component_setup: ComponentSetup, + setup_config_entry: MockConfigEntry, ) -> None: - """Test successful creds setup.""" - mock_calendars_list({"items": [test_api_calendar]}) - assert await component_setup() + """Test load and unload of a ConfigEntry.""" + await component_setup() - # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED - state = hass.states.get(TEST_YAML_ENTITY) - assert state - assert state.name == TEST_YAML_ENTITY_NAME - assert state.state == STATE_OFF - - mock_notification.assert_called() - assert "We are all setup now" in mock_notification.call_args[0][1] - - -async def test_code_error( - hass: HomeAssistant, - mock_code_flow: Mock, - component_setup: ComponentSetup, - mock_notification: Mock, -) -> None: - """Test loading the integration with no existing credentials.""" - - with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", - side_effect=OAuth2DeviceCodeError("Test Failure"), - ): - assert await component_setup() - - assert not hass.states.get(TEST_YAML_ENTITY) - - mock_notification.assert_called() - assert "Error: Test Failure" in mock_notification.call_args[0][1] - - -@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) -async def test_expired_after_exchange( - hass: HomeAssistant, - mock_code_flow: Mock, - component_setup: ComponentSetup, - mock_notification: Mock, -) -> None: - """Test loading the integration with no existing credentials.""" - - assert await component_setup() - - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - - assert not hass.states.get(TEST_YAML_ENTITY) - - mock_notification.assert_called() - assert ( - "Authentication code expired, please restart Home-Assistant and try again" - in mock_notification.call_args[0][1] - ) - - -async def test_exchange_error( - hass: HomeAssistant, - mock_code_flow: Mock, - component_setup: ComponentSetup, - mock_notification: Mock, -) -> None: - """Test an error while exchanging the code for credentials.""" - - with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", - side_effect=FlowExchangeError(), - ): - assert await component_setup() - - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - - assert not hass.states.get(TEST_YAML_ENTITY) - - mock_notification.assert_called() - assert "In order to authorize Home-Assistant" in mock_notification.call_args[0][1] - - -async def test_existing_token( - hass: HomeAssistant, - mock_token_read: None, - component_setup: ComponentSetup, - mock_calendars_yaml: None, - mock_calendars_list: ApiResult, - test_api_calendar: dict[str, Any], - mock_notification: Mock, -) -> None: - """Test setup with an existing token file.""" - mock_calendars_list({"items": [test_api_calendar]}) - assert await component_setup() - - state = hass.states.get(TEST_YAML_ENTITY) - assert state - assert state.name == TEST_YAML_ENTITY_NAME - assert state.state == STATE_OFF - - mock_notification.assert_not_called() + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( @@ -228,80 +75,61 @@ async def test_existing_token( async def test_existing_token_missing_scope( hass: HomeAssistant, token_scopes: list[str], - mock_token_read: None, component_setup: ComponentSetup, - mock_calendars_yaml: None, - mock_calendars_list: ApiResult, - test_api_calendar: dict[str, Any], - mock_notification: Mock, - mock_code_flow: Mock, - mock_exchange: Mock, + config_entry: MockConfigEntry, ) -> None: """Test setup where existing token does not have sufficient scopes.""" - mock_calendars_list({"items": [test_api_calendar]}) + config_entry.add_to_hass(hass) assert await component_setup() - # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - assert len(mock_exchange.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.SETUP_ERROR - state = hass.states.get(TEST_YAML_ENTITY) - assert state - assert state.name == TEST_YAML_ENTITY_NAME - assert state.state == STATE_OFF - - # No notifications on success - mock_notification.assert_called() - assert "We are all setup now" in mock_notification.call_args[0][1] + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" @pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]]) async def test_calendar_yaml_missing_required_fields( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, - mock_notification: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test setup with a missing schema fields, ignores the error and continues.""" assert await component_setup() assert not hass.states.get(TEST_YAML_ENTITY) - mock_notification.assert_not_called() - @pytest.mark.parametrize("calendars_config", [[{"missing-cal_id": "invalid-schema"}]]) async def test_invalid_calendar_yaml( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, - mock_notification: Mock, + setup_config_entry: MockConfigEntry, ) -> None: - """Test setup with missing entity id fields fails to setup the integration.""" - + """Test setup with missing entity id fields fails to setup the config entry.""" # Integration fails to setup - assert not await component_setup() + assert await component_setup() + + # XXX No config entries assert not hass.states.get(TEST_YAML_ENTITY) - mock_notification.assert_not_called() - async def test_calendar_yaml_error( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], - mock_notification: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test setup with yaml file not found.""" - mock_calendars_list({"items": [test_api_calendar]}) with patch("homeassistant.components.google.open", side_effect=FileNotFoundError()): @@ -344,12 +172,12 @@ async def test_calendar_yaml_error( ) async def test_track_new( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_calendars_yaml: None, expected_state: State, + setup_config_entry: MockConfigEntry, ) -> None: """Test behavior of configuration.yaml settings for tracking new calendars not in the config.""" @@ -363,11 +191,11 @@ async def test_track_new( @pytest.mark.parametrize("calendars_config", [[]]) async def test_found_calendar_from_api( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_yaml: None, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], + setup_config_entry: MockConfigEntry, ) -> None: """Test finding a calendar from the API.""" @@ -402,13 +230,13 @@ async def test_found_calendar_from_api( ) async def test_calendar_config_track_new( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_yaml: None, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], calendars_config_track: bool, expected_state: State, + setup_config_entry: MockConfigEntry, ) -> None: """Test calendar config that overrides whether or not a calendar is tracked.""" @@ -421,11 +249,11 @@ async def test_calendar_config_track_new( async def test_add_event( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_insert_event: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test service call that adds an event.""" @@ -471,7 +299,6 @@ async def test_add_event( ) async def test_add_event_date_in_x( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], @@ -479,6 +306,7 @@ async def test_add_event_date_in_x( date_fields: dict[str, Any], start_timedelta: datetime.timedelta, end_timedelta: datetime.timedelta, + setup_config_entry: MockConfigEntry, ) -> None: """Test service call that adds an event with various time ranges.""" @@ -514,10 +342,10 @@ async def test_add_event_date_in_x( async def test_add_event_date( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, mock_insert_event: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test service call that sets a date range.""" @@ -554,11 +382,11 @@ async def test_add_event_date( async def test_add_event_date_time( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_insert_event: Mock, + setup_config_entry: MockConfigEntry, ) -> None: """Test service call that adds an event with a date time range.""" @@ -601,10 +429,10 @@ async def test_add_event_date_time( async def test_scan_calendars( hass: HomeAssistant, - mock_token_read: None, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], + setup_config_entry: MockConfigEntry, ) -> None: """Test finding a calendar from the API.""" From 125ab5eb2bd342ce387afda6849cff50b7d6c8a1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 15 Mar 2022 08:20:24 +0100 Subject: [PATCH 0448/1054] Handle ConnectionClosed in SamsungTV try_connect (#68125) * Handle ConnectionClosed in SamsungTV try_connect * Add tests * Add quotes around the error message Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 9 +++++++ .../components/samsungtv/test_config_flow.py | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index acc6d5cd766..398b14a20b1 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -374,6 +374,15 @@ class SamsungTVWSBridge(SamsungTVBridge): self.token = remote.token LOGGER.debug("Working config: %s", config) return RESULT_SUCCESS + except ConnectionClosedError as err: + LOGGER.info( + "Working but unsupported config: %s, error: '%s'; this may " + "be an indication that access to the TV has been denied. Please " + "check the Device Connection Manager on your TV", + config, + err, + ) + result = RESULT_NOT_SUPPORTED except WebSocketException as err: LOGGER.debug( "Working but unsupported config: %s, error: %s", config, err diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index f8a787753f0..8f1a22395fb 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -6,7 +6,12 @@ import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.exceptions import ConnectionFailure, HttpApiError -from websockets.exceptions import WebSocketException, WebSocketProtocolError +from websockets import frames +from websockets.exceptions import ( + ConnectionClosedError, + WebSocketException, + WebSocketProtocolError, +) from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf @@ -283,6 +288,26 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED +async def test_user_websocket_access_denied( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test starting a flow by user for not supported device.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=ConnectionClosedError(rcvd=None, sent=frames.Close(1002, "")), + ): + # websocket device not supported + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_NOT_SUPPORTED + assert "Please check the Device Connection Manager on your TV" in caplog.text + + async def test_user_not_successful(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" with patch( From ef71ab04ad202b3c692aa3ae2f68e0e1e2176486 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 08:24:13 +0100 Subject: [PATCH 0449/1054] Remove unused TypeVars (#68155) --- homeassistant/components/dlna_dmr/media_player.py | 2 -- homeassistant/components/vlc_telnet/media_player.py | 1 - 2 files changed, 3 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 265c6e9dde6..abee0cf13df 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -77,8 +77,6 @@ _T = TypeVar("_T", bound="DlnaDmrEntity") _R = TypeVar("_R") _P = ParamSpec("_P") -Func = TypeVar("Func", bound=Callable[..., Any]) - def catch_request_errors( func: Callable[Concatenate[_T, _P], Awaitable[_R]] # type: ignore[misc] diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 72beede3f2f..99b37f97e0c 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -60,7 +60,6 @@ SUPPORT_VLC = ( ) _T = TypeVar("_T", bound="VlcDevice") -_R = TypeVar("_R") _P = ParamSpec("_P") From 7615f138d9839abf4456da73629d6757e73abf7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20=C3=96berg?= <62830707+droberg@users.noreply.github.com> Date: Tue, 15 Mar 2022 08:26:54 +0100 Subject: [PATCH 0450/1054] Clean up code for onewire config flow (#67970) * Change debug level of integration reload after config update to debug * Remove extra .keys() call for dict itreation * Remove unecessary type check * Remove unused LOGGER reference --- homeassistant/components/onewire/__init__.py | 2 +- .../components/onewire/config_flow.py | 28 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index ceef037bfcc..c6f3d7dfa3f 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -47,5 +47,5 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" - _LOGGER.info("Configuration options updated, reloading OneWire integration") + _LOGGER.debug("Configuration options updated, reloading OneWire integration") await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index a7373206666..25604473bcf 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -1,7 +1,6 @@ """Config flow for 1-Wire component.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -47,9 +46,6 @@ DATA_SCHEMA_MOUNTDIR = vol.Schema( ) -_LOGGER = logging.getLogger(__name__) - - async def validate_input_owserver( hass: HomeAssistant, data: dict[str, Any] ) -> dict[str, str]: @@ -255,7 +251,7 @@ class OnewireOptionsFlowHandler(OptionsFlow): default=self._get_current_configured_sensors(), description="Multiselect with list of devices to choose from", ): cv.multi_select( - {device: False for device in self.configurable_devices.keys()} + {device: False for device in self.configurable_devices} ), } ), @@ -273,18 +269,16 @@ class OnewireOptionsFlowHandler(OptionsFlow): return await self._update_options() self.current_device, description = self.devices_to_configure.popitem() - data_schema: vol.Schema - if description.family == "28": - data_schema = vol.Schema( - { - vol.Required( - OPTION_ENTRY_SENSOR_PRECISION, - default=self._get_current_setting( - description.id, OPTION_ENTRY_SENSOR_PRECISION, "temperature" - ), - ): vol.In(PRECISION_MAPPING_FAMILY_28), - } - ) + data_schema = vol.Schema( + { + vol.Required( + OPTION_ENTRY_SENSOR_PRECISION, + default=self._get_current_setting( + description.id, OPTION_ENTRY_SENSOR_PRECISION, "temperature" + ), + ): vol.In(PRECISION_MAPPING_FAMILY_28), + } + ) return self.async_show_form( step_id="configure_device", From 66f506557f1fb7181790055c923ecf4dea7d5a1d Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 15 Mar 2022 08:53:53 +0100 Subject: [PATCH 0451/1054] Prevent adding plugwise products used as secondary controllers (#68098) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/config_flow.py | 8 +++++++- homeassistant/components/plugwise/strings.json | 1 + tests/components/plugwise/test_config_flow.py | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index e69d92e3cd0..5b5c79ba2b8 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Any -from plugwise.exceptions import InvalidAuthentication, PlugwiseException +from plugwise.exceptions import ( + InvalidAuthentication, + InvalidSetupError, + PlugwiseException, +) from plugwise.smile import Smile import voluptuous as vol @@ -124,6 +128,8 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): try: api = await validate_gw_input(self.hass, user_input) + except InvalidSetupError: + errors[CONF_BASE] = "invalid_setup" except InvalidAuthentication: errors[CONF_BASE] = "invalid_auth" except PlugwiseException: diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index e5a7ab5a6a9..dfa0ac920c7 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -32,6 +32,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 9fdd0323518..7668d492e75 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, + InvalidSetupError, PlugwiseException, ) import pytest @@ -223,6 +224,7 @@ async def test_zercoconf_discovery_update_configuration(hass: HomeAssistant) -> "side_effect,reason", [ (InvalidAuthentication, "invalid_auth"), + (InvalidSetupError, "invalid_setup"), (ConnectionFailedError, "cannot_connect"), (PlugwiseException, "cannot_connect"), (RuntimeError, "unknown"), From 7ab9e5cf0f7783e1aafae3fe98702a292466653f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 09:24:52 +0100 Subject: [PATCH 0452/1054] Improve sonos error decorator typing (#67199) --- homeassistant/components/sonos/helpers.py | 29 +++++++++++++++++++---- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index a11847d2b0c..3edf23f0c3c 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar, overload from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException @@ -17,6 +17,7 @@ from .exception import SonosUpdateError if TYPE_CHECKING: from .entity import SonosEntity from .household_coordinator import SonosHouseholdCoordinator + from .media import SonosMedia from .speaker import SonosSpeaker UID_PREFIX = "RINCON_" @@ -24,11 +25,31 @@ UID_POSTFIX = "01400" _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T", bound="SonosSpeaker | SonosEntity | SonosHouseholdCoordinator") +_T = TypeVar( + "_T", bound="SonosSpeaker | SonosMedia | SonosEntity | SonosHouseholdCoordinator" +) _R = TypeVar("_R") _P = ParamSpec("_P") +@overload +def soco_error( + errorcodes: None = ..., +) -> Callable[ # type: ignore[misc] + [Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R] +]: + ... + + +@overload +def soco_error( + errorcodes: list[str], +) -> Callable[ # type: ignore[misc] + [Callable[Concatenate[_T, _P], _R]], Callable[Concatenate[_T, _P], _R | None] +]: + ... + + def soco_error( errorcodes: list[str] | None = None, ) -> Callable[ # type: ignore[misc] @@ -43,7 +64,7 @@ def soco_error( def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: """Wrap for all soco UPnP exception.""" - args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) + args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) # type: ignore[attr-defined] try: result = funct(self, *args, **kwargs) except (OSError, SoCoException, SoCoUPnPException) as err: @@ -61,7 +82,7 @@ def soco_error( message = f"Error calling {function} on {target}: {err}" raise SonosUpdateError(message) from err - dispatch_soco = args_soco or self.soco + dispatch_soco = args_soco or self.soco # type: ignore[union-attr] dispatcher_send( self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{dispatch_soco.uid}", diff --git a/mypy.ini b/mypy.ini index bf98c7dbcc6..16bf8394eee 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2660,9 +2660,6 @@ ignore_errors = true [mypy-homeassistant.components.sonos.favorites] ignore_errors = true -[mypy-homeassistant.components.sonos.helpers] -ignore_errors = true - [mypy-homeassistant.components.sonos.media_browser] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 7e739cfb7d2..fcc03a35524 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -128,7 +128,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.diagnostics", "homeassistant.components.sonos.entity", "homeassistant.components.sonos.favorites", - "homeassistant.components.sonos.helpers", "homeassistant.components.sonos.media_browser", "homeassistant.components.sonos.media_player", "homeassistant.components.sonos.number", From 376ac1bd8373a0544f9b04240cb7d04a59d787f8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 09:30:55 +0100 Subject: [PATCH 0453/1054] Don't import TypeVars from core modules (#68154) --- homeassistant/components/zha/core/channels/security.py | 3 ++- homeassistant/helpers/intent.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 5ef0ee3d9fa..8aa5c620656 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -12,7 +12,7 @@ from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import security from zigpy.zcl.clusters.security import IasAce as AceCluster -from homeassistant.core import CALLABLE_T, callback +from homeassistant.core import callback from .. import registries, typing as zha_typing from ..const import ( @@ -23,6 +23,7 @@ from ..const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) +from ..typing import CALLABLE_T from .base import ChannelStatus, ZigbeeChannel IAS_ACE_ARM = 0x0000 # ("arm", (t.enum8, t.CharacterString, t.uint8_t), False), diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 44dd21d7fa3..7b0b1033016 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -4,12 +4,12 @@ from __future__ import annotations from collections.abc import Callable, Iterable import logging import re -from typing import Any +from typing import Any, TypeVar import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES -from homeassistant.core import Context, HomeAssistant, State, T, callback +from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass @@ -17,6 +17,7 @@ from . import config_validation as cv _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] +_T = TypeVar("_T") INTENT_TURN_OFF = "HassTurnOff" INTENT_TURN_ON = "HassTurnOn" @@ -163,7 +164,7 @@ class IntentHandler: return f"<{self.__class__.__name__} - {self.intent_type}>" -def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> T | None: +def _fuzzymatch(name: str, items: Iterable[_T], key: Callable[[_T], str]) -> _T | None: """Fuzzy matching function.""" matches = [] pattern = ".*?".join(name) From 283f4555a4055208318bc543e931b174b2a32c34 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 15 Mar 2022 10:51:26 +0100 Subject: [PATCH 0454/1054] Fix deconz typing (#68143) --- homeassistant/components/deconz/light.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 4fb401ffab2..d9aeec37fb2 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import ValuesView -from typing import Any, cast +from typing import Any from pydeconz.group import Group from pydeconz.light import ( @@ -220,15 +220,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity): elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if ( - alert := FLASH_TO_DECONZ.get(cast(str, kwargs.get(ATTR_FLASH))) - ) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: data["alert"] = alert del data["on"] - if ( - effect := EFFECT_TO_DECONZ.get(cast(str, kwargs.get(ATTR_EFFECT))) - ) is not None: + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT, ""))) is not None: data["effect"] = effect await self._device.set_state(**data) @@ -244,9 +240,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["brightness"] = 0 data["transition_time"] = int(attr_transition * 10) - if ( - alert := FLASH_TO_DECONZ.get(cast(str, kwargs.get(ATTR_FLASH))) - ) is not None: + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH, ""))) is not None: data["alert"] = alert del data["on"] From fa1394ff474542783ab973ecf9cdd0d5395a2601 Mon Sep 17 00:00:00 2001 From: Numa Perez <41305393+nprez83@users.noreply.github.com> Date: Tue, 15 Mar 2022 05:59:18 -0400 Subject: [PATCH 0455/1054] Fix lyric climate (#67018) * Fixed the issues related to auto mode I was having the same issues as described in #63403, specifically, the error stating that Mode 7 is not valid, only Heat, Cool, Off when trying to do anything while the thermostat is set to Auto. This error originates with the way the Lyric API handles the modes. Basically, when one queries the changeableValues dict, you get a mode=Auto, as well as a heatCoolMode, which is set to either Heat, Cool, Off. Per the documentation, heatCoolMode contains the "heat cool mode when system switch is in Auto mode". It would make sense that when changing the thermostat settings, mode=Auto should be valid, but it's not. The way the API understands that the mode should be set to Auto when changing the thermostat settings is by setting the autoChangeoverActive variable to true, not the mode itself. This require changes in the async_set_hvac_mode, async_set_temperature, and async_set_preset_mode functions. Related to this issue, I got rid of the references to hasDualSetpointStatus, as it seems that it always remains false in the API, even when the mode is set to auto, so again, the key variable for this is autoChangeoverActive. While I was working on this I also noticed another issue. The support flag SUPPORT_TARGET_TEMPERATURE_RANGE had not been included, which did not allow for the temperature range to be available, thus invalidating the target_temperature_low and target_temperature_high functions. I added this flag and sorted out which set point (heat vs cool) should be called for each of them so things work as expected in Lovelace. I have tested all of these functionalities and they all work great on my end, so I thought I'd share. * Update climate.py * Update climate.py Fixed two additional issues: 1) When the system is turned off from Auto, the heatCoolMode variable becomes 'Off', so when you try to restart the system back to Auto, nothing happens. 2) I now prevent the async_set_temperature function from being called with a new set point when the system is Off. All changes tested and functional. * Update climate.py * Update climate.py Return SUPPORT_PRESET_MODE flag only for LCC models (i.e. they have the "thermostatSetpointStatus" variable defined). TCC models do not support this feature * Update climate.py After playing with the official Honeywell API, I realized it doesn't like to received commands with missing data, i.e., it always wants to get a mode, coolSetpoint, heatSetpoint, and autoChangeoverActive variables. This was causing some random issues with changing modes, especially from coming from off, so I modified the async_set_temperature, and async_set_hvac_mode fuctions to always send all pertinent variables. * Update climate.py * Update climate.py * Update climate.py * Update climate.py * Clean code and test everything Alright, sorry for the multiple commits, fixing this properly took a fair bit of testing. I went ahead and cleaned up the code and made the following big picture changes: 1) The integration now supports the Auto mode appropriately, to include the temperature range. 2) There's a bug that actually manifests when using the native app. When the system is 'Off' and you try to turn it on to 'Auto', it will turn on briefly but will go back to 'Off' after a few seconds. When checking the web api, this appears to be related to the fact that the heatCoolMode variable seems to continue to store 'Off', even if the mode accurately displays 'Auto', and the autoChangeoverActive=True. So to overcome that inherent limitation, when the system is 'Off' and the user turns it to 'Auto', I first turn it to Heat, wait 3 seconds, and then turn it to 'Auto', which seems to work well. * Update climate.py * Fixed errors * Fixed comments that were resulting in error. * Update climate.py * Update homeassistant/components/lyric/climate.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/lyric/climate.py Co-authored-by: Martin Hjelmare * Update climate.py I removed a blank line in 268 and another one at the end of the document. I also fixed the outdents of await commands after the _LOGGER.error calls, not sure what else may be driving the flake8 and black errors. Any guidance is much appreciated @MartinHjelmare * Update climate.py * Update climate.py corrected some indents that I think were the culprit of the flake8 errors * Update climate.py I used VS Code to fix locate the flake8 errors. I ran black on it, so I'm hoping that will fix the last lingering black error. Co-authored-by: Martin Hjelmare --- homeassistant/components/lyric/climate.py | 94 ++++++++++++++++++----- 1 file changed, 76 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index e798aca5831..dfe88048ba4 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -1,6 +1,7 @@ """Support for Honeywell Lyric climate platform.""" from __future__ import annotations +import asyncio import logging from time import localtime, strftime, time @@ -22,6 +23,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE @@ -45,7 +47,11 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE +# Only LCC models support presets +SUPPORT_FLAGS_LCC = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE_RANGE +) +SUPPORT_FLAGS_TCC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE LYRIC_HVAC_ACTION_OFF = "EquipmentOff" LYRIC_HVAC_ACTION_HEAT = "Heat" @@ -166,7 +172,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - return SUPPORT_FLAGS + if self.device.changeableValues.thermostatSetpointStatus: + support_flags = SUPPORT_FLAGS_LCC + else: + support_flags = SUPPORT_FLAGS_TCC + return support_flags @property def temperature_unit(self) -> str: @@ -200,25 +210,28 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" device = self.device - if not device.hasDualSetpointStatus: + if ( + not device.changeableValues.autoChangeoverActive + and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF + ): if self.hvac_mode == HVAC_MODE_COOL: return device.changeableValues.coolSetpoint return device.changeableValues.heatSetpoint return None @property - def target_temperature_low(self) -> float | None: - """Return the upper bound temperature we try to reach.""" + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach.""" device = self.device - if device.hasDualSetpointStatus: + if device.changeableValues.autoChangeoverActive: return device.changeableValues.coolSetpoint return None @property - def target_temperature_high(self) -> float | None: - """Return the upper bound temperature we try to reach.""" + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach.""" device = self.device - if device.hasDualSetpointStatus: + if device.changeableValues.autoChangeoverActive: return device.changeableValues.heatSetpoint return None @@ -256,11 +269,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" + device = self.device target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - device = self.device - if device.hasDualSetpointStatus: + if device.changeableValues.autoChangeoverActive: if target_temp_low is None or target_temp_high is None: raise HomeAssistantError( "Could not find target_temp_low and/or target_temp_high in arguments" @@ -270,11 +283,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): await self._update_thermostat( self.location, device, - coolSetpoint=target_temp_low, - heatSetpoint=target_temp_high, + coolSetpoint=target_temp_high, + heatSetpoint=target_temp_low, + mode=HVAC_MODES[device.changeableValues.heatCoolMode], ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) + await self.coordinator.async_refresh() else: temp = kwargs.get(ATTR_TEMPERATURE) _LOGGER.debug("Set temperature: %s", temp) @@ -289,15 +304,58 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) - await self.coordinator.async_refresh() + await self.coordinator.async_refresh() async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" - _LOGGER.debug("Set hvac mode: %s", hvac_mode) + _LOGGER.debug("HVAC mode: %s", hvac_mode) try: - await self._update_thermostat( - self.location, self.device, mode=LYRIC_HVAC_MODES[hvac_mode] - ) + if LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL: + # If the system is off, turn it to Heat first then to Auto, otherwise it turns to + # Auto briefly and then reverts to Off (perhaps related to heatCoolMode). This is the + # behavior that happens with the native app as well, so likely a bug in the api itself + + if HVAC_MODES[self.device.changeableValues.mode] == HVAC_MODE_OFF: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_COOL], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=False, + ) + # Sleep 3 seconds before proceeding + await asyncio.sleep(3) + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + ) + await self._update_thermostat( + self.location, + self.device, + mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT], + autoChangeoverActive=True, + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", + HVAC_MODES[self.device.changeableValues.mode], + ) + await self._update_thermostat( + self.location, self.device, autoChangeoverActive=True + ) + else: + _LOGGER.debug( + "HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode] + ) + await self._update_thermostat( + self.location, + self.device, + mode=LYRIC_HVAC_MODES[hvac_mode], + autoChangeoverActive=False, + ) except LYRIC_EXCEPTIONS as exception: _LOGGER.error(exception) await self.coordinator.async_refresh() From fabcdf74983f8eeb25ba1151bd1fec82c2d05b74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Mar 2022 11:06:13 +0100 Subject: [PATCH 0456/1054] Update flake8-comprehensions to 3.8.0 (#68164) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ddef6569ae4..07c0f4295ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - - flake8-comprehensions==3.7.0 + - flake8-comprehensions==3.8.0 - flake8-noqa==1.2.1 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 802ca899bf6..f14739e1f04 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,7 +3,7 @@ bandit==1.7.4 black==22.1.0 codespell==2.1.0 -flake8-comprehensions==3.7.0 +flake8-comprehensions==3.8.0 flake8-docstrings==1.6.0 flake8-noqa==1.2.1 flake8==4.0.1 From f7cb10e2f53f328db29df617f7ccfdcd481dd5f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Mar 2022 12:05:15 +0100 Subject: [PATCH 0457/1054] Update twentemilieu to 0.6.0 (#68171) --- homeassistant/components/twentemilieu/__init__.py | 2 +- homeassistant/components/twentemilieu/diagnostics.py | 4 ++-- homeassistant/components/twentemilieu/manifest.json | 2 +- homeassistant/components/twentemilieu/sensor.py | 7 ++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/twentemilieu/conftest.py | 10 +++++----- tests/components/twentemilieu/test_diagnostics.py | 10 +++++----- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index 576ab2068a7..cee6ffbf38e 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator: DataUpdateCoordinator[ - dict[WasteType, date | None] + dict[WasteType, list[date]] ] = DataUpdateCoordinator( hass, LOGGER, diff --git a/homeassistant/components/twentemilieu/diagnostics.py b/homeassistant/components/twentemilieu/diagnostics.py index 0d63fdbcf2c..a158ead0909 100644 --- a/homeassistant/components/twentemilieu/diagnostics.py +++ b/homeassistant/components/twentemilieu/diagnostics.py @@ -17,6 +17,6 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.data[CONF_ID]] return { - waste_type: waste_date.isoformat() if waste_date else None - for waste_type, waste_date in coordinator.data.items() + waste_type: [waste_date.isoformat() for waste_date in waste_dates] + for waste_type, waste_dates in coordinator.data.items() } diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index d0b94efe289..e8e280dcd3d 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -3,7 +3,7 @@ "name": "Twente Milieu", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": ["twentemilieu==0.5.0"], + "requirements": ["twentemilieu==0.6.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index f25b84ace15..a56523d53d0 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -90,11 +90,10 @@ async def async_setup_entry( ) -class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): +class TwenteMilieuSensor(CoordinatorEntity[dict[WasteType, list[date]]], SensorEntity): """Defines a Twente Milieu sensor.""" entity_description: TwenteMilieuSensorDescription - coordinator: DataUpdateCoordinator[dict[WasteType, date | None]] def __init__( self, @@ -117,4 +116,6 @@ class TwenteMilieuSensor(CoordinatorEntity, SensorEntity): @property def native_value(self) -> date | None: """Return the state of the sensor.""" - return self.coordinator.data.get(self.entity_description.waste_type) + if not (dates := self.coordinator.data[self.entity_description.waste_type]): + return None + return dates[0] diff --git a/requirements_all.txt b/requirements_all.txt index fc930536b83..5b63119186d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2315,7 +2315,7 @@ ttls==1.4.3 tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu -twentemilieu==0.5.0 +twentemilieu==0.6.0 # homeassistant.components.twilio twilio==6.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f39d58d637..0a13b5c2686 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1468,7 +1468,7 @@ ttls==1.4.3 tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu -twentemilieu==0.5.0 +twentemilieu==0.6.0 # homeassistant.components.twilio twilio==6.32.0 diff --git a/tests/components/twentemilieu/conftest.py b/tests/components/twentemilieu/conftest.py index 530e9723251..6e58ba2db48 100644 --- a/tests/components/twentemilieu/conftest.py +++ b/tests/components/twentemilieu/conftest.py @@ -65,11 +65,11 @@ def mock_twentemilieu() -> Generator[None, MagicMock, None]: twentemilieu = twentemilieu_mock.return_value twentemilieu.unique_id.return_value = 12345 twentemilieu.update.return_value = { - WasteType.NON_RECYCLABLE: date(2021, 11, 1), - WasteType.ORGANIC: date(2021, 11, 2), - WasteType.PACKAGES: date(2021, 11, 3), - WasteType.PAPER: None, - WasteType.TREE: date(2022, 1, 6), + WasteType.NON_RECYCLABLE: [date(2021, 11, 1), date(2021, 12, 1)], + WasteType.ORGANIC: [date(2021, 11, 2)], + WasteType.PACKAGES: [date(2021, 11, 3)], + WasteType.PAPER: [], + WasteType.TREE: [date(2022, 1, 6)], } yield twentemilieu diff --git a/tests/components/twentemilieu/test_diagnostics.py b/tests/components/twentemilieu/test_diagnostics.py index 2f5ffcd5eb9..efbe2fc3104 100644 --- a/tests/components/twentemilieu/test_diagnostics.py +++ b/tests/components/twentemilieu/test_diagnostics.py @@ -16,9 +16,9 @@ async def test_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { - "0": "2021-11-01", - "1": "2021-11-02", - "2": None, - "6": "2022-01-06", - "10": "2021-11-03", + "0": ["2021-11-01", "2021-12-01"], + "1": ["2021-11-02"], + "2": [], + "6": ["2022-01-06"], + "10": ["2021-11-03"], } From 1a270257936e4dafb8b7ac8d667757b49661df0a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 12:46:58 +0100 Subject: [PATCH 0458/1054] Prevent spawning script runs when shutting down (#68170) --- homeassistant/helpers/script.py | 8 ++++++++ tests/helpers/test_script.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 07a89c8cddb..1de5600f208 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -110,6 +110,7 @@ ATTR_MAX = "max" DATA_SCRIPTS = "helpers.script" DATA_SCRIPT_BREAKPOINTS = "helpers.script_breakpoints" +DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED = "helpers.script_not_allowed" RUN_ID_ANY = "*" NODE_ANY = "*" @@ -883,6 +884,7 @@ class _QueuedScriptRun(_ScriptRun): async def _async_stop_scripts_after_shutdown(hass, point_in_time): """Stop running Script objects started after shutdown.""" + hass.data[DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED] = None running_scripts = [ script for script in hass.data[DATA_SCRIPTS] if script["instance"].is_running ] @@ -1192,6 +1194,12 @@ class Script: ) context = Context() + # Prevent spawning new script runs when Home Assistant is shutting down + if DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED in self._hass.data: + self._log("Home Assistant is shutting down, starting script blocked") + return + + # Prevent spawning new script runs if not allowed by script mode if self.is_running: if self.script_mode == SCRIPT_MODE_SINGLE: if self._max_exceeded != "SILENT": diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 11ba9810b9d..03a91ce1261 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3276,6 +3276,26 @@ async def test_shutdown_after(hass, caplog): assert_action_trace(expected_trace) +async def test_start_script_after_shutdown(hass, caplog): + """Test starting scripts after shutdown is blocked.""" + delay_alias = "delay step" + sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 120}, "alias": delay_alias}) + script_obj = script.Script(hass, sequence, "test script", "test_domain") + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await hass.async_block_till_done() + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + # Attempt to spawn additional script run + await script_obj.async_run(context=Context()) + assert not script_obj.is_running + assert "Home Assistant is shutting down, starting script blocked" in caplog.text + + async def test_update_logger(hass, caplog): """Test updating logger.""" sequence = cv.SCRIPT_SCHEMA({"event": "test_event"}) From db4b69663f20f55e9c07703a0c0d4f80052577bc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:53:40 +0100 Subject: [PATCH 0459/1054] Fix met TypeVar usage (#68152) * Fix met TypeVar usage * Change format_condition --- homeassistant/components/met/__init__.py | 8 ++++---- homeassistant/components/met/weather.py | 17 +++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index a66c974f415..536cf03bde2 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -82,12 +82,12 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -class MetDataUpdateCoordinator(DataUpdateCoordinator): +class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]): """Class to manage fetching Met data.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize global Met data updater.""" - self._unsub_track_home: Callable | None = None + self._unsub_track_home: Callable[[], None] | None = None self.weather = MetWeatherData( hass, config_entry.data, hass.config.units.is_metric ) @@ -137,8 +137,8 @@ class MetWeatherData: self._is_metric = is_metric self._weather_data: metno.MetWeatherData self.current_weather_data: dict = {} - self.daily_forecast = None - self.hourly_forecast = None + self.daily_forecast: list[dict] = [] + self.hourly_forecast: list[dict] = [] self._coordinates: dict[str, str] | None = None def set_coordinates(self) -> bool: diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 53c372030f1..5791620d5ac 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -40,15 +40,12 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - T, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed +from . import MetDataUpdateCoordinator, MetWeatherData from .const import ( ATTR_FORECAST_PRECIPITATION, ATTR_MAP, @@ -109,7 +106,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add a weather entity from a config_entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: MetDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ MetWeather( @@ -130,12 +127,14 @@ def format_condition(condition: str) -> str: return condition -class MetWeather(CoordinatorEntity, WeatherEntity): +class MetWeather(CoordinatorEntity[MetWeatherData], WeatherEntity): """Implementation of a Met.no weather condition.""" + coordinator: MetDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator[T], + coordinator: MetDataUpdateCoordinator, config: MappingProxyType[str, Any], is_metric: bool, hourly: bool, @@ -187,6 +186,8 @@ class MetWeather(CoordinatorEntity, WeatherEntity): def condition(self) -> str | None: """Return the current condition.""" condition = self.coordinator.data.current_weather_data.get("condition") + if condition is None: + return None return format_condition(condition) @property From aabf46b1b35eeb9132c34eb87b1e009c6e0b2ad8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:56:08 +0100 Subject: [PATCH 0460/1054] Fix point by adding authlib constraint (#68176) * Fix point by pinning authlib * Use constraint --- homeassistant/components/point/__init__.py | 2 ++ homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 288c16df14a..99108323187 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -97,6 +97,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: token_saver=token_saver, ) try: + # pylint: disable-next=fixme + # TODO Remove authlib constraint when refactoring this code await session.ensure_active_token() except ConnectTimeout as err: _LOGGER.debug("Connection Timeout") diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e50f177ffc..5888154207c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -95,3 +95,7 @@ python-socketio>=4.6.0,<5.0 # Constrain multidict to avoid typing issues # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 + +# Required for compatibility with point integration - ensure_active_token +# https://github.com/home-assistant/core/pull/68176 +authlib<1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c95d470fdeb..9d5127a626b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -115,6 +115,10 @@ python-socketio>=4.6.0,<5.0 # Constrain multidict to avoid typing issues # https://github.com/home-assistant/core/pull/67046 multidict>=6.0.2 + +# Required for compatibility with point integration - ensure_active_token +# https://github.com/home-assistant/core/pull/68176 +authlib<1.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From cdd23abea7f8854d95c22a2dadeaffdbceeccc55 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:57:36 +0100 Subject: [PATCH 0461/1054] Add missing await [velbus] (#68153) --- homeassistant/components/velbus/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 2e2ceb761a9..4afe02f4637 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -78,4 +78,4 @@ class VelbusCover(VelbusEntity, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self._channel.set_position(100 - kwargs[ATTR_POSITION]) + await self._channel.set_position(100 - kwargs[ATTR_POSITION]) From 8ea31cea3ad151d8997ea87d279517702bbf0e8c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 16:29:04 +0100 Subject: [PATCH 0462/1054] Fix deadlock when stopping queued script (#68175) --- homeassistant/helpers/script.py | 5 +++-- tests/helpers/test_script.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1de5600f208..b7e7c05478c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -860,12 +860,13 @@ class _QueuedScriptRun(_ScriptRun): {lock_task, stop_task}, return_when=asyncio.FIRST_COMPLETED ) except asyncio.CancelledError: - lock_task.cancel() self._finish() raise + else: + self.lock_acquired = lock_task.done() and not lock_task.cancelled() finally: + lock_task.cancel() stop_task.cancel() - self.lock_acquired = lock_task.done() and not lock_task.cancelled() # If we've been told to stop, then just finish up. Otherwise, we've acquired the # lock so we can go ahead and start the run. diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 03a91ce1261..dc1a498e465 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3203,6 +3203,37 @@ async def test_script_mode_queued_cancel(hass): raise +async def test_script_mode_queued_stop(hass): + """Test stopping with a queued run.""" + script_obj = script.Script( + hass, + cv.SCRIPT_SCHEMA({"wait_template": "{{ false }}"}), + "Test Name", + "test_domain", + script_mode="queued", + max_runs=3, + ) + wait_started_flag = async_watch_for_action(script_obj, "wait") + + assert not script_obj.is_running + assert script_obj.runs == 0 + + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.sleep(0) + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.sleep(0) + + assert script_obj.is_running + assert script_obj.runs == 3 + + await script_obj.async_stop() + + assert not script_obj.is_running + assert script_obj.runs == 0 + + async def test_script_logging(hass, caplog): """Test script logging.""" script_obj = script.Script(hass, [], "Script with % Name", "test_domain") From 34eb4aa2d0efc8410c66828f3e40ac00ddfdf0ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 16:42:22 +0100 Subject: [PATCH 0463/1054] Exclude hidden entities from targets (#68149) --- homeassistant/helpers/service.py | 14 +++++++------- tests/helpers/test_service.py | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 82c98c37b84..da562fcded6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -359,7 +359,7 @@ def async_extract_referenced_entity_ids( if area_id not in area_reg.areas: selected.missing_areas.add(area_id) - # Find devices for this area + # Find devices for targeted areas selected.referenced_devices.update(selector.device_ids) for device_entry in dev_reg.devices.values(): if device_entry.area_id in selector.area_ids: @@ -369,20 +369,20 @@ def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): - # Do not add config or diagnostic entities referenced by areas or devices - - if ent_entry.entity_category is not None: + # Do not add entities which are hidden or which are config or diagnostic entities + if ent_entry.entity_category is not None or ent_entry.hidden_by is not None: continue if ( - # when area matches the target area + # The entity's area matches a targeted area ent_entry.area_id in selector.area_ids - # when device matches a referenced devices with no explicitly set area + # The entity's device matches a device referenced by an area and the entity + # has no explicitly set area or ( not ent_entry.area_id and ent_entry.device_id in selected.referenced_devices ) - # when device matches target device + # The entity's device matches a targeted device or ent_entry.device_id in selector.device_ids ): selected.indirectly_referenced.add(ent_entry.entity_id) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 54507d9b3bd..bbf4a72430c 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -121,6 +121,13 @@ def area_mock(hass): area_id="own-area", entity_category="config", ) + hidden_entity_in_own_area = ent_reg.RegistryEntry( + entity_id="light.hidden_in_own_area", + unique_id="hidden-in-own-area-id", + platform="test", + area_id="own-area", + hidden_by=ent_reg.RegistryEntryHider.USER, + ) entity_in_area = ent_reg.RegistryEntry( entity_id="light.in_area", unique_id="in-area-id", @@ -134,6 +141,13 @@ def area_mock(hass): device_id=device_in_area.id, entity_category="config", ) + hidden_entity_in_area = ent_reg.RegistryEntry( + entity_id="light.hidden_in_area", + unique_id="hidden-in-area-id", + platform="test", + device_id=device_in_area.id, + hidden_by=ent_reg.RegistryEntryHider.USER, + ) entity_in_other_area = ent_reg.RegistryEntry( entity_id="light.in_other_area", unique_id="in-area-a-id", @@ -161,6 +175,13 @@ def area_mock(hass): device_id=device_no_area.id, entity_category="config", ) + hidden_entity_no_area = ent_reg.RegistryEntry( + entity_id="light.hidden_no_area", + unique_id="hidden-no-area-id", + platform="test", + device_id=device_no_area.id, + hidden_by=ent_reg.RegistryEntryHider.USER, + ) entity_diff_area = ent_reg.RegistryEntry( entity_id="light.diff_area", unique_id="diff-area-id", @@ -186,12 +207,15 @@ def area_mock(hass): { entity_in_own_area.entity_id: entity_in_own_area, config_entity_in_own_area.entity_id: config_entity_in_own_area, + hidden_entity_in_own_area.entity_id: hidden_entity_in_own_area, entity_in_area.entity_id: entity_in_area, config_entity_in_area.entity_id: config_entity_in_area, + hidden_entity_in_area.entity_id: hidden_entity_in_area, entity_in_other_area.entity_id: entity_in_other_area, entity_assigned_to_area.entity_id: entity_assigned_to_area, entity_no_area.entity_id: entity_no_area, config_entity_no_area.entity_id: config_entity_no_area, + hidden_entity_no_area.entity_id: hidden_entity_no_area, entity_diff_area.entity_id: entity_diff_area, entity_in_area_a.entity_id: entity_in_area_a, entity_in_area_b.entity_id: entity_in_area_b, From d360ac91cad0dcc1823c7112a4f4b5cd3875cff5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 17:23:52 +0100 Subject: [PATCH 0464/1054] Disable recorder nightly jobs in tests (#68188) --- tests/components/recorder/test_init.py | 2 ++ tests/conftest.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 2bc0109e1b5..dc7881cfb42 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -600,6 +600,7 @@ def run_tasks_at_time(hass, test_time): hass.data[DATA_INSTANCE].block_till_done() +@pytest.mark.parametrize("enable_nightly_purge", [True]) def test_auto_purge(hass_recorder): """Test periodic purge scheduling.""" hass = hass_recorder() @@ -657,6 +658,7 @@ def test_auto_purge(hass_recorder): dt_util.set_default_time_zone(original_tz) +@pytest.mark.parametrize("enable_nightly_purge", [True]) def test_auto_purge_disabled(hass_recorder): """Test periodic db cleanup still run when auto purge is disabled.""" hass = hass_recorder({CONF_AUTO_PURGE: False}) diff --git a/tests/conftest.py b/tests/conftest.py index baac9ac19ee..902fc55eac4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -778,14 +778,29 @@ def enable_statistics(): @pytest.fixture -def hass_recorder(enable_statistics, hass_storage): +def enable_nightly_purge(): + """Fixture to control enabling of recorder's nightly purge job. + + To enable nightly purgin, tests can be marked with: + @pytest.mark.parametrize("enable_nightly_purge", [True]) + """ + return False + + +@pytest.fixture +def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage): """Home Assistant fixture with in-memory recorder.""" hass = get_test_home_assistant() stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None with patch( "homeassistant.components.recorder.Recorder.async_periodic_statistics", side_effect=stats, autospec=True, + ), patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, ): def setup_recorder(config=None): From c5a8e9d59cb19870842091eabf28a63c131ef644 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Mar 2022 18:14:07 +0100 Subject: [PATCH 0465/1054] Update opensensemap-api to 0.2.0 (#68193) --- homeassistant/components/opensensemap/air_quality.py | 2 +- homeassistant/components/opensensemap/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index b8028431796..5999eb91580 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -43,7 +43,7 @@ async def async_setup_platform( station_id = config[CONF_STATION_ID] session = async_get_clientsession(hass) - osm_api = OpenSenseMapData(OpenSenseMap(station_id, hass.loop, session)) + osm_api = OpenSenseMapData(OpenSenseMap(station_id, session)) await osm_api.async_update() diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 513cb5ac3da..baf62985448 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -2,7 +2,7 @@ "domain": "opensensemap", "name": "openSenseMap", "documentation": "https://www.home-assistant.io/integrations/opensensemap", - "requirements": ["opensensemap-api==0.1.5"], + "requirements": ["opensensemap-api==0.2.0"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["opensensemap_api"] diff --git a/requirements_all.txt b/requirements_all.txt index 5b63119186d..0d3f2f1b27a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1138,7 +1138,7 @@ openevsewifi==1.1.0 openhomedevice==2.0.1 # homeassistant.components.opensensemap -opensensemap-api==0.1.5 +opensensemap-api==0.2.0 # homeassistant.components.enigma2 openwebifpy==3.2.7 From 668e0c19a2728e4ae6afa25fc8a330317418b78f Mon Sep 17 00:00:00 2001 From: leranp Date: Tue, 15 Mar 2022 19:23:39 +0200 Subject: [PATCH 0466/1054] Bump gTTS to 2.2.4 (#68180) * Update gTTS to 2.2.4 * ADD Hebrew language * Update requirements_all.txt * Update requirements_test_all.txt Co-authored-by: Shay Levy --- homeassistant/components/google_translate/manifest.json | 2 +- homeassistant/components/google_translate/tts.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 70f5e129950..30c959c4a01 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -2,7 +2,7 @@ "domain": "google_translate", "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": ["gTTS==2.2.3"], + "requirements": ["gTTS==2.2.4"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["gtts"] diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 9f6ca3a88b8..eb6faf22bbc 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -35,6 +35,7 @@ SUPPORT_LANGUAGES = [ "id", "is", "it", + "iw", "ja", "jw", "km", diff --git a/requirements_all.txt b/requirements_all.txt index 0d3f2f1b27a..db676ad9ed1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,7 +670,7 @@ freesms==0.2.0 fritzconnection==1.8.0 # homeassistant.components.google_translate -gTTS==2.2.3 +gTTS==2.2.4 # homeassistant.components.garages_amsterdam garages-amsterdam==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a13b5c2686..5e7d0c25929 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -456,7 +456,7 @@ freebox-api==0.0.10 fritzconnection==1.8.0 # homeassistant.components.google_translate -gTTS==2.2.3 +gTTS==2.2.4 # homeassistant.components.garages_amsterdam garages-amsterdam==3.0.0 From 0c4efae31f617f3b60f22588a7963f7754222d9d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 15 Mar 2022 13:33:16 -0400 Subject: [PATCH 0467/1054] Clean up twitch (#67595) --- homeassistant/components/twitch/sensor.py | 118 +++++++--------------- 1 file changed, 39 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 771f88f0ef1..95006a4cab7 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -62,13 +62,16 @@ def setup_platform( client_id = config[CONF_CLIENT_ID] client_secret = config[CONF_CLIENT_SECRET] oauth_token = config.get(CONF_TOKEN) - client = Twitch(app_id=client_id, app_secret=client_secret) - client.auto_refresh_auth = False try: - client.authenticate_app(scope=OAUTH_SCOPES) + client = Twitch( + app_id=client_id, + app_secret=client_secret, + target_app_auth_scope=OAUTH_SCOPES, + ) + client.auto_refresh_auth = False except TwitchAuthorizationException: - _LOGGER.error("INvalid client ID or client secret") + _LOGGER.error("Invalid client ID or client secret") return if oauth_token: @@ -86,7 +89,7 @@ def setup_platform( channels = client.get_users(logins=channels) add_entities( - [TwitchSensor(channel=channel, client=client) for channel in channels["data"]], + [TwitchSensor(channel, client) for channel in channels["data"]], True, ) @@ -94,80 +97,38 @@ def setup_platform( class TwitchSensor(SensorEntity): """Representation of an Twitch channel.""" - def __init__(self, channel, client: Twitch): + _attr_icon = ICON + + def __init__(self, channel: dict[str, str], client: Twitch) -> None: """Initialize the sensor.""" self._client = client - self._channel = channel self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES) - self._state = None - self._preview = None - self._game = None - self._title = None - self._subscription = None - self._follow = None - self._statistics = None + self._attr_name = channel["display_name"] + self._attr_unique_id = channel["id"] - @property - def name(self): - """Return the name of the sensor.""" - return self._channel["display_name"] - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def entity_picture(self): - """Return preview of current game.""" - return self._preview - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attr = dict(self._statistics) - - if self._enable_user_auth: - attr.update(self._subscription) - attr.update(self._follow) - - if self._state == STATE_STREAMING: - attr.update({ATTR_GAME: self._game, ATTR_TITLE: self._title}) - return attr - - @property - def unique_id(self): - """Return unique ID for this sensor.""" - return self._channel["id"] - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - def update(self): + def update(self) -> None: """Update device state.""" - followers = self._client.get_users_follows(to_id=self._channel["id"])["total"] - channel = self._client.get_users(user_ids=[self._channel["id"]])["data"][0] + followers = self._client.get_users_follows(to_id=self.unique_id)["total"] + channel = self._client.get_users(user_ids=[self.unique_id])["data"][0] - self._statistics = { + self._attr_extra_state_attributes = { ATTR_FOLLOWING: followers, ATTR_VIEWS: channel["view_count"], } if self._enable_user_auth: - user = self._client.get_users()["data"][0] + user = self._client.get_users()["data"][0]["id"] subs = self._client.check_user_subscription( - user_id=user["id"], broadcaster_id=self._channel["id"] + user_id=user, broadcaster_id=self.unique_id ) if "data" in subs: - self._subscription = { - ATTR_SUBSCRIPTION: True, - ATTR_SUBSCRIPTION_GIFTED: subs["data"][0]["is_gift"], - } + self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = True + self._attr_extra_state_attributes[ATTR_SUBSCRIPTION_GIFTED] = subs[ + "data" + ][0]["is_gift"] elif "status" in subs and subs["status"] == 404: - self._subscription = {ATTR_SUBSCRIPTION: False} + self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = False elif "error" in subs: raise Exception( f"Error response on check_user_subscription: {subs['error']}" @@ -176,23 +137,22 @@ class TwitchSensor(SensorEntity): raise Exception("Unknown error response on check_user_subscription") follows = self._client.get_users_follows( - from_id=user["id"], to_id=self._channel["id"] + from_id=user, to_id=self.unique_id )["data"] - if len(follows) > 0: - self._follow = { - ATTR_FOLLOW: True, - ATTR_FOLLOW_SINCE: follows[0]["followed_at"], - } - else: - self._follow = {ATTR_FOLLOW: False} + self._attr_extra_state_attributes[ATTR_FOLLOW] = len(follows) > 0 + if len(follows): + self._attr_extra_state_attributes[ATTR_FOLLOW_SINCE] = follows[0][ + "followed_at" + ] - streams = self._client.get_streams(user_id=[self._channel["id"]])["data"] - if len(streams) > 0: + if streams := self._client.get_streams(user_id=[self.unique_id])["data"]: stream = streams[0] - self._game = stream["game_name"] - self._title = stream["title"] - self._preview = stream["thumbnail_url"] - self._state = STATE_STREAMING + self._attr_native_value = STATE_STREAMING + self._attr_extra_state_attributes[ATTR_GAME] = stream["game_name"] + self._attr_extra_state_attributes[ATTR_TITLE] = stream["title"] + self._attr_entity_picture = stream["thumbnail_url"] else: - self._preview = channel["offline_image_url"] - self._state = STATE_OFFLINE + self._attr_native_value = STATE_OFFLINE + self._attr_extra_state_attributes[ATTR_GAME] = None + self._attr_extra_state_attributes[ATTR_TITLE] = None + self._attr_entity_picture = channel["offline_image_url"] From f026245cb43c17c2072d6a53e5853c7d0eaf2bdd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 18:34:46 +0100 Subject: [PATCH 0468/1054] Small tweaks of switch_as_x tests (#68195) --- .../switch_as_x/test_config_flow.py | 35 +++--- tests/components/switch_as_x/test_init.py | 115 +++++++----------- 2 files changed, 62 insertions(+), 88 deletions(-) diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 6859dda9073..dc4ca96aa97 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -12,8 +12,18 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from homeassistant.helpers import entity_registry as er +from tests.common import MockConfigEntry -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +PLATFORMS_TO_TEST = ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.SIREN, +) + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_flow( hass: HomeAssistant, target_domain: Platform, @@ -59,7 +69,7 @@ async def test_config_flow( (None, er.RegistryEntryHider.INTEGRATION.value), ), ) -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_flow_registered_entity( hass: HomeAssistant, target_domain: Platform, @@ -110,29 +120,26 @@ async def test_config_flow_registered_entity( assert switch_entity_entry.hidden_by == hidden_by_after -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_options( hass: HomeAssistant, target_domain: Platform, mock_setup_entry: AsyncMock, ) -> None: """Test reconfiguring.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ CONF_ENTITY_ID: "switch.ceiling", CONF_TARGET_DOMAIN: target_domain, }, + title="ABC", ) - await hass.async_block_till_done() + switch_as_x_config_entry.add_to_hass(hass) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index f5c3e7c5653..e2b875b813b 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -6,23 +6,31 @@ from unittest.mock import patch import pytest from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN -from homeassistant.const import CONF_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + CONF_ENTITY_ID, + STATE_CLOSED, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNLOCKED, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry - -@pytest.mark.parametrize( - "target_domain", - ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - ), +PLATFORMS_TO_TEST = ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.SIREN, ) + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_unregistered_uuid( hass: HomeAssistant, target_domain: str ) -> None: @@ -48,19 +56,23 @@ async def test_config_entry_unregistered_uuid( @pytest.mark.parametrize( - "target_domain", + "target_domain,state_on,state_off", ( - Platform.FAN, - Platform.LIGHT, - Platform.SIREN, + (Platform.COVER, STATE_OPEN, STATE_CLOSED), + (Platform.FAN, STATE_ON, STATE_OFF), + (Platform.LIGHT, STATE_ON, STATE_OFF), + (Platform.LOCK, STATE_UNLOCKED, STATE_LOCKED), + (Platform.SIREN, STATE_ON, STATE_OFF), ), ) -async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) -> None: +async def test_entity_registry_events( + hass: HomeAssistant, target_domain: str, state_on: str, state_off: str +) -> None: """Test entity registry events are tracked.""" registry = er.async_get(hass) registry_entry = registry.async_get_or_create("switch", "test", "unique") switch_entity_id = registry_entry.entity_id - hass.states.async_set(switch_entity_id, "on") + hass.states.async_set(switch_entity_id, STATE_ON) config_entry = MockConfigEntry( data={}, @@ -77,7 +89,7 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{target_domain}.abc").state == STATE_ON + assert hass.states.get(f"{target_domain}.abc").state == state_on # Change entity_id new_switch_entity_id = f"{switch_entity_id}_new" @@ -87,12 +99,12 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - # Check tracking the new entity_id await hass.async_block_till_done() - assert hass.states.get(f"{target_domain}.abc").state == STATE_OFF + assert hass.states.get(f"{target_domain}.abc").state == state_off # The old entity_id should no longer be tracked hass.states.async_set(switch_entity_id, STATE_ON) await hass.async_block_till_done() - assert hass.states.get(f"{target_domain}.abc").state == STATE_OFF + assert hass.states.get(f"{target_domain}.abc").state == state_off # Check changing name does not reload the config entry with patch( @@ -111,16 +123,7 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - assert len(hass.config_entries.async_entries("switch_as_x")) == 0 -@pytest.mark.parametrize( - "target_domain", - ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - ), -) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_device_registry_config_entry_1( hass: HomeAssistant, target_domain: str ) -> None: @@ -178,16 +181,7 @@ async def test_device_registry_config_entry_1( assert switch_as_x_config_entry.entry_id not in device_entry.config_entries -@pytest.mark.parametrize( - "target_domain", - ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - ), -) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_device_registry_config_entry_2( hass: HomeAssistant, target_domain: str ) -> None: @@ -238,16 +232,7 @@ async def test_device_registry_config_entry_2( assert switch_as_x_config_entry.entry_id not in device_entry.config_entries -@pytest.mark.parametrize( - "target_domain", - ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - ), -) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_entity_id( hass: HomeAssistant, target_domain: Platform ) -> None: @@ -282,16 +267,7 @@ async def test_config_entry_entity_id( assert entity_entry.unique_id == config_entry.entry_id -@pytest.mark.parametrize( - "target_domain", - ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - ), -) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: """Test light switch setup from config entry with entity registry id.""" registry = er.async_get(hass) @@ -315,16 +291,7 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - assert hass.states.get(f"{target_domain}.abc") -@pytest.mark.parametrize( - "target_domain", - ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - ), -) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: """Test the entity is added to the wrapped entity's device.""" device_registry = dr.async_get(hass) @@ -360,7 +327,7 @@ async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: assert entity_entry.device_id == switch_entity_entry.device_id -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_setup_and_remove_config_entry( hass: HomeAssistant, target_domain: Platform, @@ -391,8 +358,8 @@ async def test_setup_and_remove_config_entry( await hass.async_block_till_done() # Check the state and entity registry entry are removed - assert hass.states.get(f"{target_domain}.my_min_max") is None - assert registry.async_get(f"{target_domain}.my_min_max") is None + assert hass.states.get(f"{target_domain}.abc") is None + assert registry.async_get(f"{target_domain}.abc") is None @pytest.mark.parametrize( @@ -402,7 +369,7 @@ async def test_setup_and_remove_config_entry( (er.RegistryEntryHider.INTEGRATION.value, None), ), ) -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_reset_hidden_by( hass: HomeAssistant, target_domain: Platform, From b99934f62f572dfee36543c2e7ef2a2d6d0c0617 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 18:39:18 +0100 Subject: [PATCH 0469/1054] Small tweaks of group tests (#68196) --- tests/components/group/test_config_flow.py | 44 +++++---------- tests/components/group/test_init.py | 62 +++++++++++++++++++++- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index d720b051e53..cf0254768a3 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -8,6 +8,8 @@ from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from tests.common import MockConfigEntry + @pytest.mark.parametrize( "group_type,group_state,member_state,member_attributes,extra_input,extra_options,extra_attrs", @@ -122,46 +124,26 @@ async def test_options( for member in members2: hass.states.async_set(member, member_state, {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - - assert get_suggested(result["data_schema"].schema, "group_type") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"group_type": group_type}, - ) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == group_type - - assert get_suggested(result["data_schema"].schema, "entities") is None - assert get_suggested(result["data_schema"].schema, "name") is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "name": "Bed Room", + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, }, + title="Bed Room", ) + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY state = hass.states.get(f"{group_type}.bed_room") assert state.attributes["entity_id"] == members1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == {} - assert config_entry.options == { - "group_type": group_type, - "entities": members1, - "name": "Bed Room", - **extra_options, - } result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 4eb0e455422..19b9245dc7f 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,6 +1,9 @@ """The tests for the Group components.""" # pylint: disable=protected-access +from __future__ import annotations + from collections import OrderedDict +from typing import Any from unittest.mock import patch import pytest @@ -18,11 +21,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNKNOWN, ) -from homeassistant.core import CoreState +from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component from tests.components.group import common @@ -1358,3 +1362,57 @@ async def test_plant_group(hass): await hass.async_block_till_done() assert hass.states.get("group.plants").state == "problem" assert hass.states.get("group.plant_with_binary_sensors").state == "on" + + +@pytest.mark.parametrize( + "group_type,member_state,extra_options", + ( + ("binary_sensor", "on", {"all": False}), + ("cover", "open", {}), + ("fan", "on", {}), + ("light", "on", {}), + ("media_player", "on", {}), + ), +) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + group_type: str, + member_state: str, + extra_options: dict[str, Any], +) -> None: + """Test removing a config entry.""" + registry = er.async_get(hass) + + members1 = [f"{group_type}.one", f"{group_type}.two"] + + for member in members1: + hass.states.async_set(member, member_state, {}) + + # Setup the config entry + group_config_entry = MockConfigEntry( + data={}, + domain=group.DOMAIN, + options={ + "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are present + state = hass.states.get(f"{group_type}.bed_room") + assert state.attributes["entity_id"] == members1 + assert registry.async_get(f"{group_type}.bed_room") is not None + + # Remove the config entry + assert await hass.config_entries.async_remove(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(f"{group_type}.bed_room") is None + assert registry.async_get(f"{group_type}.bed_room") is None From 46f27fdefdf38fff885a64105310875813e8587b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 15 Mar 2022 18:48:54 +0100 Subject: [PATCH 0470/1054] Don't prevent automations from triggering themselves (#68178) --- .../components/automation/__init__.py | 5 + homeassistant/helpers/script.py | 2 +- tests/components/automation/test_init.py | 207 +++++++++++++++++- tests/components/script/test_init.py | 4 - 4 files changed, 212 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index a8d009ac2bb..3c9cd07a146 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -54,6 +54,7 @@ from homeassistant.helpers.script import ( CONF_MAX, CONF_MAX_EXCEEDED, Script, + script_stack_cv, ) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import ( @@ -505,6 +506,10 @@ class AutomationEntity(ToggleEntity, RestoreEntity): EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) + # Make a new empty script stack; automations are allowed + # to recursively trigger themselves + script_stack_cv.set([]) + try: with trace_path("action"): await self.action_script.async_run( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index b7e7c05478c..1ede1d10d89 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1247,7 +1247,7 @@ class Script: and id(self) in script_stack ): script_execution_set("disallowed_recursion_detected") - _LOGGER.warning("Disallowed recursion detected") + self._log("Disallowed recursion detected", level=logging.WARNING) return if self.script_mode != SCRIPT_MODE_QUEUED: diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b90c6a90819..1c90abe72ca 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,5 +1,6 @@ """The tests for the automation component.""" import asyncio +from datetime import timedelta import logging from unittest.mock import Mock, patch @@ -25,14 +26,30 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, CoreState, State, callback +from homeassistant.core import ( + Context, + CoreState, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.exceptions import HomeAssistantError, Unauthorized +from homeassistant.helpers.script import ( + SCRIPT_MODE_CHOICES, + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + _async_stop_scripts_at_shutdown, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_capture_events, + async_fire_time_changed, async_mock_service, mock_restore_cache, ) @@ -1570,3 +1587,191 @@ async def test_trigger_condition_explicit_id(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[-1].data.get("param") == "two" + + +@pytest.mark.parametrize( + "automation_mode,automation_runs", + ( + (SCRIPT_MODE_PARALLEL, 2), + (SCRIPT_MODE_QUEUED, 2), + (SCRIPT_MODE_RESTART, 2), + (SCRIPT_MODE_SINGLE, 1), + ), +) +@pytest.mark.parametrize( + "script_mode,script_warning_msg", + ( + (SCRIPT_MODE_PARALLEL, "script1: Maximum number of runs exceeded"), + (SCRIPT_MODE_QUEUED, "script1: Disallowed recursion detected"), + (SCRIPT_MODE_RESTART, "script1: Disallowed recursion detected"), + (SCRIPT_MODE_SINGLE, "script1: Already running"), + ), +) +async def test_recursive_automation_starting_script( + hass: HomeAssistant, + automation_mode, + automation_runs, + script_mode, + script_warning_msg, + caplog, +): + """Test starting automations does not interfere with script deadlock prevention.""" + + # Fail if additional script modes are added to + # make sure we cover all script modes in tests + assert SCRIPT_MODE_CHOICES == [ + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + ] + + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def mock_stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=mock_stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "mode": script_mode, + "sequence": [ + {"event": "trigger_automation"}, + { + "wait_template": f"{{{{ float(states('sensor.test'), 0) >= {automation_runs} }}}}" + }, + {"service": "script.script1"}, + {"service": "test.script_done"}, + ], + }, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "mode": automation_mode, + "trigger": [ + {"platform": "event", "event_type": "trigger_automation"}, + ], + "action": [ + {"service": "test.automation_started"}, + {"service": "script.script1"}, + ], + } + }, + ) + + script_done_event = asyncio.Event() + script_done = [] + automation_started = [] + automation_triggered = [] + + async def async_service_handler(service: ServiceCall): + if service.service == "automation_started": + automation_started.append(service) + elif service.service == "script_done": + script_done.append(service) + if len(script_done) == 1: + script_done_event.set() + + async def async_automation_triggered(event): + """Listen to automation_triggered event from the automation integration.""" + automation_triggered.append(event) + hass.states.async_set("sensor.test", str(len(automation_triggered))) + + hass.services.async_register("test", "script_done", async_service_handler) + hass.services.async_register( + "test", "automation_started", async_service_handler + ) + hass.bus.async_listen("automation_triggered", async_automation_triggered) + + hass.bus.async_fire("trigger_automation") + await asyncio.wait_for(script_done_event.wait(), 1) + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + + assert script_warning_msg in caplog.text + + +@pytest.mark.parametrize("automation_mode", SCRIPT_MODE_CHOICES) +async def test_recursive_automation(hass: HomeAssistant, automation_mode, caplog): + """Test automation triggering itself. + + - Illegal recursion detection should not be triggered + - Home Assistant should not hang on shut down + """ + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "mode": automation_mode, + "trigger": [ + {"platform": "event", "event_type": "trigger_automation"}, + ], + "action": [ + {"event": "trigger_automation"}, + {"service": "test.automation_done"}, + ], + } + }, + ) + + service_called = asyncio.Event() + service_called_late = [] + + async def async_service_handler(service): + if service.service == "automation_done": + service_called.set() + if service.service == "automation_started_late": + service_called_late.append(service) + + hass.services.async_register("test", "automation_done", async_service_handler) + hass.services.async_register( + "test", "automation_started_late", async_service_handler + ) + + hass.bus.async_fire("trigger_automation") + await asyncio.wait_for(service_called.wait(), 1) + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert "Disallowed recursion detected" not in caplog.text diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 35875c6da12..3bd179286a9 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -840,8 +840,6 @@ async def test_recursive_script(hass, script_mode, warning_msg, caplog): service_called.set() hass.services.async_register("test", "script", async_service_handler) - hass.states.async_set("input_boolean.test", "on") - hass.states.async_set("input_boolean.test2", "off") await hass.services.async_call("script", "script1") await asyncio.wait_for(service_called.wait(), 1) @@ -908,8 +906,6 @@ async def test_recursive_script_indirect(hass, script_mode, warning_msg, caplog) service_called.set() hass.services.async_register("test", "script", async_service_handler) - hass.states.async_set("input_boolean.test", "on") - hass.states.async_set("input_boolean.test2", "off") await hass.services.async_call("script", "script1") await asyncio.wait_for(service_called.wait(), 1) From 4bb52297a6979d3bf8b440ecb90a208ab0294884 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 15 Mar 2022 18:51:12 +0100 Subject: [PATCH 0471/1054] Replace hass helper calls in deCONZ device trigger tests (#68197) --- tests/components/deconz/test_device_trigger.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 91d8e0e1ea2..de6022fa56d 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -25,6 +25,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component @@ -72,7 +73,7 @@ async def test_get_triggers(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -171,7 +172,7 @@ async def test_get_triggers_for_alarm_event(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "00:00:00:00:00:00:00:00")} ) @@ -248,7 +249,7 @@ async def test_get_triggers_manage_unsupported_remotes(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -293,7 +294,7 @@ async def test_functional_device_trigger( with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")} ) @@ -370,7 +371,7 @@ async def test_validate_trigger_unsupported_device(hass, aioclient_mock): """Test unsupported device doesn't return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, @@ -407,7 +408,7 @@ async def test_validate_trigger_unsupported_trigger(hass, aioclient_mock): """Test unsupported trigger does not return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, @@ -446,7 +447,7 @@ async def test_attach_trigger_no_matching_event(hass, aioclient_mock): """Test no matching event for device doesn't return a trigger config.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DECONZ_DOMAIN, "d0:cf:5e:ff:fe:71:a4:3a")}, From ed24638201852f13604ba491975cf14a24a3555c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 15 Mar 2022 18:51:33 +0100 Subject: [PATCH 0472/1054] Update SMA config entry when unique ID already configured (#68179) --- homeassistant/components/sma/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index c2fd48a00ca..070610d6ae2 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -78,7 +78,7 @@ class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: await self.async_set_unique_id(device_info["serial"]) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=self._data) return self.async_create_entry( title=self._data[CONF_HOST], data=self._data ) From 65c670c2c7c468fb8d32c9a391a62104648d1017 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Mar 2022 08:05:56 -1000 Subject: [PATCH 0473/1054] Add diagnostics to august (#68157) --- homeassistant/components/august/__init__.py | 26 ++-- .../components/august/binary_sensor.py | 4 +- homeassistant/components/august/button.py | 6 +- homeassistant/components/august/camera.py | 10 +- homeassistant/components/august/const.py | 2 - .../components/august/diagnostics.py | 47 ++++++ homeassistant/components/august/lock.py | 6 +- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/august/sensor.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/august/test_diagnostics.py | 139 ++++++++++++++++++ 12 files changed, 217 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/august/diagnostics.py create mode 100644 tests/components/august/test_diagnostics.py diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 031f513843f..570ec5983fe 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,10 +1,15 @@ """Support for August devices.""" +from __future__ import annotations + import asyncio +from collections.abc import ValuesView from itertools import chain import logging from aiohttp import ClientError, ClientResponseError +from yalexs.doorbell import Doorbell, DoorbellDetail from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.lock import Lock, LockDetail from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub @@ -18,19 +23,19 @@ from homeassistant.exceptions import ( ) from .activity import ActivityStream -from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS +from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS from .exceptions import CannotConnect, InvalidAuth, RequireValidation from .gateway import AugustGateway from .subscriber import AugustSubscriberMixin _LOGGER = logging.getLogger(__name__) -API_CACHED_ATTRS = ( +API_CACHED_ATTRS = { "door_state", "door_state_datetime", "lock_status", "lock_status_datetime", -) +} async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -52,7 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() + data: AugustData = hass.data[DOMAIN][entry.entry_id] + data.async_stop() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @@ -78,10 +84,8 @@ async def async_setup_august( await august_gateway.async_refresh_access_token_if_needed() hass.data.setdefault(DOMAIN, {}) - data = hass.data[DOMAIN][config_entry.entry_id] = { - DATA_AUGUST: AugustData(hass, august_gateway) - } - await data[DATA_AUGUST].async_setup() + data = hass.data[DOMAIN][config_entry.entry_id] = AugustData(hass, august_gateway) + await data.async_setup() hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -189,16 +193,16 @@ class AugustData(AugustSubscriberMixin): self.activity_stream.async_stop() @property - def doorbells(self): + def doorbells(self) -> ValuesView[Doorbell]: """Return a list of py-august Doorbell objects.""" return self._doorbells_by_id.values() @property - def locks(self): + def locks(self) -> ValuesView[Lock]: """Return a list of py-august Lock objects.""" return self._locks_by_id.values() - def get_device_detail(self, device_id): + def get_device_detail(self, device_id: str) -> DoorbellDetail | LockDetail: """Return the py-august LockDetail or DoorbellDetail object for a device.""" return self._device_detail_by_id[device_id] diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 703a1e58d87..a33d1cb96dc 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -29,7 +29,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from . import AugustData -from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN +from .const import ACTIVITY_UPDATE_INTERVAL, DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -160,7 +160,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the August binary sensors.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] entities: list[BinarySensorEntity] = [] for door in data.locks: diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index d7f2a5ba4ae..5f4032153a2 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustData -from .const import DATA_AUGUST, DOMAIN +from .const import DOMAIN from .entity import AugustEntityMixin @@ -17,8 +17,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up August lock wake buttons.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - async_add_entities([AugustWakeLockButton(data, lock) for lock in data.locks]) + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities(AugustWakeLockButton(data, lock) for lock in data.locks) class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 26889427555..c5ab5fc3cfa 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AugustData -from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN +from .const import DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN from .entity import AugustEntityMixin @@ -21,13 +21,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up August cameras.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] session = aiohttp_client.async_get_clientsession(hass) async_add_entities( - [ - AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) - for doorbell in data.doorbells - ] + AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) + for doorbell in data.doorbells ) diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index ac6f463467f..9a724d4a87b 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -19,8 +19,6 @@ MANUFACTURER = "August Home Inc." DEFAULT_AUGUST_CONFIG_FILE = ".august.conf" -DATA_AUGUST = "data_august" - DEFAULT_NAME = "August" DOMAIN = "august" diff --git a/homeassistant/components/august/diagnostics.py b/homeassistant/components/august/diagnostics.py new file mode 100644 index 00000000000..ffd62cd8fb7 --- /dev/null +++ b/homeassistant/components/august/diagnostics.py @@ -0,0 +1,47 @@ +"""Diagnostics support for august.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import AugustData +from .const import DOMAIN + +TO_REDACT = { + "HouseID", + "OfflineKeys", + "installUserID", + "invitations", + "key", + "pins", + "pubsubChannel", + "recentImage", + "remoteOperateSecret", + "users", + "zWaveDSK", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data: AugustData = hass.data[DOMAIN][entry.entry_id] + + return { + "locks": { + lock.device_id: async_redact_data( + data.get_device_detail(lock.device_id).raw, TO_REDACT + ) + for lock in data.locks + }, + "doorbells": { + doorbell.device_id: async_redact_data( + data.get_device_detail(doorbell.device_id).raw, TO_REDACT + ) + for doorbell in data.doorbells + }, + } diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e30f8301a8f..c993cf03b89 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -15,7 +15,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util from . import AugustData -from .const import DATA_AUGUST, DOMAIN +from .const import DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) @@ -29,8 +29,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up August locks.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] - async_add_entities([AugustLock(data, lock) for lock in data.locks]) + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities(AugustLock(data, lock) for lock in data.locks) class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 1eb923b91a8..329522bef28 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.22"], + "requirements": ["yalexs==1.1.23"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index cd95e1c9926..6116e7d7601 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -31,7 +31,6 @@ from .const import ( ATTR_OPERATION_KEYPAD, ATTR_OPERATION_METHOD, ATTR_OPERATION_REMOTE, - DATA_AUGUST, DOMAIN, OPERATION_METHOD_AUTORELOCK, OPERATION_METHOD_KEYPAD, @@ -93,7 +92,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the August sensors.""" - data: AugustData = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST] + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] entities: list[SensorEntity] = [] migrate_unique_id_devices = [] operation_sensors = [] diff --git a/requirements_all.txt b/requirements_all.txt index db676ad9ed1..9c71107e86a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2443,7 +2443,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.22 +yalexs==1.1.23 # homeassistant.components.yeelight yeelight==0.7.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e7d0c25929..0f59c87c706 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1563,7 +1563,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.22 +yalexs==1.1.23 # homeassistant.components.yeelight yeelight==0.7.9 diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py new file mode 100644 index 00000000000..520daa91f91 --- /dev/null +++ b/tests/components/august/test_diagnostics.py @@ -0,0 +1,139 @@ +"""Test august diagnostics.""" + +from tests.components.august.mocks import ( + _create_august_api_with_devices, + _mock_doorbell_from_fixture, + _mock_lock_from_fixture, +) +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + lock_one = await _mock_lock_from_fixture( + hass, "get_lock.online_with_doorsense.json" + ) + doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") + + entry, _ = await _create_august_api_with_devices(hass, [lock_one, doorbell_one]) + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == { + "doorbells": { + "K98GiDT45GUL": { + "HouseID": "**REDACTED**", + "LockID": "BBBB1F5F11114C24CCCC97571DD6AAAA", + "appID": "august-iphone", + "caps": ["reconnect"], + "createdAt": "2016-11-26T22:27:11.176Z", + "doorbellID": "K98GiDT45GUL", + "doorbellServerURL": "https://doorbells.august.com", + "dvrSubscriptionSetupDone": True, + "firmwareVersion": "2.3.0-RC153+201711151527", + "installDate": "2016-11-26T22:27:11.176Z", + "installUserID": "**REDACTED**", + "name": "Front Door", + "pubsubChannel": "**REDACTED**", + "recentImage": "**REDACTED**", + "serialNumber": "tBXZR0Z35E", + "settings": { + "ABREnabled": True, + "IREnabled": True, + "IVAEnabled": False, + "JPGQuality": 70, + "batteryLowThreshold": 3.1, + "batteryRun": False, + "batteryUseThreshold": 3.4, + "bitrateCeiling": 512000, + "buttonpush_notifications": True, + "debug": False, + "directLink": True, + "initialBitrate": 384000, + "irConfiguration": 8448272, + "keepEncoderRunning": True, + "micVolume": 100, + "minACNoScaling": 40, + "motion_notifications": True, + "notify_when_offline": True, + "overlayEnabled": True, + "ringSoundEnabled": True, + "speakerVolume": 92, + "turnOffCamera": False, + "videoResolution": "640x480", + }, + "status": "doorbell_call_status_online", + "status_timestamp": 1512811834532, + "telemetry": { + "BSSID": "88:ee:00:dd:aa:11", + "SSID": "foo_ssid", + "ac_in": 23.856874, + "battery": 4.061763, + "battery_soc": 96, + "battery_soh": 95, + "date": "2017-12-10 08:05:12", + "doorbell_low_battery": False, + "ip_addr": "10.0.1.11", + "link_quality": 54, + "load_average": "0.50 0.47 0.35 " "1/154 9345", + "signal_level": -56, + "steady_ac_in": 22.196405, + "temperature": 28.25, + "updated_at": "2017-12-10T08:05:13.650Z", + "uptime": "16168.75 13830.49", + "wifi_freq": 5745, + }, + "updatedAt": "2017-12-10T08:05:13.650Z", + } + }, + "locks": { + "online_with_doorsense": { + "Bridge": { + "_id": "bridgeid", + "deviceModel": "august-connect", + "firmwareVersion": "2.2.1", + "hyperBridge": True, + "mfgBridgeID": "C5WY200WSH", + "operative": True, + "status": { + "current": "online", + "lastOffline": "2000-00-00T00:00:00.447Z", + "lastOnline": "2000-00-00T00:00:00.447Z", + "updated": "2000-00-00T00:00:00.447Z", + }, + }, + "Calibrated": False, + "Created": "2000-00-00T00:00:00.447Z", + "HouseID": "**REDACTED**", + "HouseName": "Test", + "LockID": "online_with_doorsense", + "LockName": "Online door with doorsense", + "LockStatus": { + "dateTime": "2017-12-10T04:48:30.272Z", + "doorState": "open", + "isLockStatusChanged": False, + "status": "locked", + "valid": True, + }, + "SerialNumber": "XY", + "Type": 1001, + "Updated": "2000-00-00T00:00:00.447Z", + "battery": 0.922, + "currentFirmwareVersion": "undefined-4.3.0-1.8.14", + "homeKitEnabled": True, + "hostLockInfo": { + "manufacturer": "yale", + "productID": 1536, + "productTypeID": 32770, + "serialNumber": "ABC", + }, + "isGalileo": False, + "macAddress": "12:22", + "pins": "**REDACTED**", + "pubsubChannel": "**REDACTED**", + "skuNumber": "AUG-MD01", + "supportsEntryCodes": True, + "timeZone": "Pacific/Hawaii", + "zWaveEnabled": False, + } + }, + } From 2aaeb1fa99f3f691a5c4adfff984e25bf96d787d Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Tue, 15 Mar 2022 22:33:59 +0100 Subject: [PATCH 0474/1054] Fix finding matrix room that is already joined (#67967) After some debugging, it seems room.canonical_alias contains the room alias that matches the room_id_or_alias value but is not contained in room.aliases (which is empty). As a result, the matrix component thought the room wasn't alread joined, joins again, and this replaces the previous room which had the listener. This resulted in the component callback not being called for new messages in the room. This fixes #66372 --- homeassistant/components/matrix/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 76e2630b26e..03772630a9e 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -243,7 +243,10 @@ class MatrixBot: room.update_aliases() self._aliases_fetched_for.add(room.room_id) - if room_id_or_alias in room.aliases: + if ( + room_id_or_alias in room.aliases + or room_id_or_alias == room.canonical_alias + ): _LOGGER.debug( "Already in room %s (known as %s)", room.room_id, room_id_or_alias ) From 6f61ed8799014ddb87d34f7e7070958a6fe30f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 15 Mar 2022 22:46:02 +0100 Subject: [PATCH 0475/1054] Add backup platform support (#68182) --- .core_files.yaml | 1 + homeassistant/components/backup/manager.py | 77 +++++++- tests/components/backup/test_manager.py | 202 ++++++++++++++++----- 3 files changed, 227 insertions(+), 53 deletions(-) diff --git a/.core_files.yaml b/.core_files.yaml index ebc3ff376f8..6f7b57c7869 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -49,6 +49,7 @@ components: &components - homeassistant/components/alexa/* - homeassistant/components/auth/* - homeassistant/components/automation/* + - homeassistant/components/backup/* - homeassistant/components/cloud/* - homeassistant/components/config/* - homeassistant/components/configurator/* diff --git a/homeassistant/components/backup/manager.py b/homeassistant/components/backup/manager.py index 0344ffa4543..eee9919e711 100644 --- a/homeassistant/components/backup/manager.py +++ b/homeassistant/components/backup/manager.py @@ -1,6 +1,7 @@ """Backup manager for the Backup integration.""" from __future__ import annotations +import asyncio from dataclasses import asdict, dataclass import hashlib import json @@ -8,16 +9,17 @@ from pathlib import Path import tarfile from tarfile import TarError from tempfile import TemporaryDirectory -from typing import Any +from typing import Any, Protocol from securetar import SecureTarFile, atomic_contents_add from homeassistant.const import __version__ as HAVERSION from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import integration_platform from homeassistant.util import dt, json as json_util -from .const import EXCLUDE_FROM_BACKUP, LOGGER +from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER @dataclass @@ -35,6 +37,16 @@ class Backup: return {**asdict(self), "path": self.path.as_posix()} +class BackupPlatformProtocol(Protocol): + """Define the format that backup platforms can have.""" + + async def async_pre_backup(self, hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + + async def async_post_backup(self, hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" + + class BackupManager: """Backup manager for the Backup integration.""" @@ -44,14 +56,41 @@ class BackupManager: self.backup_dir = Path(hass.config.path("backups")) self.backing_up = False self.backups: dict[str, Backup] = {} - self.loaded = False + self.platforms: dict[str, BackupPlatformProtocol] = {} + self.loaded_backups = False + self.loaded_platforms = False + + async def _add_platform( + self, + hass: HomeAssistant, + integration_domain: str, + platform: BackupPlatformProtocol, + ) -> None: + """Add a platform to the backup manager.""" + if not hasattr(platform, "async_pre_backup") or not hasattr( + platform, "async_post_backup" + ): + LOGGER.warning( + "%s does not implement required functions for the backup platform", + integration_domain, + ) + return + self.platforms[integration_domain] = platform async def load_backups(self) -> None: """Load data of stored backup files.""" backups = await self.hass.async_add_executor_job(self._read_backups) LOGGER.debug("Loaded %s backups", len(backups)) self.backups = backups - self.loaded = True + self.loaded_backups = True + + async def load_platforms(self) -> None: + """Load backup platforms.""" + await integration_platform.async_process_integration_platforms( + self.hass, DOMAIN, self._add_platform + ) + LOGGER.debug("Loaded %s platforms", len(self.platforms)) + self.loaded_platforms = True def _read_backups(self) -> dict[str, Backup]: """Read backups from disk.""" @@ -75,14 +114,14 @@ class BackupManager: async def get_backups(self) -> dict[str, Backup]: """Return backups.""" - if not self.loaded: + if not self.loaded_backups: await self.load_backups() return self.backups async def get_backup(self, slug: str) -> Backup | None: """Return a backup.""" - if not self.loaded: + if not self.loaded_backups: await self.load_backups() if not (backup := self.backups.get(slug)): @@ -113,8 +152,22 @@ class BackupManager: if self.backing_up: raise HomeAssistantError("Backup already in progress") + if not self.loaded_platforms: + await self.load_platforms() + try: self.backing_up = True + pre_backup_results = await asyncio.gather( + *( + platform.async_pre_backup(self.hass) + for platform in self.platforms.values() + ), + return_exceptions=True, + ) + for result in pre_backup_results: + if isinstance(result, Exception): + raise result + backup_name = f"Core {HAVERSION}" date_str = dt.now().isoformat() slug = _generate_slug(date_str, backup_name) @@ -146,12 +199,22 @@ class BackupManager: path=tar_file_path, size=round(tar_file_path.stat().st_size / 1_048_576, 2), ) - if self.loaded: + if self.loaded_backups: self.backups[slug] = backup LOGGER.debug("Generated new backup with slug %s", slug) return backup finally: self.backing_up = False + post_backup_results = await asyncio.gather( + *( + platform.async_post_backup(self.hass) + for platform in self.platforms.values() + ), + return_exceptions=True, + ) + for result in post_backup_results: + if isinstance(result, Exception): + raise result def _generate_backup_contents( self, diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index b2e8923263d..7edd64e0cb0 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -1,15 +1,77 @@ """Tests for the Backup integration.""" +from __future__ import annotations + from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from homeassistant.components.backup import BackupManager +from homeassistant.components.backup.manager import BackupPlatformProtocol from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component from .common import TEST_BACKUP +from tests.common import MockPlatform, mock_platform + + +async def _mock_backup_generation(manager: BackupManager): + """Mock backup generator.""" + + def _mock_iterdir(path: Path) -> list[Path]: + if not path.name.endswith("testing_config"): + return [] + return [ + Path("test.txt"), + Path(".DS_Store"), + Path(".storage"), + ] + + with patch("tarfile.open", MagicMock()) as mocked_tarfile, patch( + "pathlib.Path.iterdir", _mock_iterdir + ), patch("pathlib.Path.stat", MagicMock(st_size=123)), patch( + "pathlib.Path.is_file", lambda x: x.name != ".storage" + ), patch( + "pathlib.Path.is_dir", + lambda x: x.name == ".storage", + ), patch( + "pathlib.Path.exists", + lambda x: x != manager.backup_dir, + ), patch( + "pathlib.Path.is_symlink", + lambda _: False, + ), patch( + "pathlib.Path.mkdir", + MagicMock(), + ), patch( + "homeassistant.components.backup.manager.json_util.save_json" + ) as mocked_json_util, patch( + "homeassistant.components.backup.manager.HAVERSION", + "2025.1.0", + ): + await manager.generate_backup() + + assert mocked_json_util.call_count == 1 + assert mocked_json_util.call_args[0][1]["homeassistant"] == { + "version": "2025.1.0" + } + + assert ( + manager.backup_dir.as_posix() + in mocked_tarfile.call_args_list[0].kwargs["name"] + ) + + +async def _setup_mock_domain( + hass: HomeAssistant, + platform: BackupPlatformProtocol | None = None, +) -> None: + """Set up a mock domain.""" + mock_platform(hass, "some_domain.backup", platform or MockPlatform()) + assert await async_setup_component(hass, "some_domain", {}) + async def test_constructor(hass: HomeAssistant) -> None: """Test BackupManager constructor.""" @@ -59,7 +121,7 @@ async def test_removing_backup( """Test removing backup.""" manager = BackupManager(hass) manager.backups = {TEST_BACKUP.slug: TEST_BACKUP} - manager.loaded = True + manager.loaded_backups = True with patch("pathlib.Path.exists", return_value=True): await manager.remove_backup(TEST_BACKUP.slug) @@ -84,7 +146,7 @@ async def test_getting_backup_that_does_not_exist( """Test getting backup that does not exist.""" manager = BackupManager(hass) manager.backups = {TEST_BACKUP.slug: TEST_BACKUP} - manager.loaded = True + manager.loaded_backups = True with patch("pathlib.Path.exists", return_value=False): backup = await manager.get_backup(TEST_BACKUP.slug) @@ -110,50 +172,98 @@ async def test_generate_backup( ) -> None: """Test generate backup.""" manager = BackupManager(hass) - manager.loaded = True + manager.loaded_backups = True - def _mock_iterdir(path: Path) -> list[Path]: - if not path.name.endswith("testing_config"): - return [] - return [ - Path("test.txt"), - Path(".DS_Store"), - Path(".storage"), - ] - - with patch("tarfile.open", MagicMock()) as mocked_tarfile, patch( - "pathlib.Path.iterdir", _mock_iterdir - ), patch("pathlib.Path.stat", MagicMock(st_size=123)), patch( - "pathlib.Path.is_file", lambda x: x.name != ".storage" - ), patch( - "pathlib.Path.is_dir", - lambda x: x.name == ".storage", - ), patch( - "pathlib.Path.exists", - lambda x: x != manager.backup_dir, - ), patch( - "pathlib.Path.is_symlink", - lambda _: False, - ), patch( - "pathlib.Path.mkdir", - MagicMock(), - ), patch( - "homeassistant.components.backup.manager.json_util.save_json" - ) as mocked_json_util, patch( - "homeassistant.components.backup.manager.HAVERSION", - "2025.1.0", - ): - await manager.generate_backup() - - assert mocked_json_util.call_count == 1 - assert mocked_json_util.call_args[0][1]["homeassistant"] == { - "version": "2025.1.0" - } - - assert ( - manager.backup_dir.as_posix() - in mocked_tarfile.call_args_list[0].kwargs["name"] - ) + await _mock_backup_generation(manager) assert "Generated new backup with slug " in caplog.text assert "Creating backup directory" in caplog.text + assert "Loaded 0 platforms" in caplog.text + + +async def test_loading_platforms( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading backup platforms.""" + manager = BackupManager(hass) + + assert not manager.loaded_platforms + assert not manager.platforms + + await _setup_mock_domain( + hass, + Mock( + async_pre_backup=AsyncMock(), + async_post_backup=AsyncMock(), + ), + ) + await manager.load_platforms() + + assert manager.loaded_platforms + assert len(manager.platforms) == 1 + + assert "Loaded 1 platforms" in caplog.text + + +async def test_not_loading_bad_platforms( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test loading backup platforms.""" + manager = BackupManager(hass) + + assert not manager.loaded_platforms + assert not manager.platforms + + await _setup_mock_domain(hass) + await manager.load_platforms() + + assert manager.loaded_platforms + assert len(manager.platforms) == 0 + + assert "Loaded 0 platforms" in caplog.text + assert ( + "some_domain does not implement required functions for the backup platform" + in caplog.text + ) + + +async def test_exception_plaform_pre(hass: HomeAssistant) -> None: + """Test exception in pre step.""" + manager = BackupManager(hass) + manager.loaded_backups = True + + async def _mock_step(hass: HomeAssistant) -> None: + raise HomeAssistantError("Test exception") + + await _setup_mock_domain( + hass, + Mock( + async_pre_backup=_mock_step, + async_post_backup=AsyncMock(), + ), + ) + + with pytest.raises(HomeAssistantError): + await _mock_backup_generation(manager) + + +async def test_exception_plaform_post(hass: HomeAssistant) -> None: + """Test exception in post step.""" + manager = BackupManager(hass) + manager.loaded_backups = True + + async def _mock_step(hass: HomeAssistant) -> None: + raise HomeAssistantError("Test exception") + + await _setup_mock_domain( + hass, + Mock( + async_pre_backup=AsyncMock(), + async_post_backup=_mock_step, + ), + ) + + with pytest.raises(HomeAssistantError): + await _mock_backup_generation(manager) From 484f7238751b2df79d58ffeaca44bc75abdb267f Mon Sep 17 00:00:00 2001 From: Jeef Date: Tue, 15 Mar 2022 16:52:19 -0600 Subject: [PATCH 0476/1054] IntelliFire Diagnostic Error Sensor (#67403) Co-authored-by: J. Nick Koston --- homeassistant/components/intellifire/manifest.json | 3 +-- homeassistant/components/intellifire/sensor.py | 6 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 03862a6ef5f..75d4ee2e75f 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,8 +3,7 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==0.9.9"], - "dependencies": [], + "requirements": ["intellifire4py==1.0.1"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", "loggers": ["intellifire4py"] diff --git a/homeassistant/components/intellifire/sensor.py b/homeassistant/components/intellifire/sensor.py index b61ea443728..5f57ff62a23 100644 --- a/homeassistant/components/intellifire/sensor.py +++ b/homeassistant/components/intellifire/sensor.py @@ -114,6 +114,12 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = ( value_fn=lambda data: data.connection_quality, entity_registry_enabled_default=False, ), + IntellifireSensorEntityDescription( + key="errors", + name="Errors", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.error_codes_string, + ), IntellifireSensorEntityDescription( key="ecm_latency", name="ECM Latency", diff --git a/requirements_all.txt b/requirements_all.txt index 9c71107e86a..05a46dfa62d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -873,7 +873,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.9.9 +intellifire4py==1.0.1 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f59c87c706..7e6ef229972 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.intellifire -intellifire4py==0.9.9 +intellifire4py==1.0.1 # homeassistant.components.iotawatt iotawattpy==0.1.0 From 4b963c2ac0ec1e8a82763f6d403b666e5efeaf80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Mar 2022 15:55:08 -1000 Subject: [PATCH 0477/1054] Add diagnostics support to nexia (#68215) --- homeassistant/components/nexia/diagnostics.py | 32 + tests/components/nexia/test_diagnostics.py | 7530 +++++++++++++++++ 2 files changed, 7562 insertions(+) create mode 100644 homeassistant/components/nexia/diagnostics.py create mode 100644 tests/components/nexia/test_diagnostics.py diff --git a/homeassistant/components/nexia/diagnostics.py b/homeassistant/components/nexia/diagnostics.py new file mode 100644 index 00000000000..9b3f518217b --- /dev/null +++ b/homeassistant/components/nexia/diagnostics.py @@ -0,0 +1,32 @@ +"""Diagnostics support for nexia.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import CONF_BRAND, DOMAIN +from .coordinator import NexiaDataUpdateCoordinator + +TO_REDACT = { + "dealer_contact_info", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + nexia_home = coordinator.nexia_home + + return { + "entry": { + "title": entry.title, + "brand": entry.data.get(CONF_BRAND), + }, + "automations": async_redact_data(nexia_home.automations_json, TO_REDACT), + "devices": async_redact_data(nexia_home.devices_json, TO_REDACT), + } diff --git a/tests/components/nexia/test_diagnostics.py b/tests/components/nexia/test_diagnostics.py new file mode 100644 index 00000000000..4aa91c2f301 --- /dev/null +++ b/tests/components/nexia/test_diagnostics.py @@ -0,0 +1,7530 @@ +"""Test august diagnostics.""" + +from .util import async_init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client): + """Test generating diagnostics for a config entry.""" + entry = await async_init_integration(hass) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + assert diag == { + "automations": [ + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467876", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=472ae0d2-5d7c-4a1c-9e47-4d9035fdace5" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467876" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467876" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "62.0 and cool to 83.0 AND Downstairs East " + "Wing will permanently hold the heat to 62.0 " + "and cool to 83.0 AND Downstairs West Wing " + "will permanently hold the heat to 62.0 and " + "cool to 83.0 AND Activate the mode named " + "'Away 12' AND Master Suite will permanently " + "hold the heat to 62.0 and cool to 83.0", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "plane"}, + {"modifiers": [], "name": "climate"}, + ], + "id": 3467876, + "name": "Away for 12 Hours", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467870", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=f63ee20c-3146-49a1-87c5-47429a063d15" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467870" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467870" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "60.0 and cool to 85.0 AND Downstairs East " + "Wing will permanently hold the heat to 60.0 " + "and cool to 85.0 AND Downstairs West Wing " + "will permanently hold the heat to 60.0 and " + "cool to 85.0 AND Activate the mode named " + "'Away 24' AND Master Suite will permanently " + "hold the heat to 60.0 and cool to 85.0", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "plane"}, + {"modifiers": [], "name": "climate"}, + ], + "id": 3467870, + "name": "Away For 24 Hours", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452469", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e5c59b93-efca-4937-9499-3f4c896ab17c" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452469" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452469" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "63.0 and cool to 80.0 AND Downstairs East " + "Wing will permanently hold the heat to 63.0 " + "and cool to 79.0 AND Downstairs West Wing " + "will permanently hold the heat to 63.0 and " + "cool to 79.0 AND Upstairs West Wing will " + "permanently hold the heat to 63.0 and cool " + "to 81.0 AND Upstairs West Wing will change " + "Fan Mode to Auto AND Downstairs East Wing " + "will change Fan Mode to Auto AND Downstairs " + "West Wing will change Fan Mode to Auto AND " + "Activate the mode named 'Away Short' AND " + "Master Suite will permanently hold the heat " + "to 63.0 and cool to 79.0 AND Master Suite " + "will change Fan Mode to Auto", + "enabled": False, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "key"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3452469, + "name": "Away Short", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452472", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=861b9fec-d259-4492-a798-5712251666c4" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452472" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452472" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs " + "East Wing will Run Schedule AND Downstairs " + "West Wing will Run Schedule AND Activate the " + "mode named 'Home' AND Master Suite will Run " + "Schedule", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "at_home"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3452472, + "name": "Home", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454776", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=96c71d37-66aa-4cbb-84ff-a90412fd366a" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454776" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454776" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "60.0 and cool to 85.0 AND Downstairs East " + "Wing will permanently hold the heat to 60.0 " + "and cool to 85.0 AND Downstairs West Wing " + "will permanently hold the heat to 60.0 and " + "cool to 85.0 AND Upstairs West Wing will " + "change Fan Mode to Auto AND Downstairs East " + "Wing will change Fan Mode to Auto AND " + "Downstairs West Wing will change Fan Mode to " + "Auto AND Master Suite will permanently hold " + "the heat to 60.0 and cool to 85.0 AND Master " + "Suite will change Fan Mode to Auto", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3454776, + "name": "IFTTT Power Spike", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454774", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=880c5287-d92c-4368-8494-e10975e92733" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454774" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454774" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs " + "East Wing will Run Schedule AND Downstairs " + "West Wing will Run Schedule AND Master Suite " + "will Run Schedule", + "enabled": False, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + ], + "id": 3454774, + "name": "IFTTT return to schedule", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486078", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=d33c013b-2357-47a9-8c66-d2c3693173b0" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486078" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486078" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to " + "55.0 and cool to 90.0 AND Downstairs East " + "Wing will permanently hold the heat to 55.0 " + "and cool to 90.0 AND Downstairs West Wing " + "will permanently hold the heat to 55.0 and " + "cool to 90.0 AND Activate the mode named " + "'Power Outage'", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "climate"}, + {"modifiers": [], "name": "bell"}, + ], + "id": 3486078, + "name": "Power Outage", + "settings": [], + "triggers": [], + }, + { + "_links": { + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486091", + "method": "POST", + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=b9141df8-2e5e-4524-b8ef-efcbf48d775a" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486091" + }, + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486091" + }, + }, + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs " + "East Wing will Run Schedule AND Downstairs " + "West Wing will Run Schedule AND Activate the " + "mode named 'Home'", + "enabled": True, + "icon": [ + {"modifiers": [], "name": "gears"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "settings"}, + {"modifiers": [], "name": "at_home"}, + ], + "id": 3486091, + "name": "Power Restored", + "settings": [], + "triggers": [], + }, + ], + "devices": [ + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=cd9a70e8-fd0d-4b58-b071-05a202fd8953" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059661" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/be6d8ede5cac02fe8be18c334b04d539c9200fa9230eef63" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + {"label": "AUID", "type": "label_value", "value": "000000"}, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "System Idle", + "status_icon": None, + "temperature": 71, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 71, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-71"], + "name": "thermostat", + }, + "id": 83261002, + "name": "Living East", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 71, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-77"], + "name": "thermostat", + }, + "id": 83261005, + "name": "Kitchen", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-72"], + "name": "thermostat", + }, + "id": 83261008, + "name": "Down Bedroom", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 78, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-78"], + "name": "thermostat", + }, + "id": 83261011, + "name": "Tech Room", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 78, + "type": "xxl_zone", + "zone_status": "", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_off"}, + "value": "auto", + }, + {"compressor_speed": 0.0, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-71"], "name": "thermostat"}, + {"modifiers": ["temperature-77"], "name": "thermostat"}, + {"modifiers": ["temperature-72"], "name": "thermostat"}, + {"modifiers": ["temperature-78"], "name": "thermostat"}, + ], + "id": 2059661, + "indoor_humidity": "36", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Downstairs East Wing", + "name_editable": True, + "outdoor_temperature": "88", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/dehumidify" + } + }, + "current_value": 0.5, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "System Idle", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 71, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-71"], "name": "thermostat"}, + "id": 83261002, + "name": "Living East", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 71, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-77"], "name": "thermostat"}, + "id": 83261005, + "name": "Kitchen", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-72"], "name": "thermostat"}, + "id": 83261008, + "name": "Down Bedroom", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 78, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-78"], "name": "thermostat"}, + "id": 83261011, + "name": "Tech Room", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 78, + "type": "xxl_zone", + "zone_status": "", + }, + ], + }, + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=5aae72a6-1bd0-4d84-9bfd-673e7bc4907c" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059676" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/3412f1d96eb0c5edb5466c3c0598af60c06f8443f21e9bcb" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + { + "label": "AUID", + "type": "label_value", + "value": "02853E08", + }, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "System Idle", + "status_icon": None, + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-75"], + "name": "thermostat", + }, + "id": 83261015, + "name": "Living West", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-75"], + "name": "thermostat", + }, + "id": 83261018, + "name": "David Office", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_off"}, + "value": "auto", + }, + {"compressor_speed": 0.0, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-75"], "name": "thermostat"}, + {"modifiers": ["temperature-75"], "name": "thermostat"}, + ], + "id": 2059676, + "indoor_humidity": "52", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Downstairs West Wing", + "name_editable": True, + "outdoor_temperature": "88", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/dehumidify" + } + }, + "current_value": 0.45, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "System Idle", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-75"], "name": "thermostat"}, + "id": 83261015, + "name": "Living West", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-75"], "name": "thermostat"}, + "id": 83261018, + "name": "David Office", + "operating_state": "", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + }, + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + { + "label": "AUID", + "type": "label_value", + "value": "0281B02C", + }, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Cooling", + "status_icon": {"modifiers": [], "name": "cooling"}, + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving " "Air", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-73"], + "name": "thermostat", + }, + "id": 83394133, + "name": "Bath Closet", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + }, + "cooling_setpoint": 71, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 71, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Open", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-74"], + "name": "thermostat", + }, + "id": 83394130, + "name": "Master", + "operating_state": "Damper Open", + "setpoints": {"cool": 71, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Open", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving " "Air", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-73"], + "name": "thermostat", + }, + "id": 83394136, + "name": "Nick Office", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper " "Closed", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-72"], + "name": "thermostat", + }, + "id": 83394127, + "name": "Snooze Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper " "Closed", + "status_icon": { + "modifiers": [], + "name": "cooling", + }, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-74"], + "name": "thermostat", + }, + "id": 83394139, + "name": "Safe Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_on"}, + "value": "auto", + }, + {"compressor_speed": 0.69, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-73"], "name": "thermostat"}, + {"modifiers": ["temperature-74"], "name": "thermostat"}, + {"modifiers": ["temperature-73"], "name": "thermostat"}, + {"modifiers": ["temperature-72"], "name": "thermostat"}, + {"modifiers": ["temperature-74"], "name": "thermostat"}, + ], + "id": 2293892, + "indoor_humidity": "52", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Master Suite", + "name_editable": True, + "outdoor_temperature": "87", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" + } + }, + "current_value": 0.45, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "Cooling", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving Air", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-73"], "name": "thermostat"}, + "id": 83394133, + "name": "Bath Closet", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + }, + "cooling_setpoint": 71, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 71, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Open", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-74"], "name": "thermostat"}, + "id": 83394130, + "name": "Master", + "operating_state": "Damper Open", + "setpoints": {"cool": 71, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Open", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Relieving Air", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 73, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-73"], "name": "thermostat"}, + "id": 83394136, + "name": "Nick Office", + "operating_state": "Relieving Air", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 73, + "type": "xxl_zone", + "zone_status": "Relieving Air", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Closed", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 72, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-72"], "name": "thermostat"}, + "id": 83394127, + "name": "Snooze Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 72, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + }, + "cooling_setpoint": 79, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 79, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "Damper Closed", + "status_icon": {"modifiers": [], "name": "cooling"}, + "system_status": "Cooling", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-74"], "name": "thermostat"}, + "id": 83394139, + "name": "Safe Room", + "operating_state": "Damper Closed", + "setpoints": {"cool": 79, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "Damper Closed", + }, + ], + }, + { + "_links": { + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=3679e95b-7337-48ae-aff4-e0522e9dd0eb" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059652" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/c6627726f6339d104ee66897028d6a2ea38215675b336650" + }, + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652" + }, + }, + "connected": True, + "delta": 3, + "features": [ + { + "items": [ + { + "label": "Model", + "type": "label_value", + "value": "XL1050", + }, + { + "label": "AUID", + "type": "label_value", + "value": "02853DF0", + }, + { + "label": "Firmware Build Number", + "type": "label_value", + "value": "1581321824", + }, + { + "label": "Firmware Build Date", + "type": "label_value", + "value": "2020-02-10 08:03:44 UTC", + }, + { + "label": "Firmware Version", + "type": "label_value", + "value": "5.9.1", + }, + { + "label": "Zoning Enabled", + "type": "label_value", + "value": "yes", + }, + ], + "name": "advanced_info", + }, + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "System Idle", + "status_icon": None, + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "members": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + }, + "cooling_setpoint": 80, + "current_zone_mode": "OFF", + "features": [ + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode", + "method": "POST", + } + }, + "display_value": "Off", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "OFF", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-77"], + "name": "thermostat", + }, + "id": 83260991, + "name": "Hallway", + "operating_state": "", + "setpoints": {"cool": 80, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + }, + "current_value": "OFF", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-74"], + "name": "thermostat", + }, + "id": 83260994, + "name": "Mid Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System " "Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone " "Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run " "Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow " + "or " + "override " + "the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": { + "modifiers": ["temperature-75"], + "name": "thermostat", + }, + "id": 83260997, + "name": "West Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": [ + "Permanent " "Hold", + "Run " "Schedule", + ], + "options": [ + { + "label": "Permanent " "Hold", + "value": "permanent_hold", + }, + { + "label": "Run " "Schedule", + "value": "run_schedule", + }, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + "name": "group", + }, + { + "actions": { + "update_thermostat_fan_mode": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Fan Mode", + "name": "thermostat_fan_mode", + "options": [ + { + "header": True, + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + }, + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "status_icon": {"modifiers": [], "name": "thermostat_fan_off"}, + "value": "auto", + }, + {"compressor_speed": 0.0, "name": "thermostat_compressor_speed"}, + { + "actions": { + "get_monthly_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=monthly", + "method": "GET", + }, + "get_runtime_history": { + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=daily", + "method": "GET", + }, + }, + "name": "runtime_history", + }, + ], + "has_indoor_humidity": True, + "has_outdoor_temperature": True, + "icon": [ + {"modifiers": ["temperature-77"], "name": "thermostat"}, + {"modifiers": ["temperature-74"], "name": "thermostat"}, + {"modifiers": ["temperature-75"], "name": "thermostat"}, + ], + "id": 2059652, + "indoor_humidity": "37", + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "name": "Upstairs West Wing", + "name_editable": True, + "outdoor_temperature": "87", + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "On", "Circulate"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "On", "value": "on"}, + {"label": "Circulate", "value": "circulate"}, + ], + "title": "Fan Mode", + "type": "fan_mode", + "values": ["auto", "on", "circulate"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_speed" + } + }, + "current_value": 0.35, + "labels": [ + "35%", + "40%", + "45%", + "50%", + "55%", + "60%", + "65%", + "70%", + "75%", + "80%", + "85%", + "90%", + "95%", + "100%", + ], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + {"label": "70%", "value": 0.7}, + {"label": "75%", "value": 0.75}, + {"label": "80%", "value": 0.8}, + {"label": "85%", "value": 0.85}, + {"label": "90%", "value": 0.9}, + {"label": "95%", "value": 0.95}, + {"label": "100%", "value": 1.0}, + ], + "title": "Fan Speed", + "type": "fan_speed", + "values": [ + 0.35, + 0.4, + 0.45, + 0.5, + 0.55, + 0.6, + 0.65, + 0.7, + 0.75, + 0.8, + 0.85, + 0.9, + 0.95, + 1.0, + ], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_circulation_time" + } + }, + "current_value": 30, + "labels": [ + "10 minutes", + "15 minutes", + "20 minutes", + "25 minutes", + "30 minutes", + "35 minutes", + "40 minutes", + "45 minutes", + "50 minutes", + "55 minutes", + ], + "options": [ + {"label": "10 minutes", "value": 10}, + {"label": "15 minutes", "value": 15}, + {"label": "20 minutes", "value": 20}, + {"label": "25 minutes", "value": 25}, + {"label": "30 minutes", "value": 30}, + {"label": "35 minutes", "value": 35}, + {"label": "40 minutes", "value": 40}, + {"label": "45 minutes", "value": 45}, + {"label": "50 minutes", "value": 50}, + {"label": "55 minutes", "value": 55}, + ], + "title": "Fan Circulation Time", + "type": "fan_circulation_time", + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/air_cleaner_mode" + } + }, + "current_value": "auto", + "labels": ["Auto", "Quick", "Allergy"], + "options": [ + {"label": "Auto", "value": "auto"}, + {"label": "Quick", "value": "quick"}, + {"label": "Allergy", "value": "allergy"}, + ], + "title": "Air Cleaner Mode", + "type": "air_cleaner_mode", + "values": ["auto", "quick", "allergy"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/dehumidify" + } + }, + "current_value": 0.5, + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "options": [ + {"label": "35%", "value": 0.35}, + {"label": "40%", "value": 0.4}, + {"label": "45%", "value": 0.45}, + {"label": "50%", "value": 0.5}, + {"label": "55%", "value": 0.55}, + {"label": "60%", "value": 0.6}, + {"label": "65%", "value": 0.65}, + ], + "title": "Cooling Dehumidify Set Point", + "type": "dehumidify", + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/scale" + } + }, + "current_value": "f", + "labels": ["F", "C"], + "options": [ + {"label": "F", "value": "f"}, + {"label": "C", "value": "c"}, + ], + "title": "Temperature Scale", + "type": "scale", + "values": ["f", "c"], + }, + ], + "status_secondary": None, + "status_tertiary": None, + "system_status": "System Idle", + "type": "xxl_thermostat", + "zones": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + }, + "cooling_setpoint": 80, + "current_zone_mode": "OFF", + "features": [ + { + "actions": {}, + "name": "thermostat", + "scale": "f", + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 77, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode", + "method": "POST", + } + }, + "display_value": "Off", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "OFF", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-77"], "name": "thermostat"}, + "id": 83260991, + "name": "Hallway", + "operating_state": "", + "setpoints": {"cool": 80, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + }, + "current_value": "OFF", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 77, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 74, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-74"], "name": "thermostat"}, + "id": 83260994, + "name": "Mid Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 74, + "type": "xxl_zone", + "zone_status": "", + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + }, + "cooling_setpoint": 81, + "current_zone_mode": "AUTO", + "features": [ + { + "actions": { + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + }, + "name": "thermostat", + "scale": "f", + "setpoint_cool": 81, + "setpoint_cool_max": 99, + "setpoint_cool_min": 60, + "setpoint_delta": 3, + "setpoint_heat": 63, + "setpoint_heat_max": 90, + "setpoint_heat_min": 55, + "setpoint_increment": 1.0, + "status": "", + "status_icon": None, + "system_status": "System Idle", + "temperature": 75, + }, + { + "is_connected": True, + "name": "connection", + "signal_strength": "unknown", + }, + { + "actions": { + "update_thermostat_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode", + "method": "POST", + } + }, + "display_value": "Auto", + "label": "Zone Mode", + "name": "thermostat_mode", + "options": [ + { + "header": True, + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + }, + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "value": "AUTO", + }, + { + "actions": { + "update_thermostat_run_mode": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode", + "method": "POST", + } + }, + "display_value": "Hold", + "label": "Run Mode", + "name": "thermostat_run_mode", + "options": [ + { + "header": True, + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + }, + { + "id": "info_text", + "info": True, + "label": "Follow or " + "override the " + "schedule.", + "value": "info_text", + }, + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "value": "permanent_hold", + }, + { + "actions": { + "enable_scheduling": { + "data": {"value": True}, + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + }, + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET", + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST", + }, + }, + "can_add_remove_periods": True, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997&house_id=123456", + "enabled": True, + "max_period_name_length": 10, + "max_periods_per_day": 4, + "name": "schedule", + "setpoint_increment": 1, + }, + ], + "heating_setpoint": 63, + "icon": {"modifiers": ["temperature-75"], "name": "thermostat"}, + "id": 83260997, + "name": "West Bedroom", + "operating_state": "", + "setpoints": {"cool": 81, "heat": 63}, + "settings": [ + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + }, + "current_value": 0, + "labels": ["None", "Home", "Away", "Sleep"], + "options": [ + {"label": "None", "value": 0}, + {"label": "Home", "value": 1}, + {"label": "Away", "value": 2}, + {"label": "Sleep", "value": 3}, + ], + "title": "Preset", + "type": "preset_selected", + "values": [0, 1, 2, 3], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + }, + "current_value": "AUTO", + "labels": ["Auto", "Cooling", "Heating", "Off"], + "options": [ + {"label": "Auto", "value": "AUTO"}, + {"label": "Cooling", "value": "COOL"}, + {"label": "Heating", "value": "HEAT"}, + {"label": "Off", "value": "OFF"}, + ], + "title": "Zone Mode", + "type": "zone_mode", + "values": ["AUTO", "COOL", "HEAT", "OFF"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + }, + "current_value": "permanent_hold", + "labels": ["Permanent Hold", "Run Schedule"], + "options": [ + { + "label": "Permanent Hold", + "value": "permanent_hold", + }, + {"label": "Run Schedule", "value": "run_schedule"}, + ], + "title": "Run Mode", + "type": "run_mode", + "values": ["permanent_hold", "run_schedule"], + }, + { + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + }, + "current_value": True, + "labels": ["ON", "OFF"], + "options": [ + {"label": "ON", "value": True}, + {"label": "OFF", "value": False}, + ], + "title": "Scheduling", + "type": "scheduling_enabled", + "values": [True, False], + }, + ], + "temperature": 75, + "type": "xxl_zone", + "zone_status": "", + }, + ], + }, + ], + "entry": {"brand": None, "title": "Mock Title"}, + } From 21aa07e3e58268d6ca1425cd05abcfd847b9872c Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 15 Mar 2022 22:17:51 -0400 Subject: [PATCH 0478/1054] Add Z-Wave thermostat fan entity (#65865) * Add Z-Wave thermostat fan entity * Fix failing test, increase number of entities to 27 * Add tests to improve coverage * Take back unrelated changes to climate.py * Clean up guard clauses, use info.primary_value, and make entity disabled by default * Fix tests * Add more tests for code coverage * Remove unused const * Remove speed parameter from overridden method since it was removed from entity * Address PR comments --- .../components/zwave_js/discovery.py | 12 + homeassistant/components/zwave_js/fan.py | 121 +++++- tests/components/zwave_js/conftest.py | 16 + tests/components/zwave_js/test_fan.py | 369 +++++++++++++++++- tests/components/zwave_js/test_init.py | 2 +- 5 files changed, 517 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 69a3d05539b..e580833da9d 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.sound_switch import ( ) from zwave_js_server.const.command_class.thermostat import ( THERMOSTAT_CURRENT_TEMP_PROPERTY, + THERMOSTAT_FAN_MODE_PROPERTY, THERMOSTAT_MODE_PROPERTY, THERMOSTAT_SETPOINT_PROPERTY, ) @@ -510,6 +511,17 @@ DISCOVERY_SCHEMAS = [ type={"any"}, ), ), + # thermostat fan + ZWaveDiscoverySchema( + platform="fan", + hint="thermostat_fan", + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_FAN_MODE}, + property={THERMOSTAT_FAN_MODE_PROPERTY}, + type={"number"}, + ), + entity_registry_enabled_default=False, + ), # humidifier # hygrostats supporting mode (and optional setpoint) ZWaveDiscoverySchema( diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 0291e6d3c64..585a72fc6de 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -5,15 +5,22 @@ import math from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient -from zwave_js_server.const import TARGET_VALUE_PROPERTY +from zwave_js_server.const import TARGET_VALUE_PROPERTY, CommandClass +from zwave_js_server.const.command_class.thermostat import ( + THERMOSTAT_FAN_OFF_PROPERTY, + THERMOSTAT_FAN_STATE_PROPERTY, +) +from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( @@ -26,11 +33,14 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import FanSpeedDataTemplate from .entity import ZWaveBaseEntity +from .helpers import get_value_of_zwave_value SUPPORTED_FEATURES = SUPPORT_SET_SPEED DEFAULT_SPEED_RANGE = (1, 99) # off is not included +ATTR_FAN_STATE = "fan_state" + async def async_setup_entry( hass: HomeAssistant, @@ -46,6 +56,8 @@ async def async_setup_entry( entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "configured_fan_speed": entities.append(ConfiguredSpeedRangeZwaveFan(config_entry, client, info)) + elif info.platform_hint == "thermostat_fan": + entities.append(ZwaveThermostatFan(config_entry, client, info)) else: entities.append(ZwaveFan(config_entry, client, info)) @@ -224,3 +236,110 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): # the UI handles steps e.g., for a 3-speed fan, you get steps at 33, # 67, and 100. return round(percentage) + + +class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): + """Representation of a Z-Wave thermostat fan.""" + + _fan_mode: ZwaveValue + _fan_off: ZwaveValue | None = None + _fan_state: ZwaveValue | None = None + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the thermostat fan.""" + super().__init__(config_entry, client, info) + + self._fan_mode = self.info.primary_value + + self._fan_off = self.get_zwave_value( + THERMOSTAT_FAN_OFF_PROPERTY, + CommandClass.THERMOSTAT_FAN_MODE, + add_to_watched_value_ids=True, + ) + self._fan_state = self.get_zwave_value( + THERMOSTAT_FAN_STATE_PROPERTY, + CommandClass.THERMOSTAT_FAN_STATE, + add_to_watched_value_ids=True, + ) + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn the device on.""" + if not self._fan_off: + raise HomeAssistantError("Unhandled action turn_on") + await self.info.node.async_set_value(self._fan_off, False) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + if not self._fan_off: + raise HomeAssistantError("Unhandled action turn_off") + await self.info.node.async_set_value(self._fan_off, True) + + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + if (value := get_value_of_zwave_value(self._fan_off)) is None: + return None + return not cast(bool, value) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., auto, smart, interval, favorite.""" + value = get_value_of_zwave_value(self._fan_mode) + if value is None or str(value) not in self._fan_mode.metadata.states: + return None + return cast(str, self._fan_mode.metadata.states[str(value)]) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + + try: + new_state = next( + int(state) + for state, label in self._fan_mode.metadata.states.items() + if label == preset_mode + ) + except StopIteration: + raise ValueError(f"Received an invalid fan mode: {preset_mode}") from None + + await self.info.node.async_set_value(self._fan_mode, new_state) + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes.""" + if not self._fan_mode.metadata.states: + return None + return list(self._fan_mode.metadata.states.values()) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_PRESET_MODE + + @property + def fan_state(self) -> str | None: + """Return the current state, Idle, Running, etc.""" + value = get_value_of_zwave_value(self._fan_state) + if ( + value is None + or self._fan_state is None + or str(value) not in self._fan_state.metadata.states + ): + return None + return cast(str, self._fan_state.metadata.states[str(value)]) + + @property + def extra_state_attributes(self) -> dict[str, str] | None: + """Return the optional state attributes.""" + attrs = {} + + if state := self.fan_state: + attrs[ATTR_FAN_STATE] = state + + return attrs diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 9696c922fb3..a36e3870253 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -676,6 +676,22 @@ def climate_adc_t3000_missing_mode_fixture(client, climate_adc_t3000_state): return node +@pytest.fixture(name="climate_adc_t3000_missing_fan_mode_states") +def climate_adc_t3000_missing_fan_mode_states_fixture(client, climate_adc_t3000_state): + """Mock a climate ADC-T3000 node with missing 'states' metadata on Thermostat Fan Mode.""" + data = copy.deepcopy(climate_adc_t3000_state) + data["name"] = f"{data['name']} missing fan mode states" + for value in data["values"]: + if ( + value["commandClassName"] == "Thermostat Fan Mode" + and value["property"] == "mode" + ): + del value["metadata"]["states"] + node = Node(client, data) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_danfoss_lc_13") def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): """Mock a climate radio danfoss LC-13 node.""" diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 80c057223ac..2535377e9d3 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -3,9 +3,30 @@ import math import pytest from voluptuous.error import MultipleInvalid +from zwave_js_server.const import CommandClass from zwave_js_server.event import Event -from homeassistant.components.fan import ATTR_PERCENTAGE, ATTR_PERCENTAGE_STEP +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + SUPPORT_PRESET_MODE, +) +from homeassistant.components.zwave_js.fan import ATTR_FAN_STATE +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry async def test_generic_fan(hass, client, fan_generic, integration): @@ -304,3 +325,349 @@ async def test_fixed_speeds_fan(hass, client, ge_12730, integration): state = hass.states.get(entity_id) assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + + +async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): + """Test the fan entity for a z-wave fan.""" + node = climate_adc_t3000 + entity_id = "fan.adc_t3000" + + registry = entity_registry.async_get(hass) + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + + # Test enabling entity + updated_entry = registry.async_update_entity(entity_id, disabled_by=None) + assert updated_entry != entry + assert updated_entry.disabled is False + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_FAN_STATE) == "Idle / off" + assert state.attributes.get(ATTR_PRESET_MODE) == "Auto low" + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == SUPPORT_PRESET_MODE + + # Test setting preset mode + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "Low"}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 3, + "commandClassName": "Thermostat Fan Mode", + "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "label": "Thermostat fan mode", + "max": 255, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "states": {"0": "Auto low", "1": "Low", "6": "Circulation"}, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test setting unknown preset mode + with pytest.raises(ValueError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "Turbo"}, + blocking=True, + ) + + client.async_send_command.reset_mock() + + # Test turning off + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 3, + "commandClassName": "Thermostat Fan Mode", + "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, + "endpoint": 0, + "property": "off", + "propertyName": "off", + "metadata": { + "label": "Thermostat fan turned off", + "type": "boolean", + "readable": True, + "writeable": True, + }, + "value": False, + } + assert args["value"] + + client.async_send_command.reset_mock() + + # Test turning on + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 68 + assert args["valueId"] == { + "ccVersion": 3, + "commandClassName": "Thermostat Fan Mode", + "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, + "endpoint": 0, + "property": "off", + "propertyName": "off", + "metadata": { + "label": "Thermostat fan turned off", + "type": "boolean", + "readable": True, + "writeable": True, + }, + "value": False, + } + assert not args["value"] + + client.async_send_command.reset_mock() + + # Test fan state update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Thermostat Fan State", + "commandClass": CommandClass.THERMOSTAT_FAN_STATE.value, + "endpoint": 0, + "property": "state", + "newValue": 4, + "prevValue": 0, + "propertyName": "state", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_FAN_STATE) == "Circulation mode" + + client.async_send_command.reset_mock() + + # Test unknown fan state update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Thermostat Fan State", + "commandClass": CommandClass.THERMOSTAT_FAN_STATE.value, + "endpoint": 0, + "property": "state", + "newValue": 99, + "prevValue": 0, + "propertyName": "state", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert not state.attributes.get(ATTR_FAN_STATE) + + client.async_send_command.reset_mock() + + # Test fan mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Thermostat Fan Mode", + "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, + "endpoint": 0, + "property": "mode", + "newValue": 1, + "prevValue": 0, + "propertyName": "mode", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state.attributes.get(ATTR_PRESET_MODE) == "Low" + + client.async_send_command.reset_mock() + + # Test fan mode update from value updated event for an unknown mode + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Thermostat Fan Mode", + "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, + "endpoint": 0, + "property": "mode", + "newValue": 79, + "prevValue": 0, + "propertyName": "mode", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert not state.attributes.get(ATTR_PRESET_MODE) + + client.async_send_command.reset_mock() + + # Test fan mode turned off update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 68, + "args": { + "commandClassName": "Thermostat Fan Mode", + "commandClass": CommandClass.THERMOSTAT_FAN_MODE.value, + "endpoint": 0, + "property": "off", + "newValue": True, + "prevValue": False, + "propertyName": "off", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +async def test_thermostat_fan_without_off( + hass, client, climate_radio_thermostat_ct100_plus, integration +): + """Test the fan entity for a z-wave fan without "off" property.""" + entity_id = "fan.z_wave_thermostat" + + registry = entity_registry.async_get(hass) + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + + # Test enabling entity + updated_entry = registry.async_update_entity(entity_id, disabled_by=None) + assert updated_entry != entry + assert updated_entry.disabled is False + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + + # Test turning off + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + assert state.state == STATE_UNKNOWN + + client.async_send_command.reset_mock() + + # Test turning on + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + assert state.state == STATE_UNKNOWN + + client.async_send_command.reset_mock() + + +async def test_thermostat_fan_without_preset_modes( + hass, client, climate_adc_t3000_missing_fan_mode_states, integration +): + """Test the fan entity for a z-wave fan without "states" metadata.""" + entity_id = "fan.adc_t3000_missing_fan_mode_states" + + registry = entity_registry.async_get(hass) + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + + # Test enabling entity + updated_entry = registry.async_update_entity(entity_id, disabled_by=None) + assert updated_entry != entry + assert updated_entry.disabled is False + + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + + assert not state.attributes.get(ATTR_PRESET_MODE) + assert not state.attributes.get(ATTR_PRESET_MODES) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 1b3a2d1204f..7b3fd773839 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -811,7 +811,7 @@ async def test_removed_device( # Check how many entities there are ent_reg = er.async_get(hass) entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) - assert len(entity_entries) == 28 + assert len(entity_entries) == 29 # Remove a node and reload the entry old_node = client.driver.controller.nodes.pop(13) From 984e30075bae1bb757eb1cf62bc4f87d0f37e0c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 02:18:55 -0700 Subject: [PATCH 0479/1054] Validate TTS base url (#68212) * Validate TTS base url * Update tests/components/tts/test_init.py Co-authored-by: Joakim Plate Co-authored-by: Joakim Plate --- homeassistant/components/tts/__init__.py | 14 ++++++++- tests/components/tts/test_init.py | 40 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 5e6629ca2a2..c001fb6b89b 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -17,6 +17,7 @@ from aiohttp import web import mutagen from mutagen.id3 import ID3, TextFrame as ID3Text import voluptuous as vol +import yarl from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( @@ -41,6 +42,7 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util.network import normalize_url from homeassistant.util.yaml import load_yaml from .const import DOMAIN @@ -92,6 +94,16 @@ def _deprecated_platform(value): return value +def valid_base_url(value: str) -> str: + """Validate base url, return value.""" + url = yarl.URL(cv.url(value)) + + if url.path != "/": + raise vol.Invalid("Path should be empty") + + return normalize_url(value) + + PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( { vol.Required(CONF_PLATFORM): vol.All(cv.string, _deprecated_platform), @@ -100,7 +112,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( vol.Optional(CONF_TIME_MEMORY, default=DEFAULT_TIME_MEMORY): vol.All( vol.Coerce(int), vol.Range(min=60, max=57600) ), - vol.Optional(CONF_BASE_URL): cv.string, + vol.Optional(CONF_BASE_URL): valid_base_url, vol.Optional(CONF_SERVICE_NAME): cv.string, } ) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 9f1cc849a1f..5543e2d82f5 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -3,6 +3,7 @@ from http import HTTPStatus from unittest.mock import PropertyMock, patch import pytest +import voluptuous as vol import yarl from homeassistant.components import tts @@ -16,6 +17,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component +from homeassistant.util.network import normalize_url from tests.common import assert_setup_component, async_mock_service @@ -689,3 +691,41 @@ async def test_tags_with_wave(hass, demo_provider): ) assert tagged_data != demo_data + + +@pytest.mark.parametrize( + "value", + ( + "http://example.local:8123", + "http://example.local", + "http://example.local:80", + "https://example.com", + "https://example.com:443", + "https://example.com:8123", + ), +) +def test_valid_base_url(value): + """Test we validate base urls.""" + assert tts.valid_base_url(value) == normalize_url(value) + # Test we strip trailing `/` + assert tts.valid_base_url(value + "/") == normalize_url(value) + + +@pytest.mark.parametrize( + "value", + ( + "http://example.local:8123/sub-path", + "http://example.local/sub-path", + "https://example.com/sub-path", + "https://example.com:8123/sub-path", + "mailto:some@email", + "http:example.com", + "http:/example.com", + "http//example.com", + "example.com", + ), +) +def test_invalid_base_url(value): + """Test we catch bad base urls.""" + with pytest.raises(vol.Invalid): + tts.valid_base_url(value) From 62c4fed5494f560794e8ce67bc00905eb5169c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 16 Mar 2022 10:44:06 +0100 Subject: [PATCH 0480/1054] Update aioairzone to v0.1.0 (#68194) --- homeassistant/components/airzone/__init__.py | 10 +++++++--- homeassistant/components/airzone/manifest.json | 2 +- homeassistant/components/airzone/sensor.py | 12 +++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 687e4426d76..24a2978d88f 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -1,8 +1,10 @@ """The Airzone integration.""" from __future__ import annotations +from typing import Any + from aioairzone.common import ConnectionOptions -from aioairzone.const import AZD_ZONES +from aioairzone.const import AZD_ID, AZD_NAME, AZD_SYSTEM, AZD_ZONES from aioairzone.localapi_device import AirzoneLocalApi from homeassistant.config_entries import ConfigEntry @@ -26,7 +28,7 @@ class AirzoneEntity(CoordinatorEntity): coordinator: AirzoneUpdateCoordinator, entry: ConfigEntry, system_zone_id: str, - zone_name: str, + zone_data: dict[str, Any], ) -> None: """Initialize.""" super().__init__(coordinator) @@ -34,9 +36,11 @@ class AirzoneEntity(CoordinatorEntity): self._attr_device_info: DeviceInfo = { "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, "manufacturer": MANUFACTURER, - "name": f"Airzone [{system_zone_id}] {zone_name}", + "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", } + self.system_id = zone_data[AZD_SYSTEM] self.system_zone_id = system_zone_id + self.zone_id = zone_data[AZD_ID] def get_zone_value(self, key): """Return zone value by key.""" diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index c6836a63ee5..49167699e42 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.0.2"], + "requirements": ["aioairzone==0.1.0"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index 3d8912641cb..931e74a495d 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -1,7 +1,7 @@ """Support for the Airzone sensors.""" from __future__ import annotations -from typing import Final +from typing import Any, Final from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_TEMP_UNIT, AZD_ZONES @@ -50,8 +50,6 @@ async def async_setup_entry( sensors = [] for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): - zone_name = zone_data[AZD_NAME] - for description in SENSOR_TYPES: if description.key in zone_data: sensors.append( @@ -60,7 +58,7 @@ async def async_setup_entry( description, entry, system_zone_id, - zone_name, + zone_data, ) ) @@ -76,11 +74,11 @@ class AirzoneSensor(AirzoneEntity, SensorEntity): description: SensorEntityDescription, entry: ConfigEntry, system_zone_id: str, - zone_name: str, + zone_data: dict[str, Any], ) -> None: """Initialize.""" - super().__init__(coordinator, entry, system_zone_id, zone_name) - self._attr_name = f"{zone_name} {description.name}" + super().__init__(coordinator, entry, system_zone_id, zone_data) + self._attr_name = f"{zone_data[AZD_NAME]} {description.name}" self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}" self.entity_description = description diff --git a/requirements_all.txt b/requirements_all.txt index 05a46dfa62d..e17167b3294 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -107,7 +107,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.airzone -aioairzone==0.0.2 +aioairzone==0.1.0 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e6ef229972..cb1725a16ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -88,7 +88,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.airzone -aioairzone==0.0.2 +aioairzone==0.1.0 # homeassistant.components.ambient_station aioambient==2021.11.0 From aa82f96c160928e0c83dc7736fd498769e30e34d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Mar 2022 11:21:51 +0100 Subject: [PATCH 0481/1054] Use Platform StrEnum in Hassfest manifest plugin (#68234) --- script/hassfest/manifest.py | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 64239a7fae1..5b344ed505d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -12,6 +12,7 @@ from awesomeversion import ( import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.const import Platform from homeassistant.helpers import config_validation as cv from .model import Config, Integration @@ -33,36 +34,25 @@ SUPPORTED_IOT_CLASSES = [ # List of integrations that are supposed to have no IoT class NO_IOT_CLASS = [ - "air_quality", - "alarm_control_panel", + *{platform.value for platform in Platform}, "api", "auth", "automation", - "binary_sensor", "blueprint", - "button", - "calendar", - "camera", - "climate", "color_extractor", "config", "configurator", "counter", - "cover", "default_config", "device_automation", "device_tracker", "diagnostics", "discovery", "downloader", - "fan", "ffmpeg", "frontend", - "geo_location", "history", "homeassistant", - "humidifier", - "image_processing", "image", "input_boolean", "input_button", @@ -72,18 +62,12 @@ NO_IOT_CLASS = [ "input_text", "intent_script", "intent", - "light", - "lock", "logbook", "logger", "lovelace", - "mailbox", "map", - "media_player", "media_source", "my", - "notify", - "number", "onboarding", "panel_custom", "panel_iframe", @@ -91,25 +75,14 @@ NO_IOT_CLASS = [ "profiler", "proxy", "python_script", - "remote", "safe_mode", - "scene", "script", "search", - "select", - "sensor", - "siren", - "stt", - "switch", "system_health", "system_log", "tag", "timer", "trace", - "tts", - "vacuum", - "water_heater", - "weather", "webhook", "websocket_api", "zone", From f0dba8ec70f37c2cf9435b5be75a7d3858875aa1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 16 Mar 2022 12:57:56 +0100 Subject: [PATCH 0482/1054] Add scaffold for helper integration config flow (#67803) --- script/scaffold/__main__.py | 2 + script/scaffold/docs.py | 4 + script/scaffold/gather_info.py | 5 + script/scaffold/generate.py | 13 ++ .../integration/__init__.py | 39 ++++++ .../integration/config_flow.py | 46 +++++++ .../config_flow_helper/integration/const.py | 3 + .../config_flow_helper/integration/sensor.py | 38 ++++++ .../tests/test_config_flow.py | 116 ++++++++++++++++++ .../config_flow_helper/tests/test_init.py | 50 ++++++++ 10 files changed, 316 insertions(+) create mode 100644 script/scaffold/templates/config_flow_helper/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_helper/integration/config_flow.py create mode 100644 script/scaffold/templates/config_flow_helper/integration/const.py create mode 100644 script/scaffold/templates/config_flow_helper/integration/sensor.py create mode 100644 script/scaffold/templates/config_flow_helper/tests/test_config_flow.py create mode 100644 script/scaffold/templates/config_flow_helper/tests/test_init.py diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 0504cdb8b37..05ef300d33e 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -59,6 +59,8 @@ def main(): # If it's a new integration and it's not a config flow, # create a config flow too. if not args.template.startswith("config_flow"): + if info.helper: + template = "config_flow_helper" if info.oauth2: template = "config_flow_oauth2" elif info.authentication or not info.discoverable: diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 3a871301b97..6e31f15e6d4 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -6,6 +6,10 @@ DATA = { "title": "Config Flow", "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html", }, + "config_flow_helper": { + "title": "Helper Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#helper", + }, "config_flow_discovery": { "title": "Discoverable Config Flow", "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication", diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 8442650dce4..85a94459d06 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -119,6 +119,11 @@ More info @ https://developers.home-assistant.io/docs/creating_integration_manif "default": "no", **YES_NO, }, + "helper": { + "prompt": "Is this a helper integration? (yes/no)", + "default": "no", + **YES_NO, + }, "oauth2": { "prompt": "Can the user authenticate the device using OAuth2? (yes/no)", "default": "no", diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 122d8570dc1..56b5252cfad 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -150,6 +150,19 @@ def _custom_tasks(template, info: Info) -> None: }, ) + elif template == "config_flow_helper": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "step": { + "init": { + "description": "Select the sensor for the NEW_NAME.", + "data": {"entity_id": "Sensor entity"}, + }, + }, + }, + ) + elif template == "config_flow_oauth2": info.update_manifest(config_flow=True, dependencies=["http"]) info.update_strings( diff --git a/script/scaffold/templates/config_flow_helper/integration/__init__.py b/script/scaffold/templates/config_flow_helper/integration/__init__.py new file mode 100644 index 00000000000..e0d115559a1 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/__init__.py @@ -0,0 +1,39 @@ +"""The NEW_NAME integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up NEW_NAME from a config entry.""" + # TODO Optionally store an object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = ... + + # TODO Optionally validate config entry options before setting up platform + + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + + # TODO Remove if the integration does not have an options flow + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +# TODO Remove if the integration does not have an options flow +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms( + entry, (Platform.SENSOR,) + ): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py new file mode 100644 index 00000000000..ad0e8a3eb90 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -0,0 +1,46 @@ +"""Config flow for NEW_NAME integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowStep, +) + +from .const import DOMAIN + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): selector.selector( + {"entity": {"domain": "sensor"}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required("name"): selector.selector({"text": {}}), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} + +OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} + + +class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for NEW_NAME.""" + + config_flow = CONFIG_FLOW + # TODO remove the options_flow if the integration does not have an options flow + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) if "name" in options else "" diff --git a/script/scaffold/templates/config_flow_helper/integration/const.py b/script/scaffold/templates/config_flow_helper/integration/const.py new file mode 100644 index 00000000000..e8a1c494d49 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" diff --git a/script/scaffold/templates/config_flow_helper/integration/sensor.py b/script/scaffold/templates/config_flow_helper/integration/sensor.py new file mode 100644 index 00000000000..fbf92bfdd6b --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/integration/sensor.py @@ -0,0 +1,38 @@ +"""Sensor platform for NEW_NAME integration.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize NEW_NAME config entry.""" + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + # TODO Optionally validate config entry options before creating entity + name = config_entry.title + unique_id = config_entry.entry_id + + async_add_entities([NEW_DOMAINSensorEntity(unique_id, name, entity_id)]) + + +class NEW_DOMAINSensorEntity(SensorEntity): + """NEW_DOMAIN Sensor.""" + + def __init__(self, unique_id: str, name: str, wrapped_entity_id: str) -> None: + """Initialize NEW_DOMAIN Sensor.""" + super().__init__() + self._wrapped_entity_id = wrapped_entity_id + self._attr_name = name + self._attr_unique_id = unique_id diff --git a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py new file mode 100644 index 00000000000..b7bef2c63f4 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py @@ -0,0 +1,116 @@ +"""Test the NEW_NAME config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensor_entity_id = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.NEW_DOMAIN.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"name": "My NEW_DOMAIN", "entity_id": input_sensor_entity_id}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My NEW_DOMAIN" + assert result["data"] == {} + assert result["options"] == { + "entity_id": input_sensor_entity_id, + "name": "My NEW_DOMAIN", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "entity_id": input_sensor_entity_id, + "name": "My NEW_DOMAIN", + } + assert config_entry.title == "My NEW_DOMAIN" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + input_sensor_1_entity_id = "sensor.input1" + input_sensor_2_entity_id = "sensor.input2" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": input_sensor_1_entity_id, + "name": "My NEW_DOMAIN", + }, + title="My NEW_DOMAIN", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "entity_id") == input_sensor_1_entity_id + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entity_id": input_sensor_2_entity_id, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entity_id": input_sensor_2_entity_id, + "name": "My NEW_DOMAIN", + } + assert config_entry.data == {} + assert config_entry.options == { + "entity_id": input_sensor_2_entity_id, + "name": "My NEW_DOMAIN", + } + assert config_entry.title == "My NEW_DOMAIN" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # TODO Check the state of the entity has changed as expected + state = hass.states.get(f"{platform}.my_NEW_DOMAIN") + assert state.attributes == {} diff --git a/script/scaffold/templates/config_flow_helper/tests/test_init.py b/script/scaffold/templates/config_flow_helper/tests/test_init.py new file mode 100644 index 00000000000..0e86874c745 --- /dev/null +++ b/script/scaffold/templates/config_flow_helper/tests/test_init.py @@ -0,0 +1,50 @@ +"""Test the NEW_NAME integration.""" +import pytest + +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + input_sensor_entity_id = "sensor.input" + registry = er.async_get(hass) + NEW_DOMAIN_entity_id = f"{platform}.my_NEW_DOMAIN" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": input_sensor_entity_id, + "name": "My NEW_DOMAIN", + }, + title="My NEW_DOMAIN", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(NEW_DOMAIN_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(NEW_DOMAIN_entity_id) + # TODO Check the state of the entity has changed as expected + assert state.state == "unknown" + assert state.attributes == {} + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(NEW_DOMAIN_entity_id) is None + assert registry.async_get(NEW_DOMAIN_entity_id) is None From 2c8bf006ebdee8fb98c1d8a8a72fb7798c4ea71d Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 16 Mar 2022 13:14:11 +0100 Subject: [PATCH 0483/1054] Fix precision of vicare target & current temperature (#66456) --- homeassistant/components/vicare/climate.py | 12 +++++++++++- homeassistant/components/vicare/water_heater.py | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 8320db73ad4..02998343744 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -24,7 +24,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -310,6 +315,11 @@ class ViCareClimate(ClimateEntity): @property def precision(self): """Return the precision of the system.""" + return PRECISION_TENTHS + + @property + def target_temperature_step(self) -> float: + """Set target temperature step to wholes.""" return PRECISION_WHOLE def set_temperature(self, **kwargs): diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 139c8f75e7f..e1e44188d28 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -14,7 +14,12 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -191,10 +196,15 @@ class ViCareWater(WaterHeaterEntity): """Return the maximum temperature.""" return VICARE_TEMP_WATER_MAX + @property + def target_temperature_step(self) -> float: + """Set target temperature step to wholes.""" + return PRECISION_WHOLE + @property def precision(self): """Return the precision of the system.""" - return PRECISION_WHOLE + return PRECISION_TENTHS @property def current_operation(self): From d8c966fd0a94b002b603619666178b0f0c83a42b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 16 Mar 2022 11:35:31 -0500 Subject: [PATCH 0484/1054] Fix Spotify podcasts & Plex `allow_multiple` on Sonos (#68131) * Bump soco to 0.27.0 * Bump soco to 0.27.1 --- 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 6f482e92dc9..9144ca559f2 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": ["soco==0.26.4"], + "requirements": ["soco==0.27.1"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index e17167b3294..01cfc291729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2160,7 +2160,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.26.4 +soco==0.27.1 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb1725a16ae..bfae448d44b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1373,7 +1373,7 @@ smarthab==0.21 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.26.4 +soco==0.27.1 # homeassistant.components.solaredge solaredge==0.0.2 From aa57907c1878b5d7bdaf258e0b135c189abb8f27 Mon Sep 17 00:00:00 2001 From: d6e Date: Wed, 16 Mar 2022 13:16:38 -0700 Subject: [PATCH 0485/1054] Improve logging for Discord integration (#68160) --- homeassistant/components/discord/notify.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 2e13b68225c..7c4b10f5662 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -49,8 +49,12 @@ class DiscordNotificationService(BaseNotificationService): def file_exists(self, filename): """Check if a file exists on disk and is in authorized path.""" if not self.hass.config.is_allowed_path(filename): + _LOGGER.warning("Path not allowed: %s", filename) return False - return os.path.isfile(filename) + if not os.path.isfile(filename): + _LOGGER.warning("Not a file: %s", filename) + return False + return True async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" @@ -98,8 +102,6 @@ class DiscordNotificationService(BaseNotificationService): if image_exists: images.append(image) - else: - _LOGGER.warning("Image not found: %s", image) await discord_bot.login(self.token) From f6af93ae3516a9b16dca9f9058dbbed6b2867e5a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Mar 2022 14:14:50 -0700 Subject: [PATCH 0486/1054] Add support for menu step (#68203) --- homeassistant/data_entry_flow.py | 36 +++++++++++++++++++- homeassistant/helpers/data_entry_flow.py | 5 ++- script/hassfest/translations.py | 1 + tests/test_data_entry_flow.py | 43 ++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 46311af94e0..080ef86fac6 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -21,6 +21,7 @@ RESULT_TYPE_EXTERNAL_STEP = "external" RESULT_TYPE_EXTERNAL_STEP_DONE = "external_done" RESULT_TYPE_SHOW_PROGRESS = "progress" RESULT_TYPE_SHOW_PROGRESS_DONE = "progress_done" +RESULT_TYPE_MENU = "menu" # Event that is fired when a flow is progressed via external or progress source. EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed" @@ -82,6 +83,7 @@ class FlowResult(TypedDict, total=False): result: Any last_step: bool | None options: Mapping[str, Any] + menu_options: list[str] | Mapping[str, Any] @callback @@ -249,7 +251,15 @@ class FlowManager(abc.ABC): if cur_step.get("data_schema") is not None and user_input is not None: user_input = cur_step["data_schema"](user_input) - result = await self._async_handle_step(flow, cur_step["step_id"], user_input) + # Handle a menu navigation choice + if cur_step["type"] == RESULT_TYPE_MENU and user_input: + result = await self._async_handle_step( + flow, user_input["next_step_id"], None + ) + else: + result = await self._async_handle_step( + flow, cur_step["step_id"], user_input + ) if cur_step["type"] in (RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_SHOW_PROGRESS): if cur_step["type"] == RESULT_TYPE_EXTERNAL_STEP and result["type"] not in ( @@ -343,6 +353,7 @@ class FlowManager(abc.ABC): RESULT_TYPE_EXTERNAL_STEP_DONE, RESULT_TYPE_SHOW_PROGRESS, RESULT_TYPE_SHOW_PROGRESS_DONE, + RESULT_TYPE_MENU, ): raise ValueError(f"Handler returned incorrect type: {result['type']}") @@ -352,6 +363,7 @@ class FlowManager(abc.ABC): RESULT_TYPE_EXTERNAL_STEP_DONE, RESULT_TYPE_SHOW_PROGRESS, RESULT_TYPE_SHOW_PROGRESS_DONE, + RESULT_TYPE_MENU, ): flow.cur_step = result return result @@ -507,6 +519,28 @@ class FlowHandler: "step_id": next_step_id, } + @callback + def async_show_menu( + self, + *, + step_id: str, + menu_options: list[str] | dict[str, str], + description_placeholders: dict | None = None, + ) -> FlowResult: + """Show a navigation menu to the user. + + Options dict maps step_id => i18n label + """ + return { + "type": RESULT_TYPE_MENU, + "flow_id": self.flow_id, + "handler": self.handler, + "step_id": step_id, + "data_schema": vol.Schema({"next_step_id": vol.In(menu_options)}), + "menu_options": menu_options, + "description_placeholders": description_placeholders, + } + @callback def _create_abort_data( diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 07f5e640ea3..808207c7f30 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -6,6 +6,7 @@ from typing import Any from aiohttp import web import voluptuous as vol +import voluptuous_serialize from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView @@ -32,11 +33,9 @@ class _BaseFlowManagerView(HomeAssistantView): data.pop("data") return data - if result["type"] != data_entry_flow.RESULT_TYPE_FORM: + if "data_schema" not in result: return result - import voluptuous_serialize # pylint: disable=import-outside-toplevel - data = result.copy() if (schema := data["data_schema"]) is None: diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index d174f238217..b721b4d708d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -110,6 +110,7 @@ def gen_data_entry_schema( step_title_class("title"): cv.string_with_no_html, vol.Optional("description"): cv.string_with_no_html, vol.Optional("data"): {str: cv.string_with_no_html}, + vol.Optional("menu_options"): {str: cv.string_with_no_html}, } }, vol.Optional("error"): {str: cv.string_with_no_html}, diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index b4b40b6b6c6..18d5469a162 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -487,3 +487,46 @@ async def test_abort_raises_unknown_flow_if_not_in_progress(manager): """Test abort raises UnknownFlow if the flow is not in progress.""" with pytest.raises(data_entry_flow.UnknownFlow): await manager.async_abort("wrong_flow_id") + + +@pytest.mark.parametrize( + "menu_options", + (["target1", "target2"], {"target1": "Target 1", "target2": "Target 2"}), +) +async def test_show_menu(hass, manager, menu_options): + """Test show menu.""" + manager.hass = hass + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + data = None + task_one_done = False + + async def async_step_init(self, user_input=None): + return self.async_show_menu( + step_id="init", + menu_options=menu_options, + description_placeholders={"name": "Paulus"}, + ) + + async def async_step_target1(self, user_input=None): + return self.async_show_form(step_id="target1") + + async def async_step_target2(self, user_input=None): + return self.async_show_form(step_id="target2") + + result = await manager.async_init("test") + assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["menu_options"] == menu_options + assert result["description_placeholders"] == {"name": "Paulus"} + assert len(manager.async_progress()) == 1 + assert len(manager.async_progress_by_handler("test")) == 1 + assert manager.async_get(result["flow_id"])["handler"] == "test" + + # Mimic picking a step + result = await manager.async_configure( + result["flow_id"], {"next_step_id": "target1"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "target1" From 15d9233c77780cd7fe4e6fd6f6dce70be3f4eb1f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 16 Mar 2022 18:30:04 -0400 Subject: [PATCH 0487/1054] Bump PyNaCl to 1.5.0 (#68226) --- homeassistant/components/mobile_app/manifest.json | 2 +- homeassistant/components/owntracks/helper.py | 2 +- homeassistant/components/owntracks/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 6cb4e964c9b..eb0bf100aee 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,7 +3,7 @@ "name": "Mobile App", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": ["PyNaCl==1.4.0"], + "requirements": ["PyNaCl==1.5.0"], "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/owntracks/helper.py b/homeassistant/components/owntracks/helper.py index b6ed307112c..a9499059ba0 100644 --- a/homeassistant/components/owntracks/helper.py +++ b/homeassistant/components/owntracks/helper.py @@ -2,7 +2,7 @@ try: import nacl except ImportError: - nacl = None + nacl = None # type: ignore[assignment] def supports_encryption() -> bool: diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 1b502481764..4a84f6a706c 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,7 +3,7 @@ "name": "OwnTracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": ["PyNaCl==1.4.0"], + "requirements": ["PyNaCl==1.5.0"], "dependencies": ["webhook"], "after_dependencies": ["mqtt", "cloud"], "codeowners": [], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5888154207c..7315a705ef1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,5 @@ PyJWT==2.1.0 -PyNaCl==1.4.0 +PyNaCl==1.5.0 aiodiscover==1.4.8 aiohttp==3.8.1 aiohttp_cors==0.7.0 diff --git a/requirements_all.txt b/requirements_all.txt index 01cfc291729..758e30c123a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -21,7 +21,7 @@ PyMVGLive==1.1.4 # homeassistant.components.mobile_app # homeassistant.components.owntracks -PyNaCl==1.4.0 +PyNaCl==1.5.0 # homeassistant.auth.mfa_modules.totp # homeassistant.components.homekit diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfae448d44b..8e318d8f1f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -17,7 +17,7 @@ PyFlick==0.0.2 # homeassistant.components.mobile_app # homeassistant.components.owntracks -PyNaCl==1.4.0 +PyNaCl==1.5.0 # homeassistant.auth.mfa_modules.totp # homeassistant.components.homekit From 3ecc914f290365b24724321221268b225f08c495 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 16 Mar 2022 18:40:24 -0500 Subject: [PATCH 0488/1054] 20220316.0 (#68276) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 61796f489b2..31c912db8a5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220312.0" + "home-assistant-frontend==20220316.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7315a705ef1..43c9af11f5c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220312.0 +home-assistant-frontend==20220316.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 758e30c123a..ac73a7a4f49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -806,7 +806,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220312.0 +home-assistant-frontend==20220316.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e318d8f1f7..7da671c6c11 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220312.0 +home-assistant-frontend==20220316.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From d6f5f0c794bc0a9a267c7a31bc7a64761489d4b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 17 Mar 2022 07:44:14 +0100 Subject: [PATCH 0489/1054] Bump samsungtvws to 2.4.0 (#68225) Co-authored-by: epenet --- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/requirements.py | 2 +- tests/hassfest/test_requirements.py | 1 + 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index b15a5e5d56e..3ee8fc44c94 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws[async]==2.3.0", + "samsungtvws[async,encrypted]==2.4.0", "wakeonlan==2.0.1" ], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ac73a7a4f49..7712bd20968 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2087,7 +2087,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.3.0 +samsungtvws[async,encrypted]==2.4.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7da671c6c11..b3636779bc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1333,7 +1333,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async]==2.3.0 +samsungtvws[async,encrypted]==2.4.0 # homeassistant.components.dhcp scapy==2.4.5 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 09afd11b147..510a70f30ce 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -22,7 +22,7 @@ IGNORE_PACKAGES = { commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS } PACKAGE_REGEX = re.compile( - r"^(?:--.+\s)?([-_\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" + r"^(?:--.+\s)?([-_,\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" ) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index 91496b7fa6f..a529c0769d6 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -73,6 +73,7 @@ def test_validate_requirements_format_successful(integration: Integration): integration.manifest["requirements"] = [ "test_package==1.2.3", "test_package[async]==1.2.3", + "test_package[async,encrypted]==1.2.3", ] assert validate_requirements_format(integration) assert len(integration.errors) == 0 From d021222d6de163972e2fe8755a9973b86abd2bab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 17 Mar 2022 08:51:03 +0100 Subject: [PATCH 0490/1054] Bump renault-api to 0.1.10 (#68260) Co-authored-by: epenet --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 71e2e7d64b8..d41686f0c1f 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/renault", "requirements": [ - "renault-api==0.1.9" + "renault-api==0.1.10" ], "codeowners": [ "@epenet" diff --git a/requirements_all.txt b/requirements_all.txt index 7712bd20968..7d6ed83cc29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2033,7 +2033,7 @@ raspyrfm-client==1.2.8 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.9 +renault-api==0.1.10 # homeassistant.components.python_script restrictedpython==5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3636779bc9..ca844247dd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1300,7 +1300,7 @@ radios==0.1.1 regenmaschine==2022.01.0 # homeassistant.components.renault -renault-api==0.1.9 +renault-api==0.1.10 # homeassistant.components.python_script restrictedpython==5.2 From aabfa08834103e0bc60dae96d290c4a703a8bab0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Mar 2022 01:51:07 -0700 Subject: [PATCH 0491/1054] Add if subscription is active to cloud status (#68266) --- homeassistant/components/cloud/http_api.py | 7 ++++++- tests/components/cloud/test_http_api.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 0ea1fc1d3f3..6086cef703a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -420,7 +420,11 @@ async def _account_data(hass: HomeAssistant, cloud: Cloud): """Generate the auth data JSON response.""" if not cloud.is_logged_in: - return {"logged_in": False, "cloud": STATE_DISCONNECTED} + return { + "logged_in": False, + "cloud": STATE_DISCONNECTED, + "http_use_ssl": hass.config.api.use_ssl, + } claims = cloud.claims client = cloud.client @@ -457,6 +461,7 @@ async def _account_data(hass: HomeAssistant, cloud: Cloud): "remote_connected": remote.is_connected, "remote_domain": remote.instance_domain, "http_use_ssl": hass.config.api.use_ssl, + "active_subscription": not cloud.subscription_expired, } diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 23605268649..4d0729d72b2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -430,6 +430,7 @@ async def test_websocket_status( "remote_connected": False, "remote_certificate": None, "http_use_ssl": False, + "active_subscription": False, } @@ -438,7 +439,11 @@ async def test_websocket_status_not_logged_in(hass, hass_ws_client): client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "cloud/status"}) response = await client.receive_json() - assert response["result"] == {"logged_in": False, "cloud": "disconnected"} + assert response["result"] == { + "logged_in": False, + "cloud": "disconnected", + "http_use_ssl": False, + } async def test_websocket_subscription_info( From fc693001a162ab8d0bed70098ead16a788268dfb Mon Sep 17 00:00:00 2001 From: wchan-ranelagh <71867228+wchan-ranelagh@users.noreply.github.com> Date: Thu, 17 Mar 2022 02:10:46 -0700 Subject: [PATCH 0492/1054] Change default timeout for operations in SNMP component (#68230) --- homeassistant/components/snmp/const.py | 1 + homeassistant/components/snmp/sensor.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/snmp/const.py b/homeassistant/components/snmp/const.py index b3a93cfe98b..e51bbc33b90 100644 --- a/homeassistant/components/snmp/const.py +++ b/homeassistant/components/snmp/const.py @@ -16,6 +16,7 @@ DEFAULT_HOST = "localhost" DEFAULT_NAME = "SNMP" DEFAULT_PORT = "161" DEFAULT_PRIV_PROTOCOL = "none" +DEFAULT_TIMEOUT = 8 DEFAULT_VERSION = "1" DEFAULT_VARTYPE = "none" diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 3530ca180a4..ba111ffc9bc 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -48,6 +48,7 @@ from .const import ( DEFAULT_NAME, DEFAULT_PORT, DEFAULT_PRIV_PROTOCOL, + DEFAULT_TIMEOUT, DEFAULT_VERSION, MAP_AUTH_PROTOCOLS, MAP_PRIV_PROTOCOLS, @@ -125,14 +126,14 @@ async def async_setup_platform( authProtocol=getattr(hlapi, MAP_AUTH_PROTOCOLS[authproto]), privProtocol=getattr(hlapi, MAP_PRIV_PROTOCOLS[privproto]), ), - UdpTransportTarget((host, port)), + UdpTransportTarget((host, port), timeout=DEFAULT_TIMEOUT), ContextData(), ] else: request_args = [ SnmpEngine(), CommunityData(community, mpModel=SNMP_VERSIONS[version]), - UdpTransportTarget((host, port)), + UdpTransportTarget((host, port), timeout=DEFAULT_TIMEOUT), ContextData(), ] From 38d8332e925e4980f10e1fc39d8c6abc78460db6 Mon Sep 17 00:00:00 2001 From: Myles Eftos Date: Thu, 17 Mar 2022 20:15:47 +1100 Subject: [PATCH 0493/1054] Add amberelectric price descriptors (#67981) Co-authored-by: Martin Hjelmare --- .../components/amberelectric/coordinator.py | 30 ++++++++++++ .../components/amberelectric/manifest.json | 2 +- .../components/amberelectric/sensor.py | 22 ++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/amberelectric/helpers.py | 5 +- .../amberelectric/test_binary_sensor.py | 6 +-- .../amberelectric/test_coordinator.py | 19 +++++++- tests/components/amberelectric/test_sensor.py | 47 ++++++++++++++++--- 9 files changed, 118 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/amberelectric/coordinator.py b/homeassistant/components/amberelectric/coordinator.py index 904da59f65c..75cf3fd4360 100644 --- a/homeassistant/components/amberelectric/coordinator.py +++ b/homeassistant/components/amberelectric/coordinator.py @@ -10,6 +10,7 @@ from amberelectric.model.actual_interval import ActualInterval from amberelectric.model.channel import ChannelType from amberelectric.model.current_interval import CurrentInterval from amberelectric.model.forecast_interval import ForecastInterval +from amberelectric.model.interval import Descriptor from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -44,6 +45,27 @@ def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> return interval.channel_type == ChannelType.FEED_IN +def normalize_descriptor(descriptor: Descriptor) -> str | None: + """Return the snake case versions of descriptor names. Returns None if the name is not recognized.""" + if descriptor is None: + return None + if descriptor.value == "spike": + return "spike" + if descriptor.value == "high": + return "high" + if descriptor.value == "neutral": + return "neutral" + if descriptor.value == "low": + return "low" + if descriptor.value == "veryLow": + return "very_low" + if descriptor.value == "extremelyLow": + return "extremely_low" + if descriptor.value == "negative": + return "negative" + return None + + class AmberUpdateCoordinator(DataUpdateCoordinator): """AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read.""" @@ -65,6 +87,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): result: dict[str, dict[str, Any]] = { "current": {}, + "descriptors": {}, "forecasts": {}, "grid": {}, } @@ -81,6 +104,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed("No general channel configured") result["current"]["general"] = general[0] + result["descriptors"]["general"] = normalize_descriptor(general[0].descriptor) result["forecasts"]["general"] = [ interval for interval in forecasts if is_general(interval) ] @@ -92,6 +116,9 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): ] if controlled_load: result["current"]["controlled_load"] = controlled_load[0] + result["descriptors"]["controlled_load"] = normalize_descriptor( + controlled_load[0].descriptor + ) result["forecasts"]["controlled_load"] = [ interval for interval in forecasts if is_controlled_load(interval) ] @@ -99,6 +126,9 @@ class AmberUpdateCoordinator(DataUpdateCoordinator): feed_in = [interval for interval in current if is_feed_in(interval)] if feed_in: result["current"]["feed_in"] = feed_in[0] + result["descriptors"]["feed_in"] = normalize_descriptor( + feed_in[0].descriptor + ) result["forecasts"]["feed_in"] = [ interval for interval in forecasts if is_feed_in(interval) ] diff --git a/homeassistant/components/amberelectric/manifest.json b/homeassistant/components/amberelectric/manifest.json index a4fd72f5bdb..7a13fbca9fe 100644 --- a/homeassistant/components/amberelectric/manifest.json +++ b/homeassistant/components/amberelectric/manifest.json @@ -7,7 +7,7 @@ "@madpilot" ], "requirements": [ - "amberelectric==1.0.3" + "amberelectric==1.0.4" ], "iot_class": "cloud_polling", "loggers": ["amberelectric"] diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 64ff09470e5..6d5fb105008 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, DOMAIN -from .coordinator import AmberUpdateCoordinator +from .coordinator import AmberUpdateCoordinator, normalize_descriptor ICONS = { "general": "mdi:transmission-tower", @@ -160,6 +160,7 @@ class AmberForecastSensor(AmberSensor): datum["end_time"] = interval.end_time.isoformat() datum["renewables"] = round(interval.renewables) datum["spike_status"] = interval.spike_status.value + datum["descriptor"] = normalize_descriptor(interval.descriptor) if interval.range is not None: datum["range_min"] = format_cents_to_dollars(interval.range.min) @@ -170,6 +171,15 @@ class AmberForecastSensor(AmberSensor): return data +class AmberPriceDescriptorSensor(AmberSensor): + """Amber Price Descriptor Sensor.""" + + @property + def native_value(self) -> str | None: + """Return the current price descriptor.""" + return self.coordinator.data[self.entity_description.key][self.channel_type] + + class AmberGridSensor(CoordinatorEntity, SensorEntity): """Sensor to show single grid specific values.""" @@ -214,6 +224,16 @@ async def async_setup_entry( ) entities.append(AmberPriceSensor(coordinator, description, channel_type)) + for channel_type in current: + description = SensorEntityDescription( + key="descriptors", + name=f"{entry.title} - {friendly_channel_type(channel_type)} Price Descriptor", + icon=ICONS[channel_type], + ) + entities.append( + AmberPriceDescriptorSensor(coordinator, description, channel_type) + ) + for channel_type in forecasts: description = SensorEntityDescription( key="forecasts", diff --git a/requirements_all.txt b/requirements_all.txt index 7d6ed83cc29..a67607efa49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -280,7 +280,7 @@ alpha_vantage==2.3.1 ambee==0.4.0 # homeassistant.components.amberelectric -amberelectric==1.0.3 +amberelectric==1.0.4 # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca844247dd1..cb4b9c26a6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -237,7 +237,7 @@ airtouch4pyapi==1.0.5 ambee==0.4.0 # homeassistant.components.amberelectric -amberelectric==1.0.3 +amberelectric==1.0.4 # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/tests/components/amberelectric/helpers.py b/tests/components/amberelectric/helpers.py index fbb1ebfd7ad..2bc65fdd558 100644 --- a/tests/components/amberelectric/helpers.py +++ b/tests/components/amberelectric/helpers.py @@ -6,7 +6,7 @@ from amberelectric.model.actual_interval import ActualInterval from amberelectric.model.channel import ChannelType from amberelectric.model.current_interval import CurrentInterval from amberelectric.model.forecast_interval import ForecastInterval -from amberelectric.model.interval import SpikeStatus +from amberelectric.model.interval import Descriptor, SpikeStatus from dateutil import parser @@ -26,6 +26,7 @@ def generate_actual_interval( renewables=50, channel_type=channel_type.value, spike_status=SpikeStatus.NO_SPIKE.value, + descriptor=Descriptor.LOW.value, ) @@ -45,6 +46,7 @@ def generate_current_interval( renewables=50.6, channel_type=channel_type.value, spike_status=SpikeStatus.NO_SPIKE.value, + descriptor=Descriptor.EXTREMELY_LOW.value, estimate=True, ) @@ -65,6 +67,7 @@ def generate_forecast_interval( renewables=50, channel_type=channel_type.value, spike_status=SpikeStatus.NO_SPIKE.value, + descriptor=Descriptor.VERY_LOW.value, estimate=True, ) diff --git a/tests/components/amberelectric/test_binary_sensor.py b/tests/components/amberelectric/test_binary_sensor.py index 856dcdc473e..b5d6504447e 100644 --- a/tests/components/amberelectric/test_binary_sensor.py +++ b/tests/components/amberelectric/test_binary_sensor.py @@ -112,7 +112,7 @@ async def setup_spike(hass) -> AsyncGenerator: def test_no_spike_sensor(hass: HomeAssistant, setup_no_spike) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") assert sensor assert sensor.state == "off" @@ -122,7 +122,7 @@ def test_no_spike_sensor(hass: HomeAssistant, setup_no_spike) -> None: def test_potential_spike_sensor(hass: HomeAssistant, setup_potential_spike) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") assert sensor assert sensor.state == "off" @@ -132,7 +132,7 @@ def test_potential_spike_sensor(hass: HomeAssistant, setup_potential_spike) -> N def test_spike_sensor(hass: HomeAssistant, setup_spike) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("binary_sensor.mock_title_price_spike") assert sensor assert sensor.state == "on" diff --git a/tests/components/amberelectric/test_coordinator.py b/tests/components/amberelectric/test_coordinator.py index bc80d3674d6..924cd5249c0 100644 --- a/tests/components/amberelectric/test_coordinator.py +++ b/tests/components/amberelectric/test_coordinator.py @@ -7,12 +7,15 @@ from unittest.mock import Mock, patch from amberelectric import ApiException from amberelectric.model.channel import Channel, ChannelType from amberelectric.model.current_interval import CurrentInterval -from amberelectric.model.interval import SpikeStatus +from amberelectric.model.interval import Descriptor, SpikeStatus from amberelectric.model.site import Site from dateutil import parser import pytest -from homeassistant.components.amberelectric.coordinator import AmberUpdateCoordinator +from homeassistant.components.amberelectric.coordinator import ( + AmberUpdateCoordinator, + normalize_descriptor, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed @@ -63,6 +66,18 @@ def mock_api_current_price() -> Generator: yield instance +def test_normalize_descriptor() -> None: + """Test normalizing descriptors works correctly.""" + assert normalize_descriptor(None) is None + assert normalize_descriptor(Descriptor.NEGATIVE) == "negative" + assert normalize_descriptor(Descriptor.EXTREMELY_LOW) == "extremely_low" + assert normalize_descriptor(Descriptor.VERY_LOW) == "very_low" + assert normalize_descriptor(Descriptor.LOW) == "low" + assert normalize_descriptor(Descriptor.NEUTRAL) == "neutral" + assert normalize_descriptor(Descriptor.HIGH) == "high" + assert normalize_descriptor(Descriptor.SPIKE) == "spike" + + async def test_fetch_general_site(hass: HomeAssistant, current_price_api: Mock) -> None: """Test fetching a site with only a general channel.""" diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index fa8cffe2c73..08103576b49 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -101,7 +101,7 @@ async def setup_general_and_feed_in(hass) -> AsyncGenerator: async def test_general_price_sensor(hass: HomeAssistant, setup_general: Mock) -> None: """Test the General Price sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 price = hass.states.get("sensor.mock_title_general_price") assert price assert price.state == "0.08" @@ -140,7 +140,7 @@ async def test_general_and_controlled_load_price_sensor( hass: HomeAssistant, setup_general_and_controlled_load: Mock ) -> None: """Test the Controlled Price sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_controlled_load_price") assert price assert price.state == "0.08" @@ -163,7 +163,7 @@ async def test_general_and_feed_in_price_sensor( hass: HomeAssistant, setup_general_and_feed_in: Mock ) -> None: """Test the Feed In sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_price") assert price assert price.state == "-0.08" @@ -186,7 +186,7 @@ async def test_general_forecast_sensor( hass: HomeAssistant, setup_general: Mock ) -> None: """Test the General Forecast sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 price = hass.states.get("sensor.mock_title_general_forecast") assert price assert price.state == "0.09" @@ -204,6 +204,7 @@ async def test_general_forecast_sensor( assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 assert first_forecast["spike_status"] == "none" + assert first_forecast["descriptor"] == "very_low" assert first_forecast.get("range_min") is None assert first_forecast.get("range_max") is None @@ -228,7 +229,7 @@ async def test_controlled_load_forecast_sensor( hass: HomeAssistant, setup_general_and_controlled_load: Mock ) -> None: """Test the Controlled Load Forecast sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_controlled_load_forecast") assert price assert price.state == "0.09" @@ -246,13 +247,14 @@ async def test_controlled_load_forecast_sensor( assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 assert first_forecast["spike_status"] == "none" + assert first_forecast["descriptor"] == "very_low" async def test_feed_in_forecast_sensor( hass: HomeAssistant, setup_general_and_feed_in: Mock ) -> None: """Test the Feed In Forecast sensor.""" - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 8 price = hass.states.get("sensor.mock_title_feed_in_forecast") assert price assert price.state == "-0.09" @@ -270,11 +272,42 @@ async def test_feed_in_forecast_sensor( assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 assert first_forecast["spike_status"] == "none" + assert first_forecast["descriptor"] == "very_low" def test_renewable_sensor(hass: HomeAssistant, setup_general) -> None: """Testing the creation of the Amber renewables sensor.""" - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 sensor = hass.states.get("sensor.mock_title_renewables") assert sensor assert sensor.state == "51" + + +def test_general_price_descriptor_descriptor_sensor( + hass: HomeAssistant, setup_general: Mock +) -> None: + """Test the General Price Descriptor sensor.""" + assert len(hass.states.async_all()) == 5 + price = hass.states.get("sensor.mock_title_general_price_descriptor") + assert price + assert price.state == "extremely_low" + + +def test_general_and_controlled_load_price_descriptor_sensor( + hass: HomeAssistant, setup_general_and_controlled_load: Mock +) -> None: + """Test the Controlled Price Descriptor sensor.""" + assert len(hass.states.async_all()) == 8 + price = hass.states.get("sensor.mock_title_controlled_load_price_descriptor") + assert price + assert price.state == "extremely_low" + + +def test_general_and_feed_in_price_descriptor_sensor( + hass: HomeAssistant, setup_general_and_feed_in: Mock +) -> None: + """Test the Feed In Price Descriptor sensor.""" + assert len(hass.states.async_all()) == 8 + price = hass.states.get("sensor.mock_title_feed_in_price_descriptor") + assert price + assert price.state == "extremely_low" From 4e21757b75cafbfee32cf7bb42c7a663923dca9d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 17 Mar 2022 11:13:03 +0100 Subject: [PATCH 0494/1054] Rename root node to integration name (#68214) --- homeassistant/components/arcam_fmj/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 85457ebfcef..583fa5f66c4 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -230,7 +230,7 @@ class ArcamFmj(MediaPlayerEntity): ] root = BrowseMedia( - title="Root", + title="Arcam FMJ Receiver", media_class=MEDIA_CLASS_DIRECTORY, media_content_id="root", media_content_type="library", From 6069d26800fafc006e28fd7309b4831b0ba5ed13 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 17 Mar 2022 11:13:46 +0100 Subject: [PATCH 0495/1054] Rename root media folder to integration name (#68213) --- homeassistant/components/philips_js/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index be24be632fc..20fa2ced825 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -424,7 +424,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Return root media objects.""" return BrowseMedia( - title="Library", + title="Philips TV", media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type="", From 81d001bcdc10cfbeb7cdac977298973ca5adb7d7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Mar 2022 12:57:36 +0100 Subject: [PATCH 0496/1054] Remove duplicate (partial) pytest job (#68221) --- .github/workflows/ci.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b4dba1fca52..259277b1666 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -812,22 +812,6 @@ jobs: --durations-min=1 \ -p no:sugar \ tests/components/${{ matrix.group }} - - name: Run pytest (partially); no coverage - if: needs.changes.outputs.test_full_suite == 'false' - timeout-minutes: 10 - run: | - . venv/bin/activate - python --version - python3 -X dev -m pytest \ - -qq \ - --timeout=9 \ - --durations=10 \ - -n auto \ - -o console_output_style=count \ - --durations=0 \ - --durations-min=1 \ - -p no:sugar \ - tests/components/${{ matrix.group }} - name: Upload coverage artifact uses: actions/upload-artifact@v3.0.0 with: From ead5b3e2c066fad5a8609c4eddd6dd6f777b6edc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Mar 2022 13:14:38 +0100 Subject: [PATCH 0497/1054] Improve strings for helper flow scaffold (#68257) --- script/scaffold/generate.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 56b5252cfad..8bde9504f41 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -154,10 +154,19 @@ def _custom_tasks(template, info: Info) -> None: info.update_manifest(config_flow=True) info.update_strings( config={ + "step": { + "user": { + "description": "New NEW_NAME Sensor", + "data": {"entity": "Input sensor", "name": "Name"}, + }, + }, + }, + options={ "step": { "init": { - "description": "Select the sensor for the NEW_NAME.", - "data": {"entity_id": "Sensor entity"}, + "data": { + "entity": "[%key:component::NEW_DOMAIN::config::step::user::description%]" + }, }, }, }, From a9c383a1e5e0ea8ec9be5118c848113b40586296 Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Thu, 17 Mar 2022 18:50:18 +0100 Subject: [PATCH 0498/1054] Correct naming for electrical energy meter (#68290) --- homeassistant/components/dsmr/const.py | 2 +- homeassistant/components/dsmr/sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index f68931e6fa5..6d092c1a3ef 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -32,7 +32,7 @@ DEFAULT_TIME_BETWEEN_UPDATE = 30 DATA_TASK = "task" -DEVICE_NAME_ENERGY = "Energy Meter" +DEVICE_NAME_ELECTRICITY = "Electricity Meter" DEVICE_NAME_GAS = "Gas Meter" DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 94ae2864905..e55679f8755 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -37,7 +37,7 @@ from .const import ( DEFAULT_PRECISION, DEFAULT_RECONNECT_INTERVAL, DEFAULT_TIME_BETWEEN_UPDATE, - DEVICE_NAME_ENERGY, + DEVICE_NAME_ELECTRICITY, DEVICE_NAME_GAS, DOMAIN, LOGGER, @@ -186,7 +186,7 @@ class DSMREntity(SensorEntity): self.telegram: dict[str, DSMRObject] = {} device_serial = entry.data[CONF_SERIAL_ID] - device_name = DEVICE_NAME_ENERGY + device_name = DEVICE_NAME_ELECTRICITY if entity_description.is_gas: device_serial = entry.data[CONF_SERIAL_ID_GAS] device_name = DEVICE_NAME_GAS From be7ef6115cbc2e9223fda589c624edbad53a02c4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Mar 2022 18:52:38 +0100 Subject: [PATCH 0499/1054] Make TypeVars private (1) (#68205) --- homeassistant/components/diagnostics/util.py | 10 +++++----- homeassistant/core.py | 14 ++++++-------- homeassistant/loader.py | 6 ++---- homeassistant/scripts/benchmark/__init__.py | 4 ++-- homeassistant/util/__init__.py | 8 ++++---- homeassistant/util/percentage.py | 6 +++--- homeassistant/util/read_only_dict.py | 6 +++--- homeassistant/util/yaml/loader.py | 6 +++--- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/diagnostics/util.py b/homeassistant/components/diagnostics/util.py index ba4f3d20f9a..8db2399fd53 100644 --- a/homeassistant/components/diagnostics/util.py +++ b/homeassistant/components/diagnostics/util.py @@ -8,7 +8,7 @@ from homeassistant.core import callback from .const import REDACTED -T = TypeVar("T") +_T = TypeVar("_T") @overload @@ -17,18 +17,18 @@ def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict: # type: @overload -def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: +def async_redact_data(data: _T, to_redact: Iterable[Any]) -> _T: ... @callback -def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: +def async_redact_data(data: _T, to_redact: Iterable[Any]) -> _T: """Redact sensitive data in a dict.""" if not isinstance(data, (Mapping, list)): return data if isinstance(data, list): - return cast(T, [async_redact_data(val, to_redact) for val in data]) + return cast(_T, [async_redact_data(val, to_redact) for val in data]) redacted = {**data} @@ -40,4 +40,4 @@ def async_redact_data(data: T, to_redact: Iterable[Any]) -> T: elif isinstance(value, list): redacted[key] = [async_redact_data(item, to_redact) for item in value] - return cast(T, redacted) + return cast(_T, redacted) diff --git a/homeassistant/core.py b/homeassistant/core.py index 8c662afce48..11d90d6d05b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -99,15 +99,13 @@ STAGE_3_SHUTDOWN_TIMEOUT = 30 block_async_io.enable() -T = TypeVar("T") +_T = TypeVar("_T") _R = TypeVar("_R") _R_co = TypeVar("_R_co", covariant=True) # pylint: disable=invalid-name # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} -# pylint: disable=invalid-name -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable[..., Any]) -CALLBACK_TYPE = Callable[[], None] -# pylint: enable=invalid-name +_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) +CALLBACK_TYPE = Callable[[], None] # pylint: disable=invalid-name CORE_STORAGE_KEY = "core.config" CORE_STORAGE_VERSION = 1 @@ -165,7 +163,7 @@ def valid_state(state: str) -> bool: return len(state) <= MAX_LENGTH_STATE_STATE -def callback(func: CALLABLE_T) -> CALLABLE_T: +def callback(func: _CallableT) -> _CallableT: """Annotation to mark method as safe to call from within the event loop.""" setattr(func, "_hass_callback", True) return func @@ -480,8 +478,8 @@ class HomeAssistant: @callback def async_add_executor_job( - self, target: Callable[..., T], *args: Any - ) -> asyncio.Future[T]: + self, target: Callable[..., _T], *args: Any + ) -> asyncio.Future[_T]: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor(None, target, *args) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 8e4521eddba..be70dc50f0c 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -35,9 +35,7 @@ from .util.async_ import gather_with_concurrency if TYPE_CHECKING: from .core import HomeAssistant -CALLABLE_T = TypeVar( # pylint: disable=invalid-name - "CALLABLE_T", bound=Callable[..., Any] -) +_CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) _LOGGER = logging.getLogger(__name__) @@ -805,7 +803,7 @@ class Helpers: return wrapped -def bind_hass(func: CALLABLE_T) -> CALLABLE_T: +def bind_hass(func: _CallableT) -> _CallableT: """Decorate function to indicate that first argument is hass.""" setattr(func, "__bind_hass", True) return func diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index c57d082de61..5441d39c877 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -22,7 +22,7 @@ from homeassistant.util import dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_CallableT = TypeVar("_CallableT", bound=Callable) BENCHMARKS: dict[str, Callable] = {} @@ -54,7 +54,7 @@ async def run_benchmark(bench): await hass.async_stop() -def benchmark(func: CALLABLE_T) -> CALLABLE_T: +def benchmark(func: _CallableT) -> _CallableT: """Decorate to mark a benchmark.""" BENCHMARKS[func.__name__] = func return func diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 15e9254e9d8..6562ecedb4f 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -15,8 +15,8 @@ import slugify as unicode_slug from .dt import as_local, utcnow -T = TypeVar("T") -U = TypeVar("U") # pylint: disable=invalid-name +_T = TypeVar("_T") +_U = TypeVar("_U") RE_SANITIZE_FILENAME = re.compile(r"(~|\.\.|/|\\)") RE_SANITIZE_PATH = re.compile(r"(~|\.(\.)+)") @@ -63,8 +63,8 @@ def repr_helper(inp: Any) -> str: def convert( - value: T | None, to_type: Callable[[T], U], default: U | None = None -) -> U | None: + value: _T | None, to_type: Callable[[_T], _U], default: _U | None = None +) -> _U | None: """Convert value to to_type, returns default if fails.""" try: return default if value is None else to_type(value) diff --git a/homeassistant/util/percentage.py b/homeassistant/util/percentage.py index d5646b44c0f..63bf78ed1f0 100644 --- a/homeassistant/util/percentage.py +++ b/homeassistant/util/percentage.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import TypeVar -T = TypeVar("T") +_T = TypeVar("_T") -def ordered_list_item_to_percentage(ordered_list: list[T], item: T) -> int: +def ordered_list_item_to_percentage(ordered_list: list[_T], item: _T) -> int: """Determine the percentage of an item in an ordered list. When using this utility for fan speeds, do not include "off" @@ -29,7 +29,7 @@ def ordered_list_item_to_percentage(ordered_list: list[T], item: T) -> int: return (list_position * 100) // list_len -def percentage_to_ordered_list_item(ordered_list: list[T], percentage: int) -> T: +def percentage_to_ordered_list_item(ordered_list: list[_T], percentage: int) -> _T: """Find the item that most closely matches the percentage in an ordered list. When using this utility for fan speeds, do not include "off" diff --git a/homeassistant/util/read_only_dict.py b/homeassistant/util/read_only_dict.py index f9cc949afdc..bb93dd41298 100644 --- a/homeassistant/util/read_only_dict.py +++ b/homeassistant/util/read_only_dict.py @@ -7,11 +7,11 @@ def _readonly(*args: Any, **kwargs: Any) -> Any: raise RuntimeError("Cannot modify ReadOnlyDict") -Key = TypeVar("Key") -Value = TypeVar("Value") +_KT = TypeVar("_KT") +_VT = TypeVar("_VT") -class ReadOnlyDict(dict[Key, Value]): +class ReadOnlyDict(dict[_KT, _VT]): """Read only version of dict that is compatible with dict types.""" __setitem__ = _readonly diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 84349ef91a9..3507ab96286 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -19,7 +19,7 @@ from .objects import Input, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any JSON_TYPE = Union[list, dict, str] # pylint: disable=invalid-name -DICT_T = TypeVar("DICT_T", bound=dict) # pylint: disable=invalid-name +_DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) @@ -144,8 +144,8 @@ def _add_reference( @overload def _add_reference( - obj: DICT_T, loader: SafeLineLoader, node: yaml.nodes.Node -) -> DICT_T: + obj: _DictT, loader: SafeLineLoader, node: yaml.nodes.Node +) -> _DictT: ... From eae0c756203a97f3779bfb448c7b277c23481fd6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Mar 2022 19:09:55 +0100 Subject: [PATCH 0500/1054] Make TypeVars private (2) (#68206) --- homeassistant/backports/enum.py | 6 ++++-- homeassistant/helpers/config_validation.py | 16 ++++++++-------- homeassistant/helpers/frame.py | 6 +++--- homeassistant/helpers/singleton.py | 16 ++++++++-------- homeassistant/helpers/update_coordinator.py | 16 ++++++++-------- 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 21302fe9f7b..1b5a6507847 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -4,13 +4,15 @@ from __future__ import annotations from enum import Enum from typing import Any, TypeVar -T = TypeVar("T", bound="StrEnum") +_StrEnumT = TypeVar("_StrEnumT", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - def __new__(cls: type[T], value: str, *args: Any, **kwargs: Any) -> T: + def __new__( + cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any + ) -> _StrEnumT: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 78dae80ebfb..51cd38569fb 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -102,7 +102,7 @@ sun_event = vol.All(vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)) port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) # typing typevar -T = TypeVar("T") +_T = TypeVar("_T") def path(value: Any) -> str: @@ -253,20 +253,20 @@ def ensure_list(value: None) -> list[Any]: @overload -def ensure_list(value: list[T]) -> list[T]: +def ensure_list(value: list[_T]) -> list[_T]: ... @overload -def ensure_list(value: list[T] | T) -> list[T]: +def ensure_list(value: list[_T] | _T) -> list[_T]: ... -def ensure_list(value: T | None) -> list[T] | list[Any]: +def ensure_list(value: _T | None) -> list[_T] | list[Any]: """Wrap value in list if it is not one.""" if value is None: return [] - return cast("list[T]", value) if isinstance(value, list) else [value] + return cast("list[_T]", value) if isinstance(value, list) else [value] def entity_id(value: Any) -> str: @@ -467,7 +467,7 @@ def time_period_seconds(value: float | str) -> timedelta: time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) -def match_all(value: T) -> T: +def match_all(value: _T) -> _T: """Validate that matches all values.""" return value @@ -483,7 +483,7 @@ positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) positive_time_period = vol.All(time_period, positive_timedelta) -def remove_falsy(value: list[T]) -> list[T]: +def remove_falsy(value: list[_T]) -> list[_T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -510,7 +510,7 @@ def slug(value: Any) -> str: def schema_with_slug_keys( - value_schema: T | Callable, *, slug_validator: Callable[[Any], str] = slug + value_schema: _T | Callable, *, slug_validator: Callable[[Any], str] = slug ) -> Callable: """Ensure dicts have slugs as keys. diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 13ffea48f81..2baf7cdd713 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) # Keep track of integrations already reported to prevent flooding _REPORTED_INTEGRATIONS: set[str] = set() -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_CallableT = TypeVar("_CallableT", bound=Callable) def get_integration_frame( @@ -113,7 +113,7 @@ def report_integration( ) -def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T: +def warn_use(func: _CallableT, what: str) -> _CallableT: """Mock a function to warn when it was about to be used.""" if asyncio.iscoroutinefunction(func): @@ -127,4 +127,4 @@ def warn_use(func: CALLABLE_T, what: str) -> CALLABLE_T: def report_use(*args: Any, **kwargs: Any) -> None: report(what) - return cast(CALLABLE_T, report_use) + return cast(_CallableT, report_use) diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index 7012241fe4e..a4f6d32c303 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -9,9 +9,9 @@ from typing import TypeVar, cast from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass -T = TypeVar("T") +_T = TypeVar("_T") -FUNC = Callable[[HomeAssistant], T] +FUNC = Callable[[HomeAssistant], _T] def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @@ -26,30 +26,30 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @bind_hass @functools.wraps(func) - def wrapped(hass: HomeAssistant) -> T: + def wrapped(hass: HomeAssistant) -> _T: if data_key not in hass.data: hass.data[data_key] = func(hass) - return cast(T, hass.data[data_key]) + return cast(_T, hass.data[data_key]) return wrapped @bind_hass @functools.wraps(func) - async def async_wrapped(hass: HomeAssistant) -> T: + async def async_wrapped(hass: HomeAssistant) -> _T: if data_key not in hass.data: evt = hass.data[data_key] = asyncio.Event() result = await func(hass) hass.data[data_key] = result evt.set() - return cast(T, result) + return cast(_T, result) obj_or_evt = hass.data[data_key] if isinstance(obj_or_evt, asyncio.Event): await obj_or_evt.wait() - return cast(T, hass.data[data_key]) + return cast(_T, hass.data[data_key]) - return cast(T, obj_or_evt) + return cast(_T, obj_or_evt) return async_wrapped diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index d9ab337e84e..7453a845e9d 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -23,14 +23,14 @@ from .debounce import Debouncer REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 REQUEST_REFRESH_DEFAULT_IMMEDIATE = True -T = TypeVar("T") +_T = TypeVar("_T") class UpdateFailed(Exception): """Raised when an update has failed.""" -class DataUpdateCoordinator(Generic[T]): +class DataUpdateCoordinator(Generic[_T]): """Class to manage fetching data from single endpoint.""" def __init__( @@ -40,7 +40,7 @@ class DataUpdateCoordinator(Generic[T]): *, name: str, update_interval: timedelta | None = None, - update_method: Callable[[], Awaitable[T]] | None = None, + update_method: Callable[[], Awaitable[_T]] | None = None, request_refresh_debouncer: Debouncer | None = None, ) -> None: """Initialize global data updater.""" @@ -56,7 +56,7 @@ class DataUpdateCoordinator(Generic[T]): # to make sure the first update was successful. # Set type to just T to remove annoying checks that data is not None # when it was already checked during setup. - self.data: T = None # type: ignore[assignment] + self.data: _T = None # type: ignore[assignment] self._listeners: list[CALLBACK_TYPE] = [] self._job = HassJob(self._handle_refresh_interval) @@ -140,7 +140,7 @@ class DataUpdateCoordinator(Generic[T]): """ await self._debounced_refresh.async_call() - async def _async_update_data(self) -> T: + async def _async_update_data(self) -> _T: """Fetch the latest data from the source.""" if self.update_method is None: raise NotImplementedError("Update method not implemented") @@ -265,7 +265,7 @@ class DataUpdateCoordinator(Generic[T]): update_callback() @callback - def async_set_updated_data(self, data: T) -> None: + def async_set_updated_data(self, data: _T) -> None: """Manually update data, notify listeners and reset refresh interval.""" if self._unsub_refresh: self._unsub_refresh() @@ -295,10 +295,10 @@ class DataUpdateCoordinator(Generic[T]): self._unsub_refresh = None -class CoordinatorEntity(Generic[T], entity.Entity): +class CoordinatorEntity(Generic[_T], entity.Entity): """A class for entities using DataUpdateCoordinator.""" - def __init__(self, coordinator: DataUpdateCoordinator[T]) -> None: + def __init__(self, coordinator: DataUpdateCoordinator[_T]) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator From 8f69d313222c2709bc27f8ac3526218ba02e28fb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 17 Mar 2022 19:11:14 +0100 Subject: [PATCH 0501/1054] Make TypeVars private (3) (#68207) --- homeassistant/components/august/sensor.py | 14 +++++++------- homeassistant/components/dlna_dms/dms.py | 8 ++++---- homeassistant/components/fronius/__init__.py | 6 +++--- homeassistant/components/fronius/coordinator.py | 4 ++-- homeassistant/components/vera/__init__.py | 8 ++++---- tests/components/google/conftest.py | 4 ++-- tests/components/nest/common.py | 4 ++-- tests/components/rtsp_to_webrtc/conftest.py | 4 ++-- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 6116e7d7601..29f17c69661 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -52,19 +52,19 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None: return detail.battery_percentage -T = TypeVar("T", LockDetail, KeypadDetail) +_T = TypeVar("_T", LockDetail, KeypadDetail) @dataclass -class AugustRequiredKeysMixin(Generic[T]): +class AugustRequiredKeysMixin(Generic[_T]): """Mixin for required keys.""" - value_fn: Callable[[T], int | None] + value_fn: Callable[[_T], int | None] @dataclass class AugustSensorEntityDescription( - SensorEntityDescription, AugustRequiredKeysMixin[T] + SensorEntityDescription, AugustRequiredKeysMixin[_T] ): """Describes August sensor entity.""" @@ -255,10 +255,10 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): return f"{self._device_id}_lock_operator" -class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]): +class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[_T]): """Representation of an August sensor.""" - entity_description: AugustSensorEntityDescription[T] + entity_description: AugustSensorEntityDescription[_T] _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @@ -267,7 +267,7 @@ class AugustBatterySensor(AugustEntityMixin, SensorEntity, Generic[T]): data: AugustData, device, old_device, - description: AugustSensorEntityDescription[T], + description: AugustSensorEntityDescription[_T], ): """Initialize the sensor.""" super().__init__(data, device) diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index 4cb1477778a..b43051ea4b4 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -47,7 +47,7 @@ from .const import ( ) _DlnaDmsDeviceMethod = TypeVar("_DlnaDmsDeviceMethod", bound="DmsDeviceSource") -_RetType = TypeVar("_RetType") +_R = TypeVar("_R") class DlnaDmsData: @@ -167,12 +167,12 @@ class ActionError(DlnaDmsDeviceError): def catch_request_errors( - func: Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _RetType]] -) -> Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _RetType]]: + func: Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _R]] +) -> Callable[[_DlnaDmsDeviceMethod, str], Coroutine[Any, Any, _R]]: """Catch UpnpError errors.""" @functools.wraps(func) - async def wrapper(self: _DlnaDmsDeviceMethod, req_param: str) -> _RetType: + async def wrapper(self: _DlnaDmsDeviceMethod, req_param: str) -> _R: """Catch UpnpError errors and check availability before and after request.""" if not self.available: LOGGER.warning("Device disappeared when trying to call %s", func.__name__) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 12811e84079..03340f19081 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -30,7 +30,7 @@ from .coordinator import ( _LOGGER: Final = logging.getLogger(__name__) PLATFORMS: Final = [Platform.SENSOR] -FroniusCoordinatorType = TypeVar("FroniusCoordinatorType", bound=FroniusCoordinatorBase) +_FroniusCoordinatorT = TypeVar("_FroniusCoordinatorT", bound=FroniusCoordinatorBase) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -199,8 +199,8 @@ class FroniusSolarNet: @staticmethod async def _init_optional_coordinator( - coordinator: FroniusCoordinatorType, - ) -> FroniusCoordinatorType | None: + coordinator: _FroniusCoordinatorT, + ) -> _FroniusCoordinatorT | None: """Initialize an update coordinator and return it if devices are found.""" try: await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/fronius/coordinator.py b/homeassistant/components/fronius/coordinator.py index e9d47296864..c1090dab1b1 100644 --- a/homeassistant/components/fronius/coordinator.py +++ b/homeassistant/components/fronius/coordinator.py @@ -31,7 +31,7 @@ if TYPE_CHECKING: from . import FroniusSolarNet from .sensor import _FroniusSensorEntity - FroniusEntityType = TypeVar("FroniusEntityType", bound=_FroniusSensorEntity) + _FroniusEntityT = TypeVar("_FroniusEntityT", bound=_FroniusSensorEntity) class FroniusCoordinatorBase( @@ -84,7 +84,7 @@ class FroniusCoordinatorBase( def add_entities_for_seen_keys( self, async_add_entities: AddEntitiesCallback, - entity_constructor: type[FroniusEntityType], + entity_constructor: type[_FroniusEntityT], ) -> None: """ Add entities for received keys and registers listener for future seen keys. diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index ef293c279be..d7cd8db051b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -214,14 +214,14 @@ def map_vera_device( ) -DeviceType = TypeVar("DeviceType", bound=veraApi.VeraDevice) +_DeviceTypeT = TypeVar("_DeviceTypeT", bound=veraApi.VeraDevice) -class VeraDevice(Generic[DeviceType], Entity): +class VeraDevice(Generic[_DeviceTypeT], Entity): """Representation of a Vera device entity.""" def __init__( - self, vera_device: DeviceType, controller_data: ControllerData + self, vera_device: _DeviceTypeT, controller_data: ControllerData ) -> None: """Initialize the device.""" self.vera_device = vera_device @@ -242,7 +242,7 @@ class VeraDevice(Generic[DeviceType], Entity): """Subscribe to updates.""" self.controller.register(self.vera_device, self._update_callback) - def _update_callback(self, _device: DeviceType) -> None: + def _update_callback(self, _device: _DeviceTypeT) -> None: """Update the state.""" self.schedule_update_ha_state(True) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 8efeac9983d..c3b8ae4e172 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -24,8 +24,8 @@ ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE ApiResult = Callable[[dict[str, Any]], None] ComponentSetup = Callable[[], Awaitable[bool]] -T = TypeVar("T") -YieldFixture = Generator[T, None, None] +_T = TypeVar("_T") +YieldFixture = Generator[_T, None, None] CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index e80ca84d58f..c2a9c6db157 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -22,8 +22,8 @@ from tests.common import MockConfigEntry # Typing helpers PlatformSetup = Callable[[], Awaitable[None]] -T = TypeVar("T") -YieldFixture = Generator[T, None, None] +_T = TypeVar("_T") +YieldFixture = Generator[_T, None, None] PROJECT_ID = "some-project-id" CLIENT_ID = "some-client-id" diff --git a/tests/components/rtsp_to_webrtc/conftest.py b/tests/components/rtsp_to_webrtc/conftest.py index 7148896e454..5e737efc397 100644 --- a/tests/components/rtsp_to_webrtc/conftest.py +++ b/tests/components/rtsp_to_webrtc/conftest.py @@ -24,8 +24,8 @@ CONFIG_ENTRY_DATA = {"server_url": SERVER_URL} # Typing helpers ComponentSetup = Callable[[], Awaitable[None]] -T = TypeVar("T") -YieldFixture = Generator[T, None, None] +_T = TypeVar("_T") +YieldFixture = Generator[_T, None, None] @pytest.fixture(autouse=True) From 9ea73e0d9009c3dbcbefd3361ef23839b4c0595a Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 18 Mar 2022 05:13:08 +1100 Subject: [PATCH 0502/1054] Skip unsupported LIFX Switches during discovery (#67726) Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/light.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index a7c52424d97..a6dd643655f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -74,6 +74,7 @@ MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 FIX_MAC_FW = AwesomeVersion("3.70") +SWITCH_PRODUCT_IDS = [70, 71, 89] SERVICE_LIFX_SET_STATE = "set_state" @@ -396,14 +397,18 @@ class LIFXManager: # Read initial state ack = AwaitAioLIFX().wait - # Used to populate sw_version - # no need to wait as we do not - # need it until later - bulb.get_hostfirmware() + # Get the product info first so that LIFX Switches + # can be ignored. + version_resp = await ack(bulb.get_version) + if version_resp and bulb.product in SWITCH_PRODUCT_IDS: + _LOGGER.warning( + "(Switch) action=skip_discovery, reason=unsupported, serial=%s, ip_addr=%s, type='LIFX Switch'", + str(bulb.mac_addr).replace(":", ""), + bulb.ip_addr, + ) + return False color_resp = await ack(bulb.get_color) - if color_resp: - version_resp = await ack(bulb.get_version) if color_resp is None or version_resp is None: _LOGGER.error("Failed to initialize %s", bulb.ip_addr) From 0adc7042dc3b3496419d3af789566496facbfa62 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 17 Mar 2022 20:24:00 +0100 Subject: [PATCH 0503/1054] Make new_token_callback more generic in SamsungTV (#67990) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 14 ++++----- homeassistant/components/samsungtv/bridge.py | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index c619dd4be09..4ce2b2350d4 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,6 +1,7 @@ """The Samsung TV integration.""" from __future__ import annotations +from collections.abc import Mapping from functools import partial import socket from typing import Any @@ -16,7 +17,6 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_PORT, - CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, Platform, ) @@ -102,7 +102,7 @@ def _async_get_device_bridge( data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], - data.get(CONF_TOKEN), + data, ) @@ -112,15 +112,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Initialize bridge bridge = await _async_create_bridge_with_updated_data(hass, entry) - # Ensure new token gets saved against the config_entry + # Ensure updates get saved against the config_entry @callback - def _update_token() -> None: + def _update_config_entry(updates: Mapping[str, Any]) -> None: """Update config entry with the new token.""" - hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_TOKEN: bridge.token} - ) + hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) - bridge.register_new_token_callback(_update_token) + bridge.register_update_config_entry_callback(_update_config_entry) async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 398b14a20b1..e9036d1aa59 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio from asyncio.exceptions import TimeoutError as AsyncioTimeoutError +from collections.abc import Callable, Mapping import contextlib from typing import Any, cast @@ -24,6 +25,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_TIMEOUT, + CONF_TOKEN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -87,12 +89,12 @@ class SamsungTVBridge(ABC): method: str, host: str, port: int | None = None, - token: str | None = None, + entry_data: Mapping[str, Any] | None = None, ) -> SamsungTVBridge: """Get Bridge instance.""" if method == METHOD_LEGACY or port == LEGACY_PORT: return SamsungTVLegacyBridge(hass, method, host, port) - return SamsungTVWSBridge(hass, method, host, port, token) + return SamsungTVWSBridge(hass, method, host, port, entry_data) def __init__( self, hass: HomeAssistant, method: str, host: str, port: int | None = None @@ -104,15 +106,17 @@ class SamsungTVBridge(ABC): self.host = host self.token: str | None = None self._reauth_callback: CALLBACK_TYPE | None = None - self._new_token_callback: CALLBACK_TYPE | None = None + self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None def register_reauth_callback(self, func: CALLBACK_TYPE) -> None: """Register a callback function.""" self._reauth_callback = func - def register_new_token_callback(self, func: CALLBACK_TYPE) -> None: + def register_update_config_entry_callback( + self, func: Callable[[Mapping[str, Any]], None] + ) -> None: """Register a callback function.""" - self._new_token_callback = func + self._update_config_entry = func @abstractmethod async def async_try_connect(self) -> str: @@ -147,10 +151,10 @@ class SamsungTVBridge(ABC): if self._reauth_callback is not None: self._reauth_callback() - def _notify_new_token_callback(self) -> None: - """Notify new token callback.""" - if self._new_token_callback is not None: - self._new_token_callback() + def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None: + """Notify update config callback.""" + if self._update_config_entry is not None: + self._update_config_entry(updates) class SamsungTVLegacyBridge(SamsungTVBridge): @@ -304,11 +308,12 @@ class SamsungTVWSBridge(SamsungTVBridge): method: str, host: str, port: int | None = None, - token: str | None = None, + entry_data: Mapping[str, Any] | None = None, ) -> None: """Initialize Bridge.""" super().__init__(hass, method, host, port) - self.token = token + if entry_data: + self.token = entry_data.get(CONF_TOKEN) self._rest_api: SamsungTVAsyncRest | None = None self._app_list: dict[str, str] | None = None self._device_info: dict[str, Any] | None = None @@ -505,7 +510,7 @@ class SamsungTVWSBridge(SamsungTVBridge): self._remote.token, ) self.token = self._remote.token - self._notify_new_token_callback() + self._notify_update_config_entry({CONF_TOKEN: self.token}) return self._remote @staticmethod From a8dae9791768a4da56eec5d2f459f618d18748fb Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 18 Mar 2022 06:40:43 +1100 Subject: [PATCH 0504/1054] Migrate geo_json_events to async library (#68236) * use async integration library * migrating to async library * migrating tests to async library * renamed method and fixed comment * removed callback annotation * use async dispatcher --- .../geo_json_events/geo_location.py | 66 +++++++++++-------- .../components/geo_json_events/manifest.json | 4 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- .../geo_json_events/test_geo_location.py | 43 +++++++----- 5 files changed, 73 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index f8ed84e532b..896fb7d36da 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from geojson_client.generic_feed import GenericFeedManager +from aio_geojson_generic_client import GenericFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -18,10 +18,14 @@ from homeassistant.const import ( LENGTH_KILOMETERS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -44,10 +48,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GeoJSON Events platform.""" @@ -59,27 +63,30 @@ def setup_platform( ) radius_in_km = config[CONF_RADIUS] # Initialize the entity manager. - feed = GeoJsonFeedEntityManager( - hass, add_entities, scan_interval, coordinates, url, radius_in_km + manager = GeoJsonFeedEntityManager( + hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ) + await manager.async_init() - def start_feed_manager(event): + async def start_feed_manager(event=None): """Start feed manager.""" - feed.startup() + await manager.async_update() - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) class GeoJsonFeedEntityManager: """Feed Entity Manager for GeoJSON feeds.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, url, radius_in_km + self, hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ): """Initialize the GeoJSON Feed Manager.""" self._hass = hass + websession = aiohttp_client.async_get_clientsession(hass) self._feed_manager = GenericFeedManager( + websession, self._generate_entity, self._update_entity, self._remove_entity, @@ -87,37 +94,42 @@ class GeoJsonFeedEntityManager: url, filter_radius=radius_in_km, ) - self._add_entities = add_entities + self._async_add_entities = async_add_entities self._scan_interval = scan_interval - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval - ) + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + async_track_time_interval(self._hass, update, self._scan_interval) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") def get_entry(self, external_id): """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + async def _generate_entity(self, external_id): """Generate new entity.""" new_entity = GeoJsonLocationEvent(self, external_id) # Add new entities to HA. - self._add_entities([new_entity], True) + self._async_add_entities([new_entity], True) - def _update_entity(self, external_id): + async def _update_entity(self, external_id): """Update entity.""" - dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") + async_dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") - def _remove_entity(self, external_id): + async def _remove_entity(self, external_id): """Remove entity.""" - dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") + async_dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") class GeoJsonLocationEvent(GeolocationEvent): diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 8f54c816649..ea5b56bee1e 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -2,8 +2,8 @@ "domain": "geo_json_events", "name": "GeoJSON", "documentation": "https://www.home-assistant.io/integrations/geo_json_events", - "requirements": ["geojson_client==0.6"], + "requirements": ["aio_geojson_generic_client==0.1"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["geojson_client"] + "loggers": ["aio_geojson_generic_client"] } diff --git a/requirements_all.txt b/requirements_all.txt index a67607efa49..fd2b84c5a15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -94,6 +94,9 @@ afsapi==0.0.4 # homeassistant.components.agent_dvr agent-py==0.0.23 +# homeassistant.components.geo_json_events +aio_geojson_generic_client==0.1 + # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.13 @@ -678,7 +681,6 @@ garages-amsterdam==3.0.0 # homeassistant.components.geniushub geniushub-client==0.6.30 -# homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb4b9c26a6b..1ce641901f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,6 +75,9 @@ advantage_air==0.3.1 # homeassistant.components.agent_dvr agent-py==0.0.23 +# homeassistant.components.geo_json_events +aio_geojson_generic_client==0.1 + # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.13 @@ -461,7 +464,6 @@ gTTS==2.2.4 # homeassistant.components.garages_amsterdam garages-amsterdam==3.0.0 -# homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 9d68e3d4973..a445673469b 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,5 +1,7 @@ """The tests for the geojson platform.""" -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, MagicMock, call, patch + +from aio_geojson_generic_client import GenericFeed from homeassistant.components import geo_location from homeassistant.components.geo_json_events.geo_location import ( @@ -66,9 +68,9 @@ async def test_setup(hass, legacy_patchable_time): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.generic_feed.GenericFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = ( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], ) @@ -124,7 +126,7 @@ async def test_setup(hass, legacy_patchable_time): # Simulate an update - one existing, one new entry, # one outdated entry - mock_feed.return_value.update.return_value = ( + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_4, mock_entry_3], ) @@ -136,7 +138,7 @@ async def test_setup(hass, legacy_patchable_time): # Simulate an update - empty data, but successful update, # so no changes to entities. - mock_feed.return_value.update.return_value = "OK_NO_DATA", None + mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -144,7 +146,7 @@ async def test_setup(hass, legacy_patchable_time): assert len(all_states) == 3 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -157,8 +159,13 @@ async def test_setup_with_custom_location(hass): # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 2000.5, (-31.1, 150.1)) - with patch("geojson_client.generic_feed.GenericFeed") as mock_feed: - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + with patch( + "aio_geojson_generic_client.feed_manager.GenericFeed", + wraps=GenericFeed, + ) as mock_feed, patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component( @@ -174,7 +181,9 @@ async def test_setup_with_custom_location(hass): all_states = hass.states.async_all() assert len(all_states) == 1 - assert mock_feed.call_args == call((15.1, 25.2), URL, filter_radius=200.0) + assert mock_feed.call_args == call( + ANY, (15.1, 25.2), URL, filter_radius=200.0 + ) async def test_setup_race_condition(hass, legacy_patchable_time): @@ -197,12 +206,12 @@ async def test_setup_race_condition(hass, legacy_patchable_time): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.generic_feed.GenericFeed" - ) as mock_feed, assert_setup_component(1, geo_location.DOMAIN): + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update, assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) await hass.async_block_till_done() - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + mock_feed_update.return_value = "OK", [mock_entry_1] # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -215,7 +224,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) await hass.async_block_till_done() @@ -225,7 +234,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 0 # Simulate an update - 1 entry - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + mock_feed_update.return_value = "OK", [mock_entry_1] async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -235,7 +244,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1 # Simulate an update - 1 entry - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + mock_feed_update.return_value = "OK", [mock_entry_1] async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -245,7 +254,7 @@ async def test_setup_race_condition(hass, legacy_patchable_time): assert len(hass.data[DATA_DISPATCHER][update_signal]) == 1 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 4 * SCAN_INTERVAL) await hass.async_block_till_done() From f75d62188899d4594b704ecf9a6070613e07aca7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Mar 2022 15:52:59 -0500 Subject: [PATCH 0505/1054] Normalize enqueuing Plex media on Sonos (#68132) --- homeassistant/components/sonos/media_player.py | 9 ++++++--- tests/components/sonos/test_plex_playback.py | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 0968f5d024c..4b1c9b82448 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -574,11 +574,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): else: shuffle = False media = lookup_plex_media(self.hass, media_type, json.dumps(payload)) - if not kwargs.get(ATTR_MEDIA_ENQUEUE): - soco.clear_queue() if shuffle: self.set_shuffle(True) - plex_plugin.play_now(media) + if kwargs.get(ATTR_MEDIA_ENQUEUE): + plex_plugin.add_to_queue(media) + else: + soco.clear_queue() + plex_plugin.add_to_queue(media) + soco.play_from_queue(0) return share_link = self.coordinator.share_link diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py index eeaa5544222..4c7c6f4f94a 100644 --- a/tests/components/sonos/test_plex_playback.py +++ b/tests/components/sonos/test_plex_playback.py @@ -25,8 +25,8 @@ async def test_plex_play_media(hass, async_autosetup_sonos): with patch( "homeassistant.components.sonos.media_player.lookup_plex_media" ) as mock_lookup, patch( - "soco.plugins.plex.PlexPlugin.play_now" - ) as mock_play_now, patch( + "soco.plugins.plex.PlexPlugin.add_to_queue" + ) as mock_add_to_queue, patch( "homeassistant.components.sonos.media_player.SonosMediaPlayerEntity.set_shuffle" ) as mock_shuffle: # Test successful Plex service call @@ -42,14 +42,14 @@ async def test_plex_play_media(hass, async_autosetup_sonos): ) assert len(mock_lookup.mock_calls) == 1 - assert len(mock_play_now.mock_calls) == 1 + assert len(mock_add_to_queue.mock_calls) == 1 assert not mock_shuffle.called assert mock_lookup.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC assert mock_lookup.mock_calls[0][1][2] == media_content_id # Test handling shuffle in payload mock_lookup.reset_mock() - mock_play_now.reset_mock() + mock_add_to_queue.reset_mock() shuffle_media_content_id = '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "shuffle": 1}' assert await hass.services.async_call( @@ -65,14 +65,14 @@ async def test_plex_play_media(hass, async_autosetup_sonos): assert mock_shuffle.called assert len(mock_lookup.mock_calls) == 1 - assert len(mock_play_now.mock_calls) == 1 + assert len(mock_add_to_queue.mock_calls) == 1 assert mock_lookup.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC assert mock_lookup.mock_calls[0][1][2] == media_content_id # Test failed Plex service call mock_lookup.reset_mock() mock_lookup.side_effect = HomeAssistantError - mock_play_now.reset_mock() + mock_add_to_queue.reset_mock() with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -86,4 +86,4 @@ async def test_plex_play_media(hass, async_autosetup_sonos): blocking=True, ) assert mock_lookup.called - assert not mock_play_now.called + assert not mock_add_to_queue.called From b34da1294c952df8dd69c3f26c4bef6ff8c777ae Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Mar 2022 15:57:22 -0500 Subject: [PATCH 0506/1054] Improve Plex media search failure feedback (#67493) --- homeassistant/components/plex/errors.py | 4 ++ .../components/plex/media_browser.py | 7 +-- homeassistant/components/plex/media_player.py | 6 +-- homeassistant/components/plex/media_search.py | 31 ++++++++----- homeassistant/components/plex/server.py | 44 ++++++++++--------- homeassistant/components/plex/services.py | 5 +-- tests/components/plex/test_media_search.py | 44 +++++++++++-------- tests/components/plex/test_playback.py | 6 +-- tests/components/plex/test_services.py | 2 +- 9 files changed, 85 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index 534c553d45e..ddbc1a2ea40 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -16,3 +16,7 @@ class ServerNotSpecified(PlexException): class ShouldUpdateConfigEntry(PlexException): """Config entry data is out of date and should be updated.""" + + +class MediaNotFound(PlexException): + """Requested media was not found.""" diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 11599086179..c50c173ec8d 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.errors import BrowseError from .const import DOMAIN, PLEX_URI_SCHEME +from .errors import MediaNotFound from .helpers import pretty_title @@ -115,9 +116,9 @@ def browse_media( # noqa: C901 def build_item_response(payload): """Create response payload for the provided media query.""" - media = plex_server.lookup_media(**payload) - - if media is None: + try: + media = plex_server.lookup_media(**payload) + except MediaNotFound: return None try: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 9b8b0df14c7..d652ead7dae 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -50,6 +50,7 @@ from .const import ( SERVERS, TRANSIENT_DEVICE_MODELS, ) +from .errors import MediaNotFound from .media_browser import browse_media _LOGGER = logging.getLogger(__name__) @@ -510,7 +511,7 @@ class PlexMediaPlayer(MediaPlayerEntity): try: playqueue = self.plex_server.get_playqueue(playqueue_id) except plexapi.exceptions.NotFound as err: - raise HomeAssistantError( + raise MediaNotFound( f"PlayQueue '{playqueue_id}' could not be found" ) from err else: @@ -519,9 +520,6 @@ class PlexMediaPlayer(MediaPlayerEntity): resume = src.pop("resume", False) media = self.plex_server.lookup_media(media_type, **src) - if media is None: - raise HomeAssistantError(f"Media could not be found: {media_id}") - if resume and not offset: offset = media.viewOffset diff --git a/homeassistant/components/plex/media_search.py b/homeassistant/components/plex/media_search.py index abe32f7cf4c..351ee1444c4 100644 --- a/homeassistant/components/plex/media_search.py +++ b/homeassistant/components/plex/media_search.py @@ -1,7 +1,13 @@ """Helper methods to search for Plex media.""" +from __future__ import annotations + import logging +from plexapi.base import PlexObject from plexapi.exceptions import BadRequest, NotFound +from plexapi.library import LibrarySection + +from .errors import MediaNotFound LEGACY_PARAM_MAPPING = { "show_name": "show.title", @@ -28,13 +34,19 @@ PREFERRED_LIBTYPE_ORDER = ( _LOGGER = logging.getLogger(__name__) -def search_media(media_type, library_section, allow_multiple=False, **kwargs): +def search_media( + media_type: str, + library_section: LibrarySection, + allow_multiple: bool = False, + **kwargs, +) -> PlexObject | list[PlexObject]: """Search for specified Plex media in the provided library section. - Returns a single media item or None. + Returns a media item or a list of items if `allow_multiple` is set. - If `allow_multiple` is `True`, return a list of matching items. + Raises MediaNotFound if the search was unsuccessful. """ + original_query = kwargs.copy() search_query = {} libtype = kwargs.pop("libtype", None) @@ -61,11 +73,12 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs): try: results = library_section.search(**search_query) except (BadRequest, NotFound) as exc: - _LOGGER.error("Problem in query %s: %s", search_query, exc) - return None + raise MediaNotFound(f"Problem in query {original_query}: {exc}") from exc if not results: - return None + raise MediaNotFound( + f"No {media_type} results in '{library_section.title}' for {original_query}" + ) if len(results) > 1: if allow_multiple: @@ -75,10 +88,8 @@ def search_media(media_type, library_section, allow_multiple=False, **kwargs): exact_matches = [x for x in results if x.title.lower() == title.lower()] if len(exact_matches) == 1: return exact_matches[0] - _LOGGER.warning( - "Multiple matches, make content_id more specific or use `allow_multiple`: %s", - results, + raise MediaNotFound( + f"Multiple matches, make content_id more specific or use `allow_multiple`: {results}" ) - return None return results[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index b4dc5755a73..b136bec73e9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -42,7 +42,12 @@ from .const import ( X_PLEX_PRODUCT, X_PLEX_VERSION, ) -from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry +from .errors import ( + MediaNotFound, + NoServersFound, + ServerNotSpecified, + ShouldUpdateConfigEntry, +) from .media_search import search_media from .models import PlexSession @@ -619,37 +624,34 @@ class PlexServer: key = kwargs["plex_key"] try: return self.fetch_item(key) - except NotFound: - _LOGGER.error("Media for key %s not found", key) - return None + except NotFound as err: + raise MediaNotFound(f"Media for key {key} not found") from err if media_type == MEDIA_TYPE_PLAYLIST: try: playlist_name = kwargs["playlist_name"] return self.playlist(playlist_name) - except KeyError: - _LOGGER.error("Must specify 'playlist_name' for this search") - return None - except NotFound: - _LOGGER.error( - "Playlist '%s' not found", - playlist_name, - ) - return None + except KeyError as err: + raise MediaNotFound( + "Must specify 'playlist_name' for this search" + ) from err + except NotFound as err: + raise MediaNotFound(f"Playlist '{playlist_name}' not found") from err try: library_name = kwargs.pop("library_name") library_section = self.library.section(library_name) - except KeyError: - _LOGGER.error("Must specify 'library_name' for this search") - return None - except NotFound: + except KeyError as err: + raise MediaNotFound("Must specify 'library_name' for this search") from err + except NotFound as err: library_sections = [section.title for section in self.library.sections()] - _LOGGER.error( - "Library '%s' not found in %s", library_name, library_sections - ) - return None + raise MediaNotFound( + f"Library '{library_name}' not found in {library_sections}" + ) from err + _LOGGER.debug( + "Searching for %s in %s using: %s", media_type, library_section, kwargs + ) return search_media(media_type, library_section, **kwargs) @property diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 0433ba836cd..280152b972f 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -16,6 +16,7 @@ from .const import ( SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) +from .errors import MediaNotFound REFRESH_LIBRARY_SCHEMA = vol.Schema( {vol.Optional("server_name"): str, vol.Required("library_name"): str} @@ -115,15 +116,13 @@ def lookup_plex_media(hass, content_type, content_id): try: playqueue = plex_server.get_playqueue(playqueue_id) except NotFound as err: - raise HomeAssistantError( + raise MediaNotFound( f"PlayQueue '{playqueue_id}' could not be found" ) from err return playqueue shuffle = content.pop("shuffle", 0) media = plex_server.lookup_media(content_type, **content) - if media is None: - raise HomeAssistantError(f"Plex media not found using payload: '{content_id}'") if shuffle: return plex_server.create_playqueue(media, shuffle=shuffle) diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py index f73fdea2806..e5c19d31c4e 100644 --- a/tests/components/plex/test_media_search.py +++ b/tests/components/plex/test_media_search.py @@ -16,13 +16,11 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) from homeassistant.components.plex.const import DOMAIN +from homeassistant.components.plex.errors import MediaNotFound from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.exceptions import HomeAssistantError -async def test_media_lookups( - hass, mock_plex_server, requests_mock, playqueue_created, caplog -): +async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created): """Test media lookups to Plex server.""" # Plex Key searches media_player_id = hass.states.async_entity_ids("media_player")[0] @@ -39,7 +37,7 @@ async def test_media_lookups( }, True, ) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -51,10 +49,10 @@ async def test_media_lookups( }, True, ) - assert "Media could not be found: 123" in str(excinfo.value) + assert "Media for key 123 not found" in str(excinfo.value) # TV show searches - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: payload = '{"library_name": "Not a Library", "show_name": "TV Show"}' assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -66,7 +64,7 @@ async def test_media_lookups( }, True, ) - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Library 'Not a Library' not found in" in str(excinfo.value) with patch("plexapi.library.LibrarySection.search") as search: assert await hass.services.async_call( @@ -243,8 +241,21 @@ async def test_media_lookups( ) search.assert_called_with(**{"title": "Movie 1", "libtype": None}) - # TV show searches - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: + payload = '{"title": "Movie 1"}' + assert await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert "Must specify 'library_name' for this search" in str(excinfo.value) + + with pytest.raises(MediaNotFound) as excinfo: payload = '{"library_name": "Movies", "title": "Not a Movie"}' with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): assert await hass.services.async_call( @@ -257,8 +268,7 @@ async def test_media_lookups( }, True, ) - assert "Problem in query" in caplog.text - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Problem in query" in str(excinfo.value) # Playlist searches assert await hass.services.async_call( @@ -272,7 +282,7 @@ async def test_media_lookups( True, ) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: payload = '{"playlist_name": "Not a Playlist"}' assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -284,10 +294,9 @@ async def test_media_lookups( }, True, ) - assert "Playlist 'Not a Playlist' not found" in caplog.text - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Playlist 'Not a Playlist' not found" in str(excinfo.value) - with pytest.raises(HomeAssistantError) as excinfo: + with pytest.raises(MediaNotFound) as excinfo: payload = "{}" assert await hass.services.async_call( MEDIA_PLAYER_DOMAIN, @@ -299,5 +308,4 @@ async def test_media_lookups( }, True, ) - assert "Must specify 'playlist_name' for this search" in caplog.text - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Must specify 'playlist_name' for this search" in str(excinfo.value) diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 2db0323bdcd..e67249ea375 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -43,7 +43,6 @@ async def test_media_player_playback( requests_mock, playqueue_created, player_plexweb_resources, - caplog, ): """Test playing media on a Plex media_player.""" requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) @@ -68,7 +67,7 @@ async def test_media_player_playback( }, True, ) - assert f"Media could not be found: {payload}" in str(excinfo.value) + assert f"No {MEDIA_TYPE_MOVIE} results in 'Movies' for" in str(excinfo.value) movie1 = MockPlexMedia("Movie", "movie") movie2 = MockPlexMedia("Movie II", "movie") @@ -120,8 +119,7 @@ async def test_media_player_playback( }, True, ) - assert f"Media could not be found: {payload}" in str(excinfo.value) - assert "Multiple matches, make content_id more specific" in caplog.text + assert "Multiple matches, make content_id more specific" in str(excinfo.value) # Test multiple choices with allow_multiple movies = [movie1, movie2, movie3] diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 49fdd48f662..ddc4ed58ba8 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -168,7 +168,7 @@ async def test_lookup_media_for_other_integrations( with patch("plexapi.library.LibrarySection.search", return_value=None): with pytest.raises(HomeAssistantError) as excinfo: lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA) - assert "Plex media not found" in str(excinfo.value) + assert f"No {MEDIA_TYPE_MUSIC} results in 'Music' for" in str(excinfo.value) # Test with playqueue requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) From f006cffac6f588359980b10d6bc956b05013f947 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 17 Mar 2022 23:32:46 +0100 Subject: [PATCH 0507/1054] Bump async-upnp-client to 0.27.0 (#68310) --- homeassistant/components/dlna_dmr/data.py | 4 +++- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dmr/media_player.py | 2 +- homeassistant/components/dlna_dms/dms.py | 9 +++------ homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/device.py | 3 ++- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dlna_dmr/conftest.py | 3 ++- tests/components/dlna_dmr/test_config_flow.py | 3 ++- tests/components/dlna_dmr/test_data.py | 2 +- tests/components/dlna_dmr/test_media_player.py | 2 +- tests/components/dlna_dms/conftest.py | 2 +- tests/components/dlna_dms/test_config_flow.py | 2 +- 18 files changed, 25 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 7afe4304581..1a1a28d758c 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -5,8 +5,10 @@ import asyncio from collections import defaultdict from typing import NamedTuple, cast -from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.client import UpnpRequester +from async_upnp_client.client_factory import UpnpFactory +from async_upnp_client.event_handler import UpnpEventHandler from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 3560b8c61b8..ccd0ca6e922 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.26.0"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index abee0cf13df..d4d994a779b 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta import functools from typing import Any, TypeVar -from async_upnp_client import UpnpService, UpnpStateVariable +from async_upnp_client.client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, PlayMode, TransportState diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index b43051ea4b4..32125101974 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -7,14 +7,11 @@ from dataclasses import dataclass import functools from typing import Any, TypeVar, cast -from async_upnp_client import ( - UpnpEventHandler, - UpnpFactory, - UpnpNotifyServer, - UpnpRequester, -) from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.client import UpnpRequester +from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.const import NotificationSubType +from async_upnp_client.event_handler import UpnpEventHandler, UpnpNotifyServer from async_upnp_client.exceptions import UpnpActionError, UpnpConnectionError, UpnpError from async_upnp_client.profiles.dlna import ContentDirectoryErrorCode, DmsDevice from didl_lite import didl_lite diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index c8d30dc9c08..dffd0b9b654 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.26.0"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3ed1bb6e934..e55f16d9247 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.26.0"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 19e7987a138..a3b5e63bf41 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -6,8 +6,9 @@ from collections.abc import Mapping from typing import Any from urllib.parse import urlparse -from async_upnp_client import UpnpDevice, UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.client import UpnpDevice +from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import UpnpError from async_upnp_client.profiles.igd import IgdDevice, StatusInfo diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 4ebec2fae92..2018b95a021 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.26.0"], + "requirements": ["async-upnp-client==0.27.0"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 89f065d72e1..cd47b1ee049 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.9", "async-upnp-client==0.26.0"], + "requirements": ["yeelight==0.7.9", "async-upnp-client==0.27.0"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43c9af11f5c..22184f011fb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.8 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.26.0 +async-upnp-client==0.27.0 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fd2b84c5a15..7f7705805ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -329,7 +329,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.26.0 +async-upnp-client==0.27.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ce641901f3..1be16a8cf81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -262,7 +262,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.26.0 +async-upnp-client==0.27.0 # homeassistant.components.sleepiq asyncsleepiq==1.1.2 diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 3a9025a9a29..84aec044caf 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -5,7 +5,8 @@ from collections.abc import Iterable from socket import AddressFamily # pylint: disable=no-name-in-module from unittest.mock import Mock, create_autospec, patch, seal -from async_upnp_client import UpnpDevice, UpnpFactory, UpnpService +from async_upnp_client.client import UpnpDevice, UpnpService +from async_upnp_client.client_factory import UpnpFactory import pytest from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index fb0c416c086..44ab4ea313f 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -4,7 +4,8 @@ from __future__ import annotations import dataclasses from unittest.mock import Mock -from async_upnp_client import UpnpDevice, UpnpError +from async_upnp_client.client import UpnpDevice +from async_upnp_client.exceptions import UpnpError import pytest from homeassistant import config_entries, data_entry_flow diff --git a/tests/components/dlna_dmr/test_data.py b/tests/components/dlna_dmr/test_data.py index 2a86070ea72..5b2c0b1815c 100644 --- a/tests/components/dlna_dmr/test_data.py +++ b/tests/components/dlna_dmr/test_data.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import Iterable from unittest.mock import ANY, Mock, patch -from async_upnp_client import UpnpEventHandler from async_upnp_client.aiohttp import AiohttpNotifyServer +from async_upnp_client.event_handler import UpnpEventHandler import pytest from homeassistant.components.dlna_dmr.const import DOMAIN diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index a9ac5946f30..bed89f3db9d 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -9,7 +9,7 @@ from types import MappingProxyType from typing import Any from unittest.mock import ANY, DEFAULT, Mock, patch -from async_upnp_client import UpnpService, UpnpStateVariable +from async_upnp_client.client import UpnpService, UpnpStateVariable from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, diff --git a/tests/components/dlna_dms/conftest.py b/tests/components/dlna_dms/conftest.py index 6764001be31..8c1a6f3dc43 100644 --- a/tests/components/dlna_dms/conftest.py +++ b/tests/components/dlna_dms/conftest.py @@ -5,7 +5,7 @@ from collections.abc import AsyncGenerator, Iterable from typing import Final from unittest.mock import Mock, create_autospec, patch, seal -from async_upnp_client import UpnpDevice, UpnpService +from async_upnp_client.client import UpnpDevice, UpnpService from async_upnp_client.utils import absolute_url import pytest diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index df8d55dbc25..72e42e9102e 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -5,7 +5,7 @@ import dataclasses from typing import Final from unittest.mock import Mock -from async_upnp_client import UpnpError +from async_upnp_client.exceptions import UpnpError import pytest from homeassistant import config_entries, data_entry_flow From 712c9d4a4fa009ce534c8a3dcae96cbdc1ccc6ed Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 17 Mar 2022 18:12:31 -0500 Subject: [PATCH 0508/1054] Bump frontend to 20220317.0 (#68314) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 31c912db8a5..c6b1eebaae0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220316.0" + "home-assistant-frontend==20220317.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 22184f011fb..2a9af7ff1d5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220316.0 +home-assistant-frontend==20220317.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 7f7705805ab..ac3b727e8b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -808,7 +808,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220316.0 +home-assistant-frontend==20220317.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1be16a8cf81..b25e5594f99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220316.0 +home-assistant-frontend==20220317.0 # homeassistant.components.zwave # homeassistant-pyozw==0.1.10 From 490c921763214871bb95fa780806d4e42177e48f Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 18 Mar 2022 00:29:21 +0000 Subject: [PATCH 0509/1054] Don't access hass.data directly in generic camera test. (#68316) --- tests/components/generic/test_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 042cd2ee650..b08ed58841d 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -280,7 +280,7 @@ async def test_setup_alternative_options(hass, hass_ws_client): }, ) await hass.async_block_till_done() - assert hass.data["camera"].get_entity("camera.config_test") + assert hass.states.get("camera.config_test") async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): From 95f20500ca8a758c2828b553937fda064740f8a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Mar 2022 18:33:22 -1000 Subject: [PATCH 0510/1054] Commit any pending changes before running non-EventTasks in the recorder (#68287) --- homeassistant/components/recorder/__init__.py | 15 ++++++++++++++- tests/components/recorder/test_init.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 4f1401aaee7..18acfabffaa 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -331,6 +331,8 @@ def _async_register_services(hass, instance): class RecorderTask(abc.ABC): """ABC for recorder tasks.""" + commit_before = True + @abc.abstractmethod def run(self, instance: Recorder) -> None: """Handle the task.""" @@ -439,6 +441,8 @@ class ExternalStatisticsTask(RecorderTask): class WaitTask(RecorderTask): """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + commit_before = False + def run(self, instance: Recorder) -> None: """Handle the task.""" instance._queue_watch.set() # pylint: disable=[protected-access] @@ -461,6 +465,8 @@ class DatabaseLockTask(RecorderTask): class StopTask(RecorderTask): """An object to insert into the recorder queue to stop the event handler.""" + commit_before = False + def run(self, instance: Recorder) -> None: """Handle the task.""" instance.stop_requested = True @@ -471,6 +477,7 @@ class EventTask(RecorderTask): """An object to insert into the recorder queue to stop the event handler.""" event: bool + commit_before = False def run(self, instance: Recorder) -> None: """Handle the task.""" @@ -800,6 +807,10 @@ class Recorder(threading.Thread): def _process_one_task_or_recover(self, task: RecorderTask): """Process an event, reconnect, or recover a malformed database.""" try: + # If its not an event, commit everything + # that is pending before running the task + if task.commit_before: + self._commit_event_session_or_retry() return task.run(self) except exc.DatabaseError as err: if self._handle_database_error(err): @@ -955,7 +966,9 @@ class Recorder(threading.Thread): def _commit_event_session_or_retry(self): """Commit the event session if there is work to do.""" - if not self.event_session.new and not self.event_session.dirty: + if not self.event_session or ( + not self.event_session.new and not self.event_session.dirty + ): return tries = 1 while tries <= self.db_max_retries: diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index dc7881cfb42..6bdc8250afc 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -288,7 +288,7 @@ async def test_force_shutdown_with_queue_of_writes_that_generate_exceptions( await async_wait_recording_done(hass, instance) - with patch.object(instance, "db_retry_wait", 0.2), patch.object( + with patch.object(instance, "db_retry_wait", 0.05), patch.object( instance.event_session, "flush", side_effect=OperationalError( From 7b38f81040cb5c175be56eddee8eef514b87e04e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 18 Mar 2022 00:36:58 -0500 Subject: [PATCH 0511/1054] Bump plexapi to 4.10.1 (#68313) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 85a060ae7cd..084356abf7b 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.10.0", + "plexapi==4.10.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index ac3b727e8b6..b9193e46fd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1207,7 +1207,7 @@ pillow==9.0.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.10.0 +plexapi==4.10.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b25e5594f99..5adb6e80ff7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -792,7 +792,7 @@ pilight==0.1.1 pillow==9.0.1 # homeassistant.components.plex -plexapi==4.10.0 +plexapi==4.10.1 # homeassistant.components.plex plexauth==0.0.6 From ad84a02b8ecfe9fc577d3d725ce5aeec3fdcfa40 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 Mar 2022 09:23:52 +0100 Subject: [PATCH 0512/1054] Add zha typing [api] (1) (#68220) --- homeassistant/components/zha/api.py | 189 +++++++++++++++++++--------- 1 file changed, 127 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 86dad9d6bd0..feebff87c8b 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1,10 +1,11 @@ """Web socket API for Zigbee Home Automation devices.""" +from __future__ import annotations import asyncio import collections from collections.abc import Mapping import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol from zigpy.config.validators import cv_boolean @@ -14,9 +15,10 @@ import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api from homeassistant.const import ATTR_COMMAND, ATTR_NAME -from homeassistant.core import ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service import async_register_admin_service from .core.const import ( ATTR_ARGS, @@ -69,6 +71,9 @@ from .core.helpers import ( ) from .core.typing import ZhaDeviceType, ZhaGatewayType +if TYPE_CHECKING: + from homeassistant.components.websocket_api.connection import ActiveConnection + _LOGGER = logging.getLogger(__name__) TYPE = "type" @@ -194,11 +199,16 @@ ClusterBinding = collections.namedtuple("ClusterBinding", "id endpoint_id type n @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required("type"): "zha/devices/permit", **SERVICE_PERMIT_PARAMS} + { + vol.Required("type"): "zha/devices/permit", + **SERVICE_PERMIT_PARAMS, + } ) -async def websocket_permit_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_permit_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Permit ZHA zigbee devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] duration = msg.get(ATTR_DURATION) @@ -239,9 +249,11 @@ async def websocket_permit_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices"}) -async def websocket_get_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] @@ -251,9 +263,11 @@ async def websocket_get_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"}) -async def websocket_get_groupable_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_groupable_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA devices that can be grouped.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] @@ -289,9 +303,11 @@ async def websocket_get_groupable_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"}) -async def websocket_get_groups(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_groups( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA groups.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] groups = [group.group_info for group in zha_gateway.groups.values()] @@ -299,11 +315,16 @@ async def websocket_get_groups(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert} + { + vol.Required(TYPE): "zha/device", + vol.Required(ATTR_IEEE): EUI64.convert, + } ) -async def websocket_get_device(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_device( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] @@ -321,11 +342,16 @@ async def websocket_get_device(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/group", vol.Required(GROUP_ID): cv.positive_int} + { + vol.Required(TYPE): "zha/group", + vol.Required(GROUP_ID): cv.positive_int, + } ) -async def websocket_get_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_id = msg[GROUP_ID] @@ -358,7 +384,6 @@ def cv_group_member(value: Any) -> GroupMember: @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/add", @@ -367,7 +392,10 @@ def cv_group_member(value: Any) -> GroupMember: vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) -async def websocket_add_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_add_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Add a new ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_name = msg[GROUP_NAME] @@ -378,14 +406,16 @@ async def websocket_add_group(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/remove", vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]), } ) -async def websocket_remove_groups(hass, connection, msg): +@websocket_api.async_response +async def websocket_remove_groups( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Remove the specified ZHA groups.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_ids = msg[GROUP_IDS] @@ -402,7 +432,6 @@ async def websocket_remove_groups(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/members/add", @@ -410,7 +439,10 @@ async def websocket_remove_groups(hass, connection, msg): vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) -async def websocket_add_group_members(hass, connection, msg): +@websocket_api.async_response +async def websocket_add_group_members( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Add members to a ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_id = msg[GROUP_ID] @@ -432,7 +464,6 @@ async def websocket_add_group_members(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/members/remove", @@ -440,7 +471,10 @@ async def websocket_add_group_members(hass, connection, msg): vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) -async def websocket_remove_group_members(hass, connection, msg): +@websocket_api.async_response +async def websocket_remove_group_members( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Remove members from a ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_id = msg[GROUP_ID] @@ -462,14 +496,16 @@ async def websocket_remove_group_members(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", vol.Required(ATTR_IEEE): EUI64.convert, } ) -async def websocket_reconfigure_node(hass, connection, msg): +@websocket_api.async_response +async def websocket_reconfigure_node( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Reconfigure a ZHA nodes entities by its ieee address.""" zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] @@ -495,24 +531,28 @@ async def websocket_reconfigure_node(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/topology/update", } ) -async def websocket_update_topology(hass, connection, msg): +@websocket_api.async_response +async def websocket_update_topology( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Update the ZHA network topology.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] hass.async_create_task(zha_gateway.application_controller.topology.scan()) @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert} ) -async def websocket_device_clusters(hass, connection, msg): +@websocket_api.async_response +async def websocket_device_clusters( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Return a list of device clusters.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] @@ -544,7 +584,6 @@ async def websocket_device_clusters(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", @@ -554,7 +593,10 @@ async def websocket_device_clusters(hass, connection, msg): vol.Required(ATTR_CLUSTER_TYPE): str, } ) -async def websocket_device_cluster_attributes(hass, connection, msg): +@websocket_api.async_response +async def websocket_device_cluster_attributes( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Return a list of cluster attributes.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] @@ -589,7 +631,6 @@ async def websocket_device_cluster_attributes(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", @@ -599,7 +640,10 @@ async def websocket_device_cluster_attributes(hass, connection, msg): vol.Required(ATTR_CLUSTER_TYPE): str, } ) -async def websocket_device_cluster_commands(hass, connection, msg): +@websocket_api.async_response +async def websocket_device_cluster_commands( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Return a list of cluster commands.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] cluster_id = msg[ATTR_CLUSTER_ID] @@ -647,7 +691,6 @@ async def websocket_device_cluster_commands(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", @@ -659,7 +702,10 @@ async def websocket_device_cluster_commands(hass, connection, msg): vol.Optional(ATTR_MANUFACTURER): object, } ) -async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): +@websocket_api.async_response +async def websocket_read_zigbee_cluster_attributes( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Read zigbee attribute for cluster on zha entity.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee = msg[ATTR_IEEE] @@ -700,11 +746,13 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert} ) -async def websocket_get_bindable_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_bindable_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Directly bind devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_IEEE] @@ -728,7 +776,6 @@ async def websocket_get_bindable_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", @@ -736,7 +783,10 @@ async def websocket_get_bindable_devices(hass, connection, msg): vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) -async def websocket_bind_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_bind_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Directly bind devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] @@ -754,7 +804,6 @@ async def websocket_bind_devices(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", @@ -762,7 +811,10 @@ async def websocket_bind_devices(hass, connection, msg): vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) -async def websocket_unbind_devices(hass, connection, msg): +@websocket_api.async_response +async def websocket_unbind_devices( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Remove a direct binding between devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] @@ -797,7 +849,6 @@ def is_cluster_binding(value: Any) -> ClusterBinding: @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/bind", @@ -806,7 +857,10 @@ def is_cluster_binding(value: Any) -> ClusterBinding: vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), } ) -async def websocket_bind_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_bind_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Directly bind a device to a group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] @@ -818,7 +872,6 @@ async def websocket_bind_group(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/unbind", @@ -827,7 +880,10 @@ async def websocket_bind_group(hass, connection, msg): vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), } ) -async def websocket_unbind_group(hass, connection, msg): +@websocket_api.async_response +async def websocket_unbind_group( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Unbind a device from a group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] @@ -879,9 +935,11 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(TYPE): "zha/configuration"}) -async def websocket_get_configuration(hass, connection, msg): +@websocket_api.async_response +async def websocket_get_configuration( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Get ZHA configuration.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] import voluptuous_serialize # pylint: disable=import-outside-toplevel @@ -913,14 +971,16 @@ async def websocket_get_configuration(hass, connection, msg): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/configuration/update", vol.Required("data"): ZHA_CONFIG_SCHEMAS, } ) -async def websocket_update_zha_configuration(hass, connection, msg): +@websocket_api.async_response +async def websocket_update_zha_configuration( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Update the ZHA configuration.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] options = zha_gateway.config_entry.options @@ -940,7 +1000,7 @@ async def websocket_update_zha_configuration(hass, connection, msg): @callback -def async_load_api(hass): +def async_load_api(hass: HomeAssistant) -> None: """Set up the web socket API.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] application_controller = zha_gateway.application_controller @@ -972,8 +1032,8 @@ def async_load_api(hass): _LOGGER.info("Permitting joins for %ss", duration) await application_controller.permit(time_s=duration, node=ieee) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT] + async_register_admin_service( + hass, DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT] ) async def remove(service: ServiceCall) -> None: @@ -990,8 +1050,8 @@ def async_load_api(hass): _LOGGER.info("Removing node %s", ieee) await application_controller.remove(ieee) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE] + async_register_admin_service( + hass, DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE] ) async def set_zigbee_cluster_attributes(service: ServiceCall) -> None: @@ -1034,7 +1094,8 @@ def async_load_api(hass): response, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE, set_zigbee_cluster_attributes, @@ -1085,7 +1146,8 @@ def async_load_api(hass): response, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND, issue_zigbee_cluster_command, @@ -1122,7 +1184,8 @@ def async_load_api(hass): response, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND, issue_zigbee_group_command, @@ -1170,7 +1233,8 @@ def async_load_api(hass): level, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK, warning_device_squawk, @@ -1214,7 +1278,8 @@ def async_load_api(hass): level, ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_WARNING_DEVICE_WARN, warning_device_warn, @@ -1247,7 +1312,7 @@ def async_load_api(hass): @callback -def async_unload_api(hass): +def async_unload_api(hass: HomeAssistant) -> None: """Unload the ZHA API.""" hass.services.async_remove(DOMAIN, SERVICE_PERMIT) hass.services.async_remove(DOMAIN, SERVICE_REMOVE) From ad1e43e08357d6e8461ecf3e33a5ce2e3277126b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Mar 2022 01:25:22 -0700 Subject: [PATCH 0513/1054] Add support for variables on trigger (#68275) --- homeassistant/helpers/config_validation.py | 21 ++++++++--- homeassistant/helpers/trigger.py | 31 ++++++++++++++-- tests/helpers/test_trigger.py | 42 ++++++++++++++++++++++ 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 51cd38569fb..fb920c4ef1b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1316,12 +1316,25 @@ CONDITION_ACTION_SCHEMA: vol.Schema = vol.Schema( ) TRIGGER_BASE_SCHEMA = vol.Schema( - {vol.Required(CONF_PLATFORM): str, vol.Optional(CONF_ID): str} + { + vol.Required(CONF_PLATFORM): str, + vol.Optional(CONF_ID): str, + vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, + } ) -TRIGGER_SCHEMA = vol.All( - ensure_list, [TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)] -) + +_base_trigger_validator_schema = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + + +# This is first round of validation, we don't want to process the config here already, +# just ensure basics as platform and ID are there. +def _base_trigger_validator(value: Any) -> Any: + _base_trigger_validator_schema(value) + return value + + +TRIGGER_SCHEMA = vol.All(ensure_list, [_base_trigger_validator]) _SCRIPT_DELAY_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 0b18ad9aa42..79ac9f33f24 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -3,13 +3,14 @@ from __future__ import annotations import asyncio from collections.abc import Callable +import functools import logging from typing import Any import voluptuous as vol -from homeassistant.const import CONF_ID, CONF_PLATFORM -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import CONF_ID, CONF_PLATFORM, CONF_VARIABLES +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -55,6 +56,25 @@ async def async_validate_trigger_config( return config +def _trigger_action_wrapper( + hass: HomeAssistant, action: Callable, conf: ConfigType +) -> Callable: + """Wrap trigger action with extra vars if configured.""" + if CONF_VARIABLES not in conf: + return action + + @functools.wraps(action) + async def with_vars( + run_variables: dict[str, Any], context: Context | None = None + ) -> None: + """Wrap action with extra vars.""" + trigger_variables = conf[CONF_VARIABLES] + run_variables.update(trigger_variables.async_render(hass, run_variables)) + await action(run_variables, context) + + return with_vars + + async def async_initialize_triggers( hass: HomeAssistant, trigger_config: list[ConfigType], @@ -80,7 +100,12 @@ async def async_initialize_triggers( "variables": variables, "trigger_data": trigger_data, } - triggers.append(platform.async_attach_trigger(hass, conf, action, info)) + + triggers.append( + platform.async_attach_trigger( + hass, conf, _trigger_action_wrapper(hass, action, conf), info + ) + ) attach_results = await asyncio.gather(*triggers, return_exceptions=True) removes: list[Callable[[], None]] = [] diff --git a/tests/helpers/test_trigger.py b/tests/helpers/test_trigger.py index 7afdb629792..598906b48c3 100644 --- a/tests/helpers/test_trigger.py +++ b/tests/helpers/test_trigger.py @@ -8,6 +8,15 @@ from homeassistant.helpers.trigger import ( _async_get_trigger_platform, async_validate_trigger_config, ) +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") async def test_bad_trigger_platform(hass): @@ -24,3 +33,36 @@ async def test_trigger_subtype(hass): ) as integration_mock: await _async_get_trigger_platform(hass, {"platform": "test.subtype"}) assert integration_mock.call_args == call(hass, "test") + + +async def test_trigger_variables(hass): + """Test trigger variables.""" + + +async def test_if_fires_on_event(hass, calls): + """Test the firing of events.""" + assert await async_setup_component( + hass, + "automation", + { + "automation": { + "trigger": { + "platform": "event", + "event_type": "test_event", + "variables": { + "name": "Paulus", + "via_event": "{{ trigger.event.event_type }}", + }, + }, + "action": { + "service": "test.automation", + "data_template": {"hello": "{{ name }} + {{ via_event }}"}, + }, + } + }, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["hello"] == "Paulus + test_event" From 0655ebbd843a0845c779d2a25b5bd2991dd7544f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Mar 2022 09:52:17 +0100 Subject: [PATCH 0514/1054] Add config flow for integration sensor (#68288) --- .../components/integration/__init__.py | 24 ++- .../components/integration/config_flow.py | 103 +++++++++++++ homeassistant/components/integration/const.py | 14 ++ .../components/integration/manifest.json | 7 +- .../components/integration/sensor.py | 90 +++++++---- .../components/integration/strings.json | 28 ++++ .../integration/translations/en.json | 28 ++++ homeassistant/generated/config_flows.py | 1 + .../integration/test_config_flow.py | 145 ++++++++++++++++++ tests/components/integration/test_init.py | 61 ++++++++ 10 files changed, 471 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/integration/config_flow.py create mode 100644 homeassistant/components/integration/const.py create mode 100644 homeassistant/components/integration/strings.json create mode 100644 homeassistant/components/integration/translations/en.json create mode 100644 tests/components/integration/test_config_flow.py create mode 100644 tests/components/integration/test_init.py diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 4eea25fefe1..84bc28e3d2f 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -1 +1,23 @@ -"""The integration component.""" +"""The Integration integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Integration from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py new file mode 100644 index 00000000000..76379a89002 --- /dev/null +++ b/homeassistant/components/integration/config_flow.py @@ -0,0 +1,103 @@ +"""Config flow for Integration - Riemann sum integral integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import ( + CONF_METHOD, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + TIME_DAYS, + TIME_HOURS, + TIME_MINUTES, + TIME_SECONDS, +) +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowStep, +) + +from .const import ( + CONF_ROUND_DIGITS, + CONF_SOURCE_SENSOR, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, + DOMAIN, + METHOD_LEFT, + METHOD_RIGHT, + METHOD_TRAPEZOIDAL, +) + +UNIT_PREFIXES = [ + {"value": "none", "label": "none"}, + {"value": "k", "label": "k (kilo)"}, + {"value": "M", "label": "M (mega)"}, + {"value": "G", "label": "T (tera)"}, + {"value": "T", "label": "P (peta)"}, +] +TIME_UNITS = [ + {"value": TIME_SECONDS, "label": "s (seconds)"}, + {"value": TIME_MINUTES, "label": "min (minutes)"}, + {"value": TIME_HOURS, "label": "h (hours)"}, + {"value": TIME_DAYS, "label": "d (days)"}, +] +INTEGRATION_METHODS = [ + {"value": METHOD_TRAPEZOIDAL, "label": "Trapezoidal rule"}, + {"value": METHOD_LEFT, "label": "Left Riemann sum"}, + {"value": METHOD_RIGHT, "label": "Right Riemann sum"}, +] + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + {"number": {"min": 0, "max": 6, "mode": "box"}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_SOURCE_SENSOR): selector.selector( + {"entity": {"domain": "sensor"}}, + ), + vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.selector( + {"select": {"options": INTEGRATION_METHODS}} + ), + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + { + "number": { + "min": 0, + "max": 6, + "mode": "box", + CONF_UNIT_OF_MEASUREMENT: "decimals", + } + } + ), + vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector( + {"select": {"options": UNIT_PREFIXES}} + ), + vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector( + {"select": {"options": TIME_UNITS}} + ), + } +) + +CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} + +OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} + + +class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Integration.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/integration/const.py b/homeassistant/components/integration/const.py new file mode 100644 index 00000000000..b05e4e8f80b --- /dev/null +++ b/homeassistant/components/integration/const.py @@ -0,0 +1,14 @@ +"""Constants for the Integration - Riemann sum integral integration.""" + +DOMAIN = "integration" + +CONF_ROUND_DIGITS = "round" +CONF_SOURCE_SENSOR = "source" +CONF_UNIT_OF_MEASUREMENT = "unit" +CONF_UNIT_PREFIX = "unit_prefix" +CONF_UNIT_TIME = "unit_time" + +METHOD_TRAPEZOIDAL = "trapezoidal" +METHOD_LEFT = "left" +METHOD_RIGHT = "right" +INTEGRATION_METHODS = [METHOD_TRAPEZOIDAL, METHOD_LEFT, METHOD_RIGHT] diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index afec4dbe9ec..a36dc51555f 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -2,7 +2,10 @@ "domain": "integration", "name": "Integration - Riemann sum integral", "documentation": "https://www.home-assistant.io/integrations/integration", - "codeowners": ["@dgomes"], + "codeowners": [ + "@dgomes" + ], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 7a6248254d8..837886bbf56 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -25,29 +26,30 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + CONF_ROUND_DIGITS, + CONF_SOURCE_SENSOR, + CONF_UNIT_OF_MEASUREMENT, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, + INTEGRATION_METHODS, + METHOD_LEFT, + METHOD_RIGHT, + METHOD_TRAPEZOIDAL, +) + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" -CONF_SOURCE_SENSOR = "source" -CONF_ROUND_DIGITS = "round" -CONF_UNIT_PREFIX = "unit_prefix" -CONF_UNIT_TIME = "unit_time" -CONF_UNIT_OF_MEASUREMENT = "unit" - -TRAPEZOIDAL_METHOD = "trapezoidal" -LEFT_METHOD = "left" -RIGHT_METHOD = "right" -INTEGRATION_METHOD = [TRAPEZOIDAL_METHOD, LEFT_METHOD, RIGHT_METHOD] - # SI Metric prefixes UNIT_PREFIXES = {None: 1, "k": 10**3, "M": 10**6, "G": 10**9, "T": 10**12} @@ -73,14 +75,44 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), vol.Optional(CONF_UNIT_TIME, default=TIME_HOURS): vol.In(UNIT_TIME), vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_METHOD, default=TRAPEZOIDAL_METHOD): vol.In( - INTEGRATION_METHOD + vol.Optional(CONF_METHOD, default=METHOD_TRAPEZOIDAL): vol.In( + INTEGRATION_METHODS ), } ), ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Integration - Riemann sum integral config entry.""" + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + source_entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_SOURCE_SENSOR] + ) + + unit_prefix = config_entry.options[CONF_UNIT_PREFIX] + if unit_prefix == "none": + unit_prefix = None + + integral = IntegrationSensor( + integration_method=config_entry.options[CONF_METHOD], + name=config_entry.title, + round_digits=int(config_entry.options[CONF_ROUND_DIGITS]), + source_entity=source_entity_id, + unique_id=config_entry.entry_id, + unit_of_measurement=None, + unit_prefix=unit_prefix, + unit_time=config_entry.options[CONF_UNIT_TIME], + ) + + async_add_entities([integral]) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -89,13 +121,14 @@ async def async_setup_platform( ) -> None: """Set up the integration sensor.""" integral = IntegrationSensor( - config[CONF_SOURCE_SENSOR], - config.get(CONF_NAME), - config[CONF_ROUND_DIGITS], - config[CONF_UNIT_PREFIX], - config[CONF_UNIT_TIME], - config.get(CONF_UNIT_OF_MEASUREMENT), - config[CONF_METHOD], + integration_method=config[CONF_METHOD], + name=config.get(CONF_NAME), + round_digits=config[CONF_ROUND_DIGITS], + source_entity=config[CONF_SOURCE_SENSOR], + unique_id=None, + unit_of_measurement=config.get(CONF_UNIT_OF_MEASUREMENT), + unit_prefix=config[CONF_UNIT_PREFIX], + unit_time=config[CONF_UNIT_TIME], ) async_add_entities([integral]) @@ -106,15 +139,18 @@ class IntegrationSensor(RestoreEntity, SensorEntity): def __init__( self, - source_entity: str, + *, + integration_method: str, name: str | None, round_digits: int, + source_entity: str, + unique_id: str | None, + unit_of_measurement: str | None, unit_prefix: str | None, unit_time: str, - unit_of_measurement: str | None, - integration_method: str, ) -> None: """Initialize the integration sensor.""" + self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits self._state = None @@ -187,15 +223,15 @@ class IntegrationSensor(RestoreEntity, SensorEntity): new_state.last_updated - old_state.last_updated ).total_seconds() - if self._method == TRAPEZOIDAL_METHOD: + if self._method == METHOD_TRAPEZOIDAL: area = ( (Decimal(new_state.state) + Decimal(old_state.state)) * Decimal(elapsed_time) / 2 ) - elif self._method == LEFT_METHOD: + elif self._method == METHOD_LEFT: area = Decimal(old_state.state) * Decimal(elapsed_time) - elif self._method == RIGHT_METHOD: + elif self._method == METHOD_RIGHT: area = Decimal(new_state.state) * Decimal(elapsed_time) integral = area / (self._unit_prefix * self._unit_time) diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json new file mode 100644 index 00000000000..9566ca686d6 --- /dev/null +++ b/homeassistant/components/integration/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "title": "New Integration sensor", + "description": "Precision controls the number of decimal digits in the output.\nThe sum will be scaled according to the selected metric prefix and integration time.", + "data": { + "method": "Integration method", + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "unit_prefix": "Metric prefix", + "unit_time": "Integration time" + } + } + } + }, + "options": { + "step": { + "options": { + "description": "Precision controls the number of decimal digits in the output.", + "data": { + "round": "[%key:component::integration::config::step::user::data::round%]" + } + } + } + } +} diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json new file mode 100644 index 00000000000..31223f01842 --- /dev/null +++ b/homeassistant/components/integration/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integration method", + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "unit_prefix": "Metric prefix", + "unit_time": "Integration time" + }, + "description": "Precision controls the number of decimal digits in the output.\nThe sum will be scaled according to the selected metric prefix and integration time.", + "title": "New Integration sensor" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "round": "Precision" + }, + "description": "Precision controls the number of decimal digits in the output." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6d572efb0c7..d3c6e4816e0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -157,6 +157,7 @@ FLOWS = [ "icloud", "ifttt", "insteon", + "integration", "intellifire", "ios", "iotawatt", diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py new file mode 100644 index 00000000000..5992d480f80 --- /dev/null +++ b/tests/components/integration/test_config_flow.py @@ -0,0 +1,145 @@ +"""Test the Integration - Riemann sum integral config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.integration.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensor_entity_id = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.integration.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "method": "left", + "name": "My integration", + "round": 1, + "source": input_sensor_entity_id, + "unit_prefix": "none", + "unit_time": "min", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My integration" + assert result["data"] == {} + assert result["options"] == { + "method": "left", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "none", + "unit_time": "min", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "method": "left", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "none", + "unit_time": "min", + } + assert config_entry.title == "My integration" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "left", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + }, + title="My integration", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "round") == 1.0 + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "round": 2.0, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "method": "left", + "name": "My integration", + "round": 2.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + } + assert config_entry.data == {} + assert config_entry.options == { + "method": "left", + "name": "My integration", + "round": 2.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + } + assert config_entry.title == "My integration" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + hass.states.async_set("sensor.input", 10, {"unit_of_measurement": "dog"}) + hass.states.async_set("sensor.input", 11, {"unit_of_measurement": "dog"}) + await hass.async_block_till_done() + + state = hass.states.get(f"{platform}.my_integration") + assert state.state != "unknown" + assert state.attributes["unit_of_measurement"] == "kdogmin" diff --git a/tests/components/integration/test_init.py b/tests/components/integration/test_init.py new file mode 100644 index 00000000000..b68e3cdb1eb --- /dev/null +++ b/tests/components/integration/test_init.py @@ -0,0 +1,61 @@ +"""Test the Integration - Riemann sum integral integration.""" +import pytest + +from homeassistant.components.integration.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + input_sensor_entity_id = "sensor.input" + registry = er.async_get(hass) + integration_entity_id = f"{platform}.my_integration" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "method": "trapezoidal", + "name": "My integration", + "round": 1.0, + "source": "sensor.input", + "unit_prefix": "k", + "unit_time": "min", + }, + title="My integration", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(integration_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(integration_entity_id) + assert state.state == "unknown" + assert "unit_of_measurement" not in state.attributes + assert state.attributes["source"] == "sensor.input" + + hass.states.async_set(input_sensor_entity_id, 10, {"unit_of_measurement": "cat"}) + hass.states.async_set(input_sensor_entity_id, 11, {"unit_of_measurement": "cat"}) + await hass.async_block_till_done() + state = hass.states.get(integration_entity_id) + assert state.state != "unknown" + assert state.attributes["unit_of_measurement"] == "kcatmin" + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(integration_entity_id) is None + assert registry.async_get(integration_entity_id) is None From bc862e97ed68cce8c437327651f85892787e755e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Mar 2022 23:09:01 -1000 Subject: [PATCH 0515/1054] Use a dedicated executor pool for database operations (#68105) Co-authored-by: Erik Montnemery Co-authored-by: Franck Nijhof --- homeassistant/components/history/__init__.py | 12 ++-- .../components/history_stats/sensor.py | 4 +- homeassistant/components/logbook/__init__.py | 3 +- homeassistant/components/plant/__init__.py | 5 +- homeassistant/components/recorder/__init__.py | 48 ++++++++++++++- homeassistant/components/recorder/const.py | 2 + homeassistant/components/recorder/executor.py | 59 +++++++++++++++++++ homeassistant/components/recorder/pool.py | 44 +++++++++++--- .../components/recorder/websocket_api.py | 3 +- tests/components/recorder/test_init.py | 27 ++++++++- tests/components/recorder/test_pool.py | 36 +++++++++-- tests/components/recorder/test_util.py | 15 ++++- 12 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/recorder/executor.py diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 1cbd18f44a7..5d870ffa8ee 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -14,7 +14,11 @@ import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import history, models as history_models +from homeassistant.components.recorder import ( + get_instance, + history, + models as history_models, +) from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, @@ -142,7 +146,7 @@ async def ws_get_statistics_during_period( else: end_time = None - statistics = await hass.async_add_executor_job( + statistics = await get_instance(hass).async_add_executor_job( statistics_during_period, hass, start_time, @@ -164,7 +168,7 @@ async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - statistic_ids = await hass.async_add_executor_job( + statistic_ids = await get_instance(hass).async_add_executor_job( list_statistic_ids, hass, msg.get("statistic_type"), @@ -232,7 +236,7 @@ class HistoryPeriodView(HomeAssistantView): return cast( web.Response, - await hass.async_add_executor_job( + await get_instance(hass).async_add_executor_job( self._sorted_significant_states_json, hass, start_time, diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 2af3706e4e8..b33b7ca4db9 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -7,7 +7,7 @@ import math import voluptuous as vol -from homeassistant.components.recorder import history +from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( CONF_ENTITY_ID, @@ -225,7 +225,7 @@ class HistoryStatsSensor(SensorEntity): # Don't compute anything as the value cannot have changed return - await self.hass.async_add_executor_job( + await get_instance(self.hass).async_add_executor_job( self._update, start, end, now_timestamp, start_timestamp, end_timestamp ) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 28b0460ac7a..66c78e30eab 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -15,6 +15,7 @@ from homeassistant.components import frontend from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.history import sqlalchemy_filter_from_include_exclude_conf from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( Events, States, @@ -254,7 +255,7 @@ class LogbookView(HomeAssistantView): ) ) - return await hass.async_add_executor_job(json_events) + return await get_instance(hass).async_add_executor_job(json_events) def humanify(hass, events, entity_attr_cache, context_lookup): diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 37ae8422af0..8b46ad7801e 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -6,6 +6,7 @@ import logging import voluptuous as vol +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( @@ -283,7 +284,9 @@ class Plant(Entity): """After being added to hass, load from history.""" if ENABLE_LOAD_HISTORY and "recorder" in self.hass.config.components: # only use the database if it's configured - await self.hass.async_add_executor_job(self._load_history_from_db) + await get_instance(self.hass).async_add_executor_job( + self._load_history_from_db + ) self.async_write_ha_state() async_track_state_change_event( diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 18acfabffaa..6894d1367e6 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -12,7 +12,7 @@ import queue import sqlite3 import threading import time -from typing import Any +from typing import Any, TypeVar from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.exc import SQLAlchemyError @@ -57,10 +57,12 @@ from . import history, migration, purge, statistics, websocket_api from .const import ( CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, + DB_WORKER_PREFIX, DOMAIN, MAX_QUEUE_BACKLOG, SQLITE_URL_PREFIX, ) +from .executor import DBInterruptibleThreadPoolExecutor from .models import ( Base, Events, @@ -69,7 +71,7 @@ from .models import ( StatisticsRuns, process_timestamp, ) -from .pool import RecorderPool +from .pool import POOL_SIZE, RecorderPool from .util import ( dburl_to_path, end_incomplete_runs, @@ -83,6 +85,9 @@ from .util import ( _LOGGER = logging.getLogger(__name__) +T = TypeVar("T") + + SERVICE_PURGE = "purge" SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" @@ -182,6 +187,15 @@ CONFIG_SCHEMA = vol.Schema( ) +# Pool size must accommodate Recorder thread + All db executors +MAX_DB_EXECUTOR_WORKERS = POOL_SIZE - 1 + + +def get_instance(hass: HomeAssistant) -> Recorder: + """Get the recorder instance.""" + return hass.data[DATA_INSTANCE] + + @bind_hass def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """Check if an entity is being recorded. @@ -537,6 +551,7 @@ class Recorder(threading.Thread): self._queue_watcher = None self._db_supports_row_number = True self._database_lock_task: DatabaseLockTask | None = None + self._db_executor: DBInterruptibleThreadPoolExecutor | None = None self.enabled = True @@ -544,6 +559,20 @@ class Recorder(threading.Thread): """Enable or disable recording events and states.""" self.enabled = enable + @callback + def async_start_executor(self): + """Start the executor.""" + self._db_executor = DBInterruptibleThreadPoolExecutor( + thread_name_prefix=DB_WORKER_PREFIX, + max_workers=MAX_DB_EXECUTOR_WORKERS, + shutdown_hook=self._shutdown_pool, + ) + + def _shutdown_pool(self): + """Close the dbpool connections in the current thread.""" + if hasattr(self.engine.pool, "shutdown"): + self.engine.pool.shutdown() + @callback def async_initialize(self): """Initialize the recorder.""" @@ -554,6 +583,19 @@ class Recorder(threading.Thread): self.hass, self._async_check_queue, timedelta(minutes=10) ) + @callback + def async_add_executor_job( + self, target: Callable[..., T], *args: Any + ) -> asyncio.Future[T]: + """Add an executor job from within the event loop.""" + return self.hass.loop.run_in_executor(self._db_executor, target, *args) + + def _stop_executor(self) -> None: + """Stop the executor.""" + assert self._db_executor is not None + self._db_executor.shutdown() + self._db_executor = None + @callback def _async_check_queue(self, *_): """Periodic check of the queue size to ensure we do not exaust memory. @@ -680,6 +722,7 @@ class Recorder(threading.Thread): def async_connection_success(self): """Connect success tasks.""" self.async_db_ready.set_result(True) + self.async_start_executor() @callback def _async_recorder_ready(self): @@ -1212,6 +1255,7 @@ class Recorder(threading.Thread): def _shutdown(self): """Save end time for current run.""" self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) + self._stop_executor() self._end_session() self._close_connection() diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index a04218264ee..ae0b37e211a 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -15,3 +15,5 @@ MAX_QUEUE_BACKLOG = 30000 # We can increase this back to 1000 once most # have upgraded their sqlite version MAX_ROWS_TO_PURGE = 998 + +DB_WORKER_PREFIX = "DbWorker" diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py new file mode 100644 index 00000000000..782c3422e19 --- /dev/null +++ b/homeassistant/components/recorder/executor.py @@ -0,0 +1,59 @@ +"""Database executor helpers.""" +from __future__ import annotations + +from collections.abc import Callable +from concurrent.futures.thread import _threads_queues, _worker +import threading +from typing import Any +import weakref + +from homeassistant.util.executor import InterruptibleThreadPoolExecutor + + +def _worker_with_shutdown_hook(shutdown_hook, *args, **kwargs): + """Create a worker that calls a function after its finished.""" + _worker(*args, **kwargs) + shutdown_hook() + + +class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor): + """A database instance that will not deadlock on shutdown.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Init the executor with a shutdown hook support.""" + self._shutdown_hook: Callable[[], None] = kwargs.pop("shutdown_hook") + super().__init__(*args, **kwargs) + + def _adjust_thread_count(self) -> None: + """Overridden to add support for shutdown hook. + + Based on the CPython 3.10 implementation. + """ + # if idle threads are available, don't spin new threads + if self._idle_semaphore.acquire( # pylint: disable=consider-using-with + timeout=0 + ): + return + + # When the executor gets lost, the weakref callback will wake up + # the worker threads. + def weakref_cb(_, q=self._work_queue): # pylint: disable=invalid-name + q.put(None) + + num_threads = len(self._threads) + if num_threads < self._max_workers: + thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads) + executor_thread = threading.Thread( + name=thread_name, + target=_worker_with_shutdown_hook, + args=( + self._shutdown_hook, + weakref.ref(self, weakref_cb), + self._work_queue, + self._initializer, + self._initargs, + ), + ) + executor_thread.start() + self._threads.add(executor_thread) # type: ignore[attr-defined] + _threads_queues[executor_thread] = self._work_queue # type: ignore[index] diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index b30237f98da..76b8aceb30f 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -1,34 +1,60 @@ """A pool for sqlite connections.""" import threading -from sqlalchemy.pool import NullPool, StaticPool +from sqlalchemy.pool import NullPool, SingletonThreadPool + +from homeassistant.helpers.frame import report + +from .const import DB_WORKER_PREFIX + +POOL_SIZE = 5 -class RecorderPool(StaticPool, NullPool): - """A hybrid of NullPool and StaticPool. +class RecorderPool(SingletonThreadPool, NullPool): + """A hybrid of NullPool and SingletonThreadPool. - When called from the creating thread acts like StaticPool + When called from the creating thread or db executor acts like SingletonThreadPool When called from any other thread, acts like NullPool """ def __init__(self, *args, **kw): # pylint: disable=super-init-not-called """Create the pool.""" - self._tid = threading.current_thread().ident - StaticPool.__init__(self, *args, **kw) + kw["pool_size"] = POOL_SIZE + SingletonThreadPool.__init__(self, *args, **kw) + + @property + def recorder_or_dbworker(self) -> bool: + """Check if the thread is a recorder or dbworker thread.""" + thread_name = threading.current_thread().name + return bool( + thread_name == "Recorder" or thread_name.startswith(DB_WORKER_PREFIX) + ) def _do_return_conn(self, conn): - if threading.current_thread().ident == self._tid: + if self.recorder_or_dbworker: return super()._do_return_conn(conn) conn.close() + def shutdown(self): + """Close the connection.""" + if self.recorder_or_dbworker and (conn := self._conn.current()): + conn.close() + def dispose(self): """Dispose of the connection.""" - if threading.current_thread().ident == self._tid: + if self.recorder_or_dbworker: return super().dispose() def _do_get(self): - if threading.current_thread().ident == self._tid: + if self.recorder_or_dbworker: return super()._do_get() + report( + "accesses the database without the database executor; " + "Use homeassistant.components.recorder.get_instance(hass).async_add_executor_job() " + "for faster database operations", + exclude_integrations={"recorder"}, + error_if_core=False, + ) return super( # pylint: disable=bad-super-call NullPool, self )._create_connection() diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index aec7905615f..ee5081b3c1c 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -40,7 +40,8 @@ async def ws_validate_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - statistic_ids = await hass.async_add_executor_job( + instance: Recorder = hass.data[DATA_INSTANCE] + statistic_ids = await instance.async_add_executor_job( validate_statistics, hass, ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 6bdc8250afc..ab05c6ec05b 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -4,7 +4,7 @@ import asyncio from datetime import datetime, timedelta import sqlite3 import threading -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError @@ -22,6 +22,7 @@ from homeassistant.components.recorder import ( SERVICE_PURGE_ENTITIES, SQLITE_URL_PREFIX, Recorder, + get_instance, run_information, run_information_from_instance, run_information_with_session, @@ -101,6 +102,30 @@ async def test_shutdown_before_startup_finishes(hass): assert run_info.end is not None +async def test_shutdown_closes_connections(hass): + """Test shutdown closes connections.""" + + hass.state = CoreState.not_running + + await async_init_recorder_component(hass) + instance = get_instance(hass) + await instance.async_db_ready + await hass.async_block_till_done() + pool = instance.engine.pool + pool.shutdown = Mock() + + def _ensure_connected(): + with session_scope(hass=hass) as session: + list(session.query(States)) + + await instance.async_add_executor_job(_ensure_connected) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + assert len(pool.shutdown.mock_calls) == 1 + + async def test_state_gets_saved_when_set_before_start_event( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): diff --git a/tests/components/recorder/test_pool.py b/tests/components/recorder/test_pool.py index e59dc18fc8b..ca6a88d84a7 100644 --- a/tests/components/recorder/test_pool.py +++ b/tests/components/recorder/test_pool.py @@ -4,15 +4,16 @@ import threading from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from homeassistant.components.recorder.const import DB_WORKER_PREFIX from homeassistant.components.recorder.pool import RecorderPool -def test_recorder_pool(): +def test_recorder_pool(caplog): """Test RecorderPool gives the same connection in the creating thread.""" engine = create_engine("sqlite://", poolclass=RecorderPool) get_session = sessionmaker(bind=engine) - + shutdown = False connections = [] def _get_connection_twice(): @@ -20,15 +21,42 @@ def test_recorder_pool(): connections.append(session.connection().connection.connection) session.close() + if shutdown: + engine.pool.shutdown() + session = get_session() connections.append(session.connection().connection.connection) session.close() _get_connection_twice() - assert connections[0] == connections[1] + assert "accesses the database without the database executor" in caplog.text + assert connections[0] != connections[1] + caplog.clear() new_thread = threading.Thread(target=_get_connection_twice) new_thread.start() new_thread.join() - + assert "accesses the database without the database executor" in caplog.text assert connections[2] != connections[3] + + caplog.clear() + new_thread = threading.Thread(target=_get_connection_twice, name=DB_WORKER_PREFIX) + new_thread.start() + new_thread.join() + assert "accesses the database without the database executor" not in caplog.text + assert connections[4] == connections[5] + + caplog.clear() + new_thread = threading.Thread(target=_get_connection_twice, name="Recorder") + new_thread.start() + new_thread.join() + assert "accesses the database without the database executor" not in caplog.text + assert connections[6] == connections[7] + + shutdown = True + caplog.clear() + new_thread = threading.Thread(target=_get_connection_twice, name=DB_WORKER_PREFIX) + new_thread.start() + new_thread.join() + assert "accesses the database without the database executor" not in caplog.text + assert connections[8] != connections[9] diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ec74ea73975..fe38aa2ab4f 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -570,8 +570,17 @@ async def test_write_lock_db(hass, tmp_path): instance = hass.data[DATA_INSTANCE] + def _drop_table(): + with instance.engine.connect() as connection: + connection.execute(text("DROP TABLE events;")) + with util.write_lock_db_sqlite(instance): # Database should be locked now, try writing SQL command - with instance.engine.connect() as connection: - with pytest.raises(OperationalError): - connection.execute(text("DROP TABLE events;")) + with pytest.raises(OperationalError): + # This needs to be called in another thread since + # the lock method is BEGIN IMMEDIATE and since we have + # a connection per thread with sqlite now, we cannot do it + # in the same thread as the one holding the lock since it + # would be allowed to proceed as the goal is to prevent + # all the other threads from accessing the database + await hass.async_add_executor_job(_drop_table) From 2fcced333d2ec0a6e3775209c973ba8fb1d5f18a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Mar 2022 10:12:15 +0100 Subject: [PATCH 0516/1054] Add config flow for sun (#68295) Co-authored-by: Franck Nijhof --- homeassistant/components/sun/__init__.py | 53 +++++++++++++-- homeassistant/components/sun/config_flow.py | 31 +++++++++ homeassistant/components/sun/const.py | 6 ++ homeassistant/components/sun/manifest.json | 3 +- homeassistant/components/sun/strings.json | 10 +++ homeassistant/generated/config_flows.py | 1 + tests/components/sun/test_config_flow.py | 75 +++++++++++++++++++++ tests/components/sun/test_init.py | 65 +++++++++++++++++- 8 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/sun/config_flow.py create mode 100644 homeassistant/components/sun/const.py create mode 100644 tests/components/sun/test_config_flow.py diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 4789490ef0d..b569e9df7bd 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_ELEVATION, EVENT_CORE_CONFIG_UPDATE, @@ -18,12 +19,12 @@ from homeassistant.helpers.sun import ( from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util +from .const import DOMAIN + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) -DOMAIN = "sun" - ENTITY_ID = "sun.sun" STATE_ABOVE_HORIZON = "above_horizon" @@ -80,7 +81,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "Elevation is now configured in Home Assistant core. " "See https://www.home-assistant.io/docs/configuration/basic/" ) - Sun(hass) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + hass.data[DOMAIN] = Sun(hass) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + sun = hass.data.pop(DOMAIN) + sun.remove_listeners() + hass.states.async_remove(sun.entity_id) return True @@ -100,6 +121,9 @@ class Sun(Entity): self.solar_elevation = self.solar_azimuth = None self.rising = self.phase = None self._next_change = None + self._config_listener = None + self._update_events_listener = None + self._update_sun_position_listener = None @callback def update_location(_event): @@ -108,10 +132,24 @@ class Sun(Entity): return self.location = location self.elevation = elevation + if self._update_events_listener: + self._update_events_listener() self.update_events() update_location(None) - self.hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_location) + self._config_listener = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, update_location + ) + + @callback + def remove_listeners(self): + """Remove listeners.""" + if self._config_listener: + self._config_listener() + if self._update_events_listener: + self._update_events_listener() + if self._update_sun_position_listener: + self._update_sun_position_listener() @property def name(self): @@ -211,10 +249,12 @@ class Sun(Entity): _LOGGER.debug( "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase ) + if self._update_sun_position_listener: + self._update_sun_position_listener() self.update_sun_position() # Set timer for the next solar event - event.async_track_point_in_utc_time( + self._update_events_listener = event.async_track_point_in_utc_time( self.hass, self.update_events, self._next_change ) _LOGGER.debug("next time: %s", self._next_change.isoformat()) @@ -244,7 +284,8 @@ class Sun(Entity): # if the next update is within 1.25 of the next # position update just drop it if utc_point_in_time + delta * 1.25 > self._next_change: + self._update_sun_position_listener = None return - event.async_track_point_in_utc_time( + self._update_sun_position_listener = event.async_track_point_in_utc_time( self.hass, self.update_sun_position, utc_point_in_time + delta ) diff --git a/homeassistant/components/sun/config_flow.py b/homeassistant/components/sun/config_flow.py new file mode 100644 index 00000000000..ae2e5a42efc --- /dev/null +++ b/homeassistant/components/sun/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow to configure the Sun integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class SunConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Sun.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is not None: + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="user") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/sun/const.py b/homeassistant/components/sun/const.py new file mode 100644 index 00000000000..f567c77e62a --- /dev/null +++ b/homeassistant/components/sun/const.py @@ -0,0 +1,6 @@ +"""Constants for the Sun integration.""" +from typing import Final + +DOMAIN: Final = "sun" + +DEFAULT_NAME: Final = "Sun" diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index 93fb76629cc..dd13bf556fb 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sun", "codeowners": ["@Swamp-Ig"], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/sun/strings.json b/homeassistant/components/sun/strings.json index 980879b95cb..cdcaa416eda 100644 --- a/homeassistant/components/sun/strings.json +++ b/homeassistant/components/sun/strings.json @@ -1,5 +1,15 @@ { "title": "Sun", + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, "state": { "_": { "above_horizon": "Above horizon", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d3c6e4816e0..a6591cc693c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -323,6 +323,7 @@ FLOWS = [ "steamist", "stookalert", "subaru", + "sun", "surepetcare", "switch_as_x", "switchbot", diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py new file mode 100644 index 00000000000..1712e8f6fc9 --- /dev/null +++ b/tests/components/sun/test_config_flow.py @@ -0,0 +1,75 @@ +"""Tests for the Sun config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.sun.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.sun.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "Sun" + assert result.get("data") == {} + assert result.get("options") == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_single_instance_allowed( + hass: HomeAssistant, + source: str, +) -> None: + """Test we abort if already setup.""" + mock_config_entry = MockConfigEntry(domain=DOMAIN) + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" + + +async def test_import_flow( + hass: HomeAssistant, +) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "Sun" + assert result.get("data") == {} + assert result.get("options") == {} diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 800d3ab82fd..85814d33d6c 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -10,6 +10,8 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import MockConfigEntry + async def test_setting_rising(hass, legacy_patchable_time): """Test retrieving sun setting and rising.""" @@ -105,7 +107,7 @@ async def test_setting_rising(hass, legacy_patchable_time): ) -async def test_state_change(hass, legacy_patchable_time): +async def test_state_change(hass, legacy_patchable_time, caplog): """Test if the state changes at next setting/rising.""" now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=now): @@ -131,12 +133,34 @@ async def test_state_change(hass, legacy_patchable_time): assert sun.STATE_ABOVE_HORIZON == hass.states.get(sun.ENTITY_ID).state + # Update core configuration with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=now): await hass.config.async_update(longitude=hass.config.longitude + 90) await hass.async_block_till_done() assert sun.STATE_ABOVE_HORIZON == hass.states.get(sun.ENTITY_ID).state + # Test listeners are not duplicated after a core configuration change + test_time = dt_util.parse_datetime( + hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_DUSK] + ) + assert test_time is not None + + patched_time = test_time + timedelta(seconds=5) + caplog.clear() + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=patched_time + ): + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: patched_time}) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert caplog.text.count("sun phase_update") == 1 + # Called once by time listener, once from Sun.update_events + assert caplog.text.count("sun position_update") == 2 + + assert sun.STATE_BELOW_HORIZON == hass.states.get(sun.ENTITY_ID).state + async def test_norway_in_june(hass): """Test location in Norway where the sun doesn't set in summer.""" @@ -194,3 +218,42 @@ async def test_state_change_count(hass): await hass.async_block_till_done() assert len(events) < 721 + + +async def test_setup_and_remove_config_entry( + hass: ha.HomeAssistant, legacy_patchable_time +) -> None: + """Test setting up and removing a config entry.""" + # Setup the config entry + config_entry = MockConfigEntry(domain=sun.DOMAIN) + config_entry.add_to_hass(hass) + now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=now): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the platform is setup correctly + state = hass.states.get("sun.sun") + assert state is not None + + test_time = dt_util.parse_datetime( + hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_RISING] + ) + assert test_time is not None + assert sun.STATE_BELOW_HORIZON == hass.states.get(sun.ENTITY_ID).state + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state is removed, and does not reappear + assert hass.states.get("sun.sun") is None + + patched_time = test_time + timedelta(seconds=5) + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=patched_time + ): + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: patched_time}) + await hass.async_block_till_done() + + assert hass.states.get("sun.sun") is None From d7145095ef6d7de153289f00e7c13e8a77fe9d55 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Mar 2022 10:26:05 +0100 Subject: [PATCH 0517/1054] Tweak selectors (#68267) * Optionally don't convert output of duration and time selectors * Allow number selector selection to be None * Never convert output of duration and time selectors * Revert "Allow number selector selection to be None" This reverts commit b6f52c1e83dc287a30c8ef4c2e417dc20b26a1d6. --- homeassistant/helpers/selector.py | 12 ++++++------ tests/helpers/test_selector.py | 15 ++------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 3359deb9b09..6668229c0cf 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from datetime import time as time_sys, timedelta from typing import Any, cast import voluptuous as vol @@ -261,9 +260,10 @@ class TimeSelector(Selector): CONFIG_SCHEMA = vol.Schema({}) - def __call__(self, data: Any) -> time_sys: + def __call__(self, data: Any) -> str: """Validate the passed selection.""" - return cv.time(data) + cv.time(data) + return cast(str, data) @SELECTORS.register("target") @@ -409,10 +409,10 @@ class DurationSelector(Selector): CONFIG_SCHEMA = vol.Schema({}) - def __call__(self, data: Any) -> timedelta: + def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" - duration: timedelta = cv.time_period_dict(data) - return duration + cv.time_period_dict(data) + return cast(dict[str, float], data) @SELECTORS.register("icon") diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 7031f16d249..e9e70f15fad 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1,11 +1,8 @@ """Test selectors.""" -from datetime import timedelta - import pytest import voluptuous as vol from homeassistant.helpers import config_validation as cv, selector -from homeassistant.util import dt as dt_util FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @@ -253,9 +250,7 @@ def test_boolean_selector_schema(schema, valid_selections, invalid_selections): ) def test_time_selector_schema(schema, valid_selections, invalid_selections): """Test time selector.""" - _test_selector( - "time", schema, valid_selections, invalid_selections, dt_util.parse_time - ) + _test_selector("time", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( @@ -393,13 +388,7 @@ def test_attribute_selector_schema(schema, valid_selections, invalid_selections) ) def test_duration_selector_schema(schema, valid_selections, invalid_selections): """Test duration selector.""" - _test_selector( - "duration", - schema, - valid_selections, - invalid_selections, - lambda x: timedelta(**x), - ) + _test_selector("duration", schema, valid_selections, invalid_selections) @pytest.mark.parametrize( From 9215702388eef03c7c3ed9f756ea0db533d5beec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 00:23:13 -1000 Subject: [PATCH 0518/1054] Separate attrs into another table (reduces database size) (#68224) --- homeassistant/components/logbook/__init__.py | 35 ++- homeassistant/components/plant/__init__.py | 23 +- homeassistant/components/recorder/__init__.py | 75 +++-- homeassistant/components/recorder/history.py | 26 +- .../components/recorder/manifest.json | 2 +- .../components/recorder/migration.py | 3 + homeassistant/components/recorder/models.py | 102 +++++-- homeassistant/components/recorder/purge.py | 133 ++++++++- homeassistant/components/statistics/sensor.py | 15 +- homeassistant/package_constraints.txt | 2 + requirements_all.txt | 4 + requirements_test_all.txt | 4 + tests/components/logbook/test_init.py | 46 +++- tests/components/plant/test_init.py | 19 +- tests/components/recorder/test_init.py | 17 +- tests/components/recorder/test_models.py | 123 ++++++++- tests/components/recorder/test_purge.py | 257 +++++++++++++++++- 17 files changed, 788 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 66c78e30eab..8860daaec3c 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -5,6 +5,7 @@ from http import HTTPStatus from itertools import groupby import json import re +from typing import Any import sqlalchemy from sqlalchemy.orm import aliased @@ -18,6 +19,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( Events, + StateAttributes, States, process_timestamp_to_utc_isoformat, ) @@ -494,6 +496,7 @@ def _generate_events_query(session): States.entity_id, States.domain, States.attributes, + StateAttributes.shared_attrs, ) @@ -504,6 +507,7 @@ def _generate_events_query_without_states(session): literal(value=None, type_=sqlalchemy.String).label("entity_id"), literal(value=None, type_=sqlalchemy.String).label("domain"), literal(value=None, type_=sqlalchemy.Text).label("attributes"), + literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), ) @@ -519,6 +523,9 @@ def _generate_states_query(session, start_day, end_day, old_state, entity_ids): (States.last_updated == States.last_changed) & States.entity_id.in_(entity_ids) ) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) ) @@ -534,7 +541,9 @@ def _apply_events_types_and_states_filter(hass, query, old_state): (Events.event_type != EVENT_STATE_CHANGED) | _continuous_entity_matcher() ) ) - return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES) + return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES).outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) def _missing_state_matcher(old_state): @@ -556,6 +565,9 @@ def _continuous_entity_matcher(): return sqlalchemy.or_( sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)), sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)), + sqlalchemy.not_( + StateAttributes.shared_attrs.contains(UNIT_OF_MEASUREMENT_JSON) + ), ) @@ -709,8 +721,9 @@ class LazyEventPartialState: """Extract the icon from the decoded attributes or json.""" if self._attributes: return self._attributes.get(ATTR_ICON) - - result = ICON_JSON_EXTRACT.search(self._row.attributes) + result = ICON_JSON_EXTRACT.search( + self._row.shared_attrs or self._row.attributes + ) return result and result.group(1) @property @@ -734,14 +747,12 @@ class LazyEventPartialState: @property def attributes(self): """State attributes.""" - if not self._attributes: - if ( - self._row.attributes is None - or self._row.attributes == EMPTY_JSON_OBJECT - ): + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if source == EMPTY_JSON_OBJECT or source is None: self._attributes = {} else: - self._attributes = json.loads(self._row.attributes) + self._attributes = json.loads(source) return self._attributes @property @@ -772,12 +783,12 @@ class EntityAttributeCache: that are expected to change state. """ - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Init the cache.""" self._hass = hass - self._cache = {} + self._cache: dict[str, dict[str, Any]] = {} - def get(self, entity_id, attribute, event): + def get(self, entity_id: str, attribute: str, event: LazyEventPartialState) -> Any: """Lookup an attribute for an entity or get it from the cache.""" if entity_id in self._cache: if attribute in self._cache[entity_id]: diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 8b46ad7801e..d9d207e215b 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -7,7 +7,7 @@ import logging import voluptuous as vol from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.models import StateAttributes, States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( ATTR_TEMPERATURE, @@ -110,11 +110,6 @@ DOMAIN = "plant" CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_EXTRA) -# Flag for enabling/disabling the loading of the history from the database. -# This feature is turned off right now as its tests are not 100% stable. -ENABLE_LOAD_HISTORY = False - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Plant component.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -282,7 +277,7 @@ class Plant(Entity): async def async_added_to_hass(self): """After being added to hass, load from history.""" - if ENABLE_LOAD_HISTORY and "recorder" in self.hass.config.components: + if "recorder" in self.hass.config.components: # only use the database if it's configured await get_instance(self.hass).async_add_executor_job( self._load_history_from_db @@ -315,14 +310,24 @@ class Plant(Entity): _LOGGER.debug("Initializing values for %s from the database", self._name) with session_scope(hass=self.hass) as session: query = ( - session.query(States) + session.query(States, StateAttributes) .filter( (States.entity_id == entity_id.lower()) and (States.last_updated > start_date) ) + .outerjoin( + StateAttributes, + States.attributes_id == StateAttributes.attributes_id, + ) .order_by(States.last_updated.asc()) ) - states = execute(query, to_native=True, validate_entity_ids=False) + states = [] + if results := execute(query, to_native=False, validate_entity_ids=False): + for state, attributes in results: + native = state.to_native() + if not native.attributes: + native.attributes = attributes.to_native() + states.append(native) for state in states: # filter out all None, NaN and "unknown" states diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 6894d1367e6..2ee6896d032 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -14,6 +14,7 @@ import threading import time from typing import Any, TypeVar +from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker @@ -67,6 +68,7 @@ from .models import ( Base, Events, RecorderRuns, + StateAttributes, States, StatisticsRuns, process_timestamp, @@ -131,6 +133,15 @@ KEEPALIVE_TIME = 30 # States and Events objects EXPIRE_AFTER_COMMITS = 120 +# The number of attribute ids to cache in memory +# +# Based on: +# - The number of overlapping attributes +# - How frequently states with overlapping attributes will change +# - How much memory our low end hardware has +STATE_ATTRIBUTES_ID_CACHE_SIZE = 2048 + + DB_LOCK_TIMEOUT = 30 DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 @@ -541,6 +552,8 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 self._keepalive_count = 0 self._old_states: dict[str, States] = {} + self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) + self._pending_state_attributes: dict[str, StateAttributes] = {} self._pending_expunge: list[States] = [] self.event_session = None self.get_session = None @@ -964,33 +977,58 @@ class Recorder(threading.Thread): dbevent.event_data = None else: dbevent = Events.from_event(event) - self.event_session.add(dbevent) except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) return + self.event_session.add(dbevent) if event.event_type == EVENT_STATE_CHANGED: try: dbstate = States.from_event(event) - has_new_state = event.data.get("new_state") - if dbstate.entity_id in self._old_states: - old_state = self._old_states.pop(dbstate.entity_id) - if old_state.state_id: - dbstate.old_state_id = old_state.state_id - else: - dbstate.old_state = old_state - if not has_new_state: - dbstate.state = None - dbstate.event = dbevent - self.event_session.add(dbstate) - if has_new_state: - self._old_states[dbstate.entity_id] = dbstate - self._pending_expunge.append(dbstate) - except (TypeError, ValueError): + dbstate_attributes = StateAttributes.from_event(event) + except (TypeError, ValueError) as ex: _LOGGER.warning( - "State is not JSON serializable: %s", + "State is not JSON serializable: %s: %s", event.data.get("new_state"), + ex, ) + return + + dbstate.attributes = None + shared_attrs = dbstate_attributes.shared_attrs + # Matching attributes found in the pending commit + if pending_attributes := self._pending_state_attributes.get(shared_attrs): + dbstate.state_attributes = pending_attributes + # Matching attributes id found in the cache + elif attributes_id := self._state_attributes_ids.get(shared_attrs): + dbstate.attributes_id = attributes_id + # Matching attributes found in the database + elif ( + attributes := self.event_session.query(StateAttributes.attributes_id) + .filter(StateAttributes.hash == dbstate_attributes.hash) + .filter(StateAttributes.shared_attrs == shared_attrs) + .first() + ): + dbstate.attributes_id = attributes[0] + self._state_attributes_ids[shared_attrs] = attributes[0] + # No matching attributes found, save them in the DB + else: + dbstate.state_attributes = dbstate_attributes + self._pending_state_attributes[shared_attrs] = dbstate_attributes + self.event_session.add(dbstate_attributes) + + if old_state := self._old_states.pop(dbstate.entity_id, None): + if old_state.state_id: + dbstate.old_state_id = old_state.state_id + else: + dbstate.old_state = old_state + if event.data.get("new_state"): + self._old_states[dbstate.entity_id] = dbstate + self._pending_expunge.append(dbstate) + else: + dbstate.state = None + self.event_session.add(dbstate) + dbstate.event = dbevent # If they do not have a commit interval # than we commit right away @@ -1042,6 +1080,7 @@ class Recorder(threading.Thread): if dbstate in self.event_session: self.event_session.expunge(dbstate) self._pending_expunge = [] + self._pending_state_attributes = {} self.event_session.commit() # Expire is an expensive operation (frequently more expensive @@ -1062,6 +1101,8 @@ class Recorder(threading.Thread): def _close_event_session(self): """Close the event session.""" self._old_states = {} + self._state_attributes_ids = {} + self._pending_state_attributes = {} if not self.event_session: return diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 8be60a60ac6..13574bc654f 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -13,7 +13,12 @@ from homeassistant.components import recorder from homeassistant.core import split_entity_id import homeassistant.util.dt as dt_util -from .models import LazyState, States, process_timestamp_to_utc_isoformat +from .models import ( + LazyState, + StateAttributes, + States, + process_timestamp_to_utc_isoformat, +) from .util import execute, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -46,6 +51,7 @@ QUERY_STATES = [ States.attributes, States.last_changed, States.last_updated, + StateAttributes.shared_attrs, ] HISTORY_BAKERY = "recorder_history_bakery" @@ -114,6 +120,9 @@ def get_significant_states_with_session( if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) states = execute( @@ -159,6 +168,9 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) states = execute( @@ -186,6 +198,9 @@ def get_last_state_changes(hass, number_of_states, entity_id): baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by( States.entity_id, States.last_updated.desc() ) @@ -263,6 +278,8 @@ def _get_states_with_session( query = query.join( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, + ).outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) else: # We did not get an include-list of entities, query all states in the inner @@ -301,7 +318,9 @@ def _get_states_with_session( query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) if filters: query = filters.apply(query) - + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) return [LazyState(row) for row in execute(query)] @@ -315,6 +334,9 @@ def _get_single_entity_states_with_session(hass, session, utc_point_in_time, ent States.last_updated < bindparam("utc_point_in_time"), States.entity_id == bindparam("entity_id"), ) + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.last_updated.desc()) baked_query += lambda q: q.limit(1) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index d4ac7fa91eb..fdadbfb328e 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.32"], + "requirements": ["sqlalchemy==1.4.32","fnvhash==0.1.0","lru-dict==1.1.7"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 48dca4d42ed..096ec380cf6 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -638,6 +638,9 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) + elif new_version == 25: + _add_columns(instance, "states", ["attributes_id INTEGER(20)"]) + _create_index(instance, "states", "ix_states_attributes_id") else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index b49189a9c3a..329af989568 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -6,7 +6,9 @@ import json import logging from typing import TypedDict, overload +from fnvhash import fnv1a_32 from sqlalchemy import ( + BigInteger, Boolean, Column, DateTime, @@ -40,7 +42,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 24 +SCHEMA_VERSION = 25 _LOGGER = logging.getLogger(__name__) @@ -48,6 +50,7 @@ DB_TIMEZONE = "+00:00" TABLE_EVENTS = "events" TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" TABLE_RECORDER_RUNS = "recorder_runs" TABLE_SCHEMA_CHANGES = "schema_changes" TABLE_STATISTICS = "statistics" @@ -66,6 +69,9 @@ ALL_TABLES = [ TABLE_STATISTICS_SHORT_TERM, ] +EMPTY_JSON_OBJECT = "{}" + + DATETIME_TYPE = DateTime(timezone=True).with_variant( mysql.DATETIME(timezone=True, fsp=6), "mysql" ) @@ -161,8 +167,12 @@ class States(Base): # type: ignore[misc,valid-type] last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) event = relationship("Events", uselist=False) old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") def __repr__(self) -> str: """Return string representation of instance for debugging.""" @@ -171,7 +181,7 @@ class States(Base): # type: ignore[misc,valid-type] f"id={self.state_id}, domain='{self.domain}', entity_id='{self.entity_id}', " f"state='{self.state}', event_id='{self.event_id}', " f"last_updated='{self.last_updated.isoformat(sep=' ', timespec='seconds')}', " - f"old_state_id={self.old_state_id}" + f"old_state_id={self.old_state_id}, attributes_id={self.attributes_id}" f")>" ) @@ -182,20 +192,17 @@ class States(Base): # type: ignore[misc,valid-type] state = event.data.get("new_state") dbstate = States(entity_id=entity_id) + dbstate.attributes = None # State got deleted if state is None: dbstate.state = "" dbstate.domain = split_entity_id(entity_id)[0] - dbstate.attributes = "{}" dbstate.last_changed = event.time_fired dbstate.last_updated = event.time_fired else: dbstate.domain = state.domain dbstate.state = state.state - dbstate.attributes = json.dumps( - dict(state.attributes), cls=JSONEncoder, separators=(",", ":") - ) dbstate.last_changed = state.last_changed dbstate.last_updated = state.last_updated @@ -207,7 +214,9 @@ class States(Base): # type: ignore[misc,valid-type] return State( self.entity_id, self.state, - json.loads(self.attributes), + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + json.loads(self.attributes) if self.attributes else {}, process_timestamp(self.last_changed), process_timestamp(self.last_updated), # Join the events table on event_id to get the context instead @@ -221,6 +230,53 @@ class States(Base): # type: ignore[misc,valid-type] return None +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + state = event.data.get("new_state") + dbstate = StateAttributes() + # State got deleted + if state is None: + dbstate.shared_attrs = "{}" + else: + dbstate.shared_attrs = json.dumps( + dict(state.attributes), + cls=JSONEncoder, + separators=(",", ":"), + ) + dbstate.hash = fnv1a_32(dbstate.shared_attrs.encode("utf-8")) + return dbstate + + def to_native(self): + """Convert to an HA state object.""" + try: + return json.loads(self.shared_attrs) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + class StatisticResult(TypedDict): """Statistic result data class. @@ -492,12 +548,18 @@ class LazyState(State): @property # type: ignore[override] def attributes(self): """State attributes.""" - if not self._attributes: + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if source == EMPTY_JSON_OBJECT or source is None: + self._attributes = {} + return self._attributes try: - self._attributes = json.loads(self._row.attributes) + self._attributes = json.loads(source) except ValueError: # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self._row) + _LOGGER.exception( + "Error converting row to state attributes: %s", self._row + ) self._attributes = {} return self._attributes @@ -549,18 +611,22 @@ class LazyState(State): To be used for JSON serialization. """ - if self._last_changed: - last_changed_isoformat = self._last_changed.isoformat() - else: + if self._last_changed is None and self._last_updated is None: last_changed_isoformat = process_timestamp_to_utc_isoformat( self._row.last_changed ) - if self._last_updated: - last_updated_isoformat = self._last_updated.isoformat() + if self._row.last_changed == self._row.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) else: - last_updated_isoformat = process_timestamp_to_utc_isoformat( - self._row.last_updated - ) + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() return { "entity_id": self.entity_id, "state": self.state, diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index dd80fb15479..0109d68f0f5 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -10,8 +10,17 @@ from sqlalchemy import func from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct +from homeassistant.const import EVENT_STATE_CHANGED + from .const import MAX_ROWS_TO_PURGE -from .models import Events, RecorderRuns, States, StatisticsRuns, StatisticsShortTerm +from .models import ( + Events, + RecorderRuns, + StateAttributes, + States, + StatisticsRuns, + StatisticsShortTerm, +) from .repack import repack_database from .util import retryable_database_job, session_scope @@ -37,7 +46,12 @@ def purge_old_data( with session_scope(session=instance.get_session()) as session: # type: ignore[misc] # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record event_ids = _select_event_ids_to_purge(session, purge_before) - state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) + state_ids, attributes_ids = _select_state_and_attributes_ids_to_purge( + session, purge_before, event_ids + ) + attributes_ids = _remove_attributes_ids_used_by_newer_states( + session, purge_before, attributes_ids + ) statistics_runs = _select_statistics_runs_to_purge(session, purge_before) short_term_statistics = _select_short_term_statistics_to_purge( session, purge_before @@ -46,6 +60,9 @@ def purge_old_data( if state_ids: _purge_state_ids(instance, session, state_ids) + if attributes_ids: + _purge_attributes_ids(instance, session, attributes_ids) + if event_ids: _purge_event_ids(session, event_ids) @@ -82,20 +99,47 @@ def _select_event_ids_to_purge(session: Session, purge_before: datetime) -> list return [event.event_id for event in events] -def _select_state_ids_to_purge( +def _select_state_and_attributes_ids_to_purge( session: Session, purge_before: datetime, event_ids: list[int] -) -> set[int]: +) -> tuple[set[int], set[int]]: """Return a list of state ids to purge.""" if not event_ids: - return set() + return set(), set() states = ( - session.query(States.state_id) + session.query(States.state_id, States.attributes_id) .filter(States.last_updated < purge_before) .filter(States.event_id.in_(event_ids)) .all() ) _LOGGER.debug("Selected %s state ids to remove", len(states)) - return {state.state_id for state in states} + state_ids = set() + attributes_ids = set() + for state in states: + state_ids.add(state.state_id) + if state.attributes_id: + attributes_ids.add(state.attributes_id) + return state_ids, attributes_ids + + +def _remove_attributes_ids_used_by_newer_states( + session: Session, purge_before: datetime, attributes_ids: set[int] +) -> set[int]: + """Remove attributes ids that are still in use for states we are not purging yet.""" + if not attributes_ids: + return set() + keep_attributes_ids = { + state.attributes_id + for state in session.query(States.attributes_id) + .filter(States.last_updated >= purge_before) + .filter(States.attributes_id.in_(attributes_ids)) + .group_by(States.attributes_id) + } + to_remove = attributes_ids - keep_attributes_ids + _LOGGER.debug( + "Selected %s shared attributes to remove", + len(to_remove), + ) + return to_remove def _select_statistics_runs_to_purge( @@ -143,7 +187,9 @@ def _purge_state_ids(instance: Recorder, session: Session, state_ids: set[int]) disconnected_rows = ( session.query(States) .filter(States.old_state_id.in_(state_ids)) - .update({"old_state_id": None}, synchronize_session=False) + .update( + {"old_state_id": None, "attributes_id": None}, synchronize_session=False + ) ) _LOGGER.debug("Updated %s states to remove old_state_id", disconnected_rows) @@ -175,6 +221,44 @@ def _evict_purged_states_from_old_states_cache( old_states.pop(old_state_reversed[purged_state_id], None) +def _evict_purged_attributes_from_attributes_cache( + instance: Recorder, purged_attributes_ids: set[int] +) -> None: + """Evict purged attribute ids from the attribute ids cache.""" + # Make a map from attributes_id to the attributes json + state_attributes_ids = ( + instance._state_attributes_ids # pylint: disable=protected-access + ) + state_attributes_ids_reversed = { + attributes_id: attributes + for attributes, attributes_id in state_attributes_ids.items() + } + + # Evict any purged attributes from the state_attributes_ids cache + for purged_attribute_id in purged_attributes_ids.intersection( + state_attributes_ids_reversed + ): + state_attributes_ids.pop( + state_attributes_ids_reversed[purged_attribute_id], None + ) + + +def _purge_attributes_ids( + instance: Recorder, session: Session, attributes_ids: set[int] +) -> None: + """Delete old attributes ids.""" + + deleted_rows = ( + session.query(StateAttributes) + .filter(StateAttributes.attributes_id.in_(attributes_ids)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s attribute states", deleted_rows) + + # Evict any entries in the state_attributes_ids cache referring to a purged state + _evict_purged_attributes_from_attributes_cache(instance, attributes_ids) + + def _purge_statistics_runs(session: Session, statistics_runs: list[int]) -> None: """Delete by run_id.""" deleted_rows = ( @@ -248,26 +332,52 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: return True +def _remove_attributes_ids_used_by_other_entities( + session: Session, entities: list[str], attributes_ids: set[int] +) -> set[int]: + """Remove attributes ids that are still in use for entitiy_ids we are not purging yet.""" + if not attributes_ids: + return set() + keep_attributes_ids = { + state.attributes_id + for state in session.query(States.attributes_id) + .filter(States.entity_id.not_in(entities)) + .filter(States.attributes_id.in_(attributes_ids)) + .group_by(States.attributes_id) + } + to_remove = attributes_ids - keep_attributes_ids + _LOGGER.debug( + "Selected %s shared attributes to remove", + len(to_remove), + ) + return to_remove + + def _purge_filtered_states( instance: Recorder, session: Session, excluded_entity_ids: list[str] ) -> None: """Remove filtered states and linked events.""" state_ids: list[int] + attributes_ids: list[int] event_ids: list[int | None] - state_ids, event_ids = zip( + state_ids, attributes_ids, event_ids = zip( *( - session.query(States.state_id, States.event_id) + session.query(States.state_id, States.attributes_id, States.event_id) .filter(States.entity_id.in_(excluded_entity_ids)) .limit(MAX_ROWS_TO_PURGE) .all() ) ) event_ids = [id_ for id_ in event_ids if id_ is not None] + attributes_ids_set = _remove_attributes_ids_used_by_other_entities( + session, excluded_entity_ids, {id_ for id_ in attributes_ids if id_ is not None} + ) _LOGGER.debug( "Selected %s state_ids to remove that should be filtered", len(state_ids) ) _purge_state_ids(instance, session, set(state_ids)) _purge_event_ids(session, event_ids) # type: ignore[arg-type] # type of event_ids already narrowed to 'list[int]' + _purge_attributes_ids(instance, session, attributes_ids_set) def _purge_filtered_events( @@ -290,6 +400,9 @@ def _purge_filtered_events( state_ids: set[int] = {state.state_id for state in states} _purge_state_ids(instance, session, state_ids) _purge_event_ids(session, event_ids) + if EVENT_STATE_CHANGED in excluded_event_types: + session.query(StateAttributes).delete(synchronize_session=False) + instance._state_attributes_ids = {} # pylint: disable=protected-access @retryable_database_job("purge") diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index fb5daa97475..6d73f8ea6e7 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -12,7 +12,7 @@ from typing import Any, Literal, cast import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.models import StateAttributes, States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.sensor import ( PLATFORM_SCHEMA, @@ -482,9 +482,10 @@ class StatisticsSensor(SensorEntity): """ _LOGGER.debug("%s: initializing values from the database", self.entity_id) + states = [] with session_scope(hass=self.hass) as session: - query = session.query(States).filter( + query = session.query(States, StateAttributes).filter( States.entity_id == self._source_entity_id.lower() ) @@ -499,10 +500,18 @@ class StatisticsSensor(SensorEntity): else: _LOGGER.debug("%s: retrieving all records", self.entity_id) + query = query.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) query = query.order_by(States.last_updated.desc()).limit( self._samples_max_buffer_size ) - states = execute(query, to_native=True, validate_entity_ids=False) + if results := execute(query, to_native=False, validate_entity_ids=False): + for state, attributes in results: + native = state.to_native() + if not native.attributes: + native.attributes = attributes.to_native() + states.append(native) if states: for state in reversed(states): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2a9af7ff1d5..d8d88133149 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,11 +13,13 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==35.0.0 +fnvhash==0.1.0 hass-nabucasa==0.54.0 home-assistant-frontend==20220317.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 +lru-dict==1.1.7 paho-mqtt==1.6.1 pillow==9.0.1 pip>=21.0,<22.1 diff --git a/requirements_all.txt b/requirements_all.txt index b9193e46fd7..ba4ec4536e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -651,6 +651,7 @@ flipr-api==1.4.2 flux_led==0.28.27 # homeassistant.components.homekit +# homeassistant.components.recorder fnvhash==0.1.0 # homeassistant.components.foobot @@ -952,6 +953,9 @@ logi_circle==0.2.2 # homeassistant.components.london_underground london-tube-status==0.2 +# homeassistant.components.recorder +lru-dict==1.1.7 + # homeassistant.components.luftdaten luftdaten==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5adb6e80ff7..da1d72eb5e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -443,6 +443,7 @@ flipr-api==1.4.2 flux_led==0.28.27 # homeassistant.components.homekit +# homeassistant.components.recorder fnvhash==0.1.0 # homeassistant.components.foobot @@ -636,6 +637,9 @@ libsoundtouch==0.8 # homeassistant.components.logi_circle logi_circle==0.2.2 +# homeassistant.components.recorder +lru-dict==1.1.7 + # homeassistant.components.luftdaten luftdaten==0.7.2 diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index fab1121542f..29dea015921 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -43,7 +43,11 @@ from tests.common import ( async_init_recorder_component, mock_platform, ) -from tests.components.recorder.common import trigger_db_commit +from tests.components.recorder.common import ( + async_trigger_db_commit, + async_wait_recording_done_without_instance, + trigger_db_commit, +) EMPTY_CONFIG = logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}}) @@ -280,12 +284,14 @@ def create_state_changed_event_from_old_new( "attributes" "state_id", "old_state_id", + "shared_attrs", ], ) row.event_type = EVENT_STATE_CHANGED row.event_data = "{}" row.attributes = attributes_json + row.shared_attrs = attributes_json row.time_fired = event_time_fired row.state = new_state and new_state.get("state") row.entity_id = entity_id @@ -636,6 +642,44 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): assert json_dict[0]["entity_id"] == entity_id_second +async def test_logbook_entity_no_longer_in_state_machine(hass, hass_client): + """Test the logbook view with an entity that hass been removed from the state machine.""" + await async_init_recorder_component(hass) + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await async_wait_recording_done_without_instance(hass) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set( + entity_id_test, STATE_OFF, {ATTR_FRIENDLY_NAME: "Alarm Control Panel"} + ) + hass.states.async_set( + entity_id_test, STATE_ON, {ATTR_FRIENDLY_NAME: "Alarm Control Panel"} + ) + + async_trigger_db_commit(hass) + await async_wait_recording_done_without_instance(hass) + + hass.states.async_remove(entity_id_test) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + assert json_dict[0]["name"] == "Alarm Control Panel" + + async def test_filter_continuous_sensor_values(hass, hass_client): """Test remove continuous sensor events from logbook.""" await async_init_recorder_component(hass) diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 14e0f3668b0..602f368abcd 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -1,9 +1,6 @@ """Unit tests for platform/plant.py.""" from datetime import datetime, timedelta -import pytest - -from homeassistant.components import recorder import homeassistant.components.plant as plant from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -12,12 +9,12 @@ from homeassistant.const import ( STATE_OK, STATE_PROBLEM, STATE_UNAVAILABLE, - STATE_UNKNOWN, ) from homeassistant.core import State from homeassistant.setup import async_setup_component -from tests.common import init_recorder_component +from tests.common import async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance GOOD_DATA = { "moisture": 50, @@ -148,19 +145,13 @@ async def test_state_problem_if_unavailable(hass): assert state.attributes[plant.READING_MOISTURE] == STATE_UNAVAILABLE -@pytest.mark.skipif( - plant.ENABLE_LOAD_HISTORY is False, - reason="tests for loading from DB are unstable, thus" - "this feature is turned of until tests become" - "stable", -) async def test_load_from_db(hass): """Test bootstrapping the brightness history from the database. This test can should only be executed if the loading of the history is enabled via plant.ENABLE_LOAD_HISTORY. """ - init_recorder_component(hass) + await async_init_recorder_component(hass) plant_name = "wise_plant" for value in [20, 30, 10]: @@ -169,7 +160,7 @@ async def test_load_from_db(hass): ) await hass.async_block_till_done() # wait for the recorder to really store the data - hass.data[recorder.DATA_INSTANCE].block_till_done() + await async_wait_recording_done_without_instance(hass) assert await async_setup_component( hass, plant.DOMAIN, {plant.DOMAIN: {plant_name: GOOD_CONFIG}} @@ -177,7 +168,7 @@ async def test_load_from_db(hass): await hass.async_block_till_done() state = hass.states.get(f"plant.{plant_name}") - assert state.state == STATE_UNKNOWN + assert state.state == STATE_PROBLEM max_brightness = state.attributes.get(plant.ATTR_MAX_BRIGHTNESS_HISTORY) assert max_brightness == 30 diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index ab05c6ec05b..ef1ffd1133f 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -31,6 +31,7 @@ from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import ( Events, RecorderRuns, + StateAttributes, States, StatisticsRuns, process_timestamp, @@ -166,10 +167,13 @@ async def test_saving_state( await async_wait_recording_done(hass, instance) with session_scope(hass=hass) as session: - db_states = list(session.query(States)) + db_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + db_states.append(db_state) + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() assert len(db_states) == 1 assert db_states[0].event_id > 0 - state = db_states[0].to_native() assert state == _state_empty_context(hass, entity_id) @@ -400,7 +404,14 @@ def _add_entities(hass, entity_ids): wait_recording_done(hass) with session_scope(hass=hass) as session: - return [st.to_native() for st in session.query(States)] + states = [] + for state, state_attributes in session.query(States, StateAttributes).outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ): + native_state = state.to_native() + native_state.attributes = state_attributes.to_native() + states.append(native_state) + return states def _add_events(hass, events): diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 9f32f1c5746..c3de4161ea1 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -1,5 +1,6 @@ """The tests for the Recorder component.""" -from datetime import datetime +from datetime import datetime, timedelta +from unittest.mock import PropertyMock import pytest from sqlalchemy import create_engine @@ -8,7 +9,9 @@ from sqlalchemy.orm import scoped_session, sessionmaker from homeassistant.components.recorder.models import ( Base, Events, + LazyState, RecorderRuns, + StateAttributes, States, process_timestamp, process_timestamp_to_utc_isoformat, @@ -16,8 +19,7 @@ from homeassistant.components.recorder.models import ( from homeassistant.const import EVENT_STATE_CHANGED import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError -from homeassistant.util import dt -import homeassistant.util.dt as dt_util +from homeassistant.util import dt, dt as dt_util def test_from_event_to_db_event(): @@ -40,6 +42,27 @@ def test_from_event_to_db_state(): assert state == States.from_event(event).to_native() +def test_from_event_to_db_state_attributes(): + """Test converting event to db state attributes.""" + attrs = {"this_attr": True} + state = ha.State("sensor.temperature", "18", attrs) + event = ha.Event( + EVENT_STATE_CHANGED, + {"entity_id": "sensor.temperature", "old_state": None, "new_state": state}, + context=state.context, + ) + assert StateAttributes.from_event(event).to_native() == attrs + + +def test_handling_broken_json_state_attributes(caplog): + """Test we handle broken json in state attributes.""" + state_attributes = StateAttributes( + attributes_id=444, hash=1234, shared_attrs="{NOT_PARSE}" + ) + assert state_attributes.to_native() == {} + assert "Error converting row to state attributes" in caplog.text + + def test_from_event_to_delete_state(): """Test converting deleting state event to db state.""" event = ha.Event( @@ -215,3 +238,97 @@ async def test_event_to_db_model(): native = Events.from_event(event, event_data="{}").to_native() event.data = {} assert native == event + + +async def test_lazy_state_handles_include_json(caplog): + """Test that the LazyState class handles invalid json.""" + row = PropertyMock( + entity_id="sensor.invalid", + shared_attrs="{INVALID_JSON}", + ) + assert LazyState(row).attributes == {} + assert "Error converting row to state attributes" in caplog.text + + +async def test_lazy_state_prefers_shared_attrs_over_attrs(caplog): + """Test that the LazyState prefers shared_attrs over attributes.""" + row = PropertyMock( + entity_id="sensor.invalid", + shared_attrs='{"shared":true}', + attributes='{"shared":false}', + ) + assert LazyState(row).attributes == {"shared": True} + + +async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog): + """Test that the LazyState handles different last_updated and last_changed.""" + now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) + row = PropertyMock( + entity_id="sensor.valid", + state="off", + shared_attrs='{"shared":true}', + last_updated=now, + last_changed=now - timedelta(seconds=60), + ) + lstate = LazyState(row) + assert lstate.as_dict() == { + "attributes": {"shared": True}, + "entity_id": "sensor.valid", + "last_changed": "2021-06-12T03:03:01.000323+00:00", + "last_updated": "2021-06-12T03:04:01.000323+00:00", + "state": "off", + } + assert lstate.last_updated == row.last_updated + assert lstate.last_changed == row.last_changed + assert lstate.as_dict() == { + "attributes": {"shared": True}, + "entity_id": "sensor.valid", + "last_changed": "2021-06-12T03:03:01.000323+00:00", + "last_updated": "2021-06-12T03:04:01.000323+00:00", + "state": "off", + } + + +async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): + """Test that the LazyState handles same last_updated and last_changed.""" + now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) + row = PropertyMock( + entity_id="sensor.valid", + state="off", + shared_attrs='{"shared":true}', + last_updated=now, + last_changed=now, + ) + lstate = LazyState(row) + assert lstate.as_dict() == { + "attributes": {"shared": True}, + "entity_id": "sensor.valid", + "last_changed": "2021-06-12T03:04:01.000323+00:00", + "last_updated": "2021-06-12T03:04:01.000323+00:00", + "state": "off", + } + assert lstate.last_updated == row.last_updated + assert lstate.last_changed == row.last_changed + assert lstate.as_dict() == { + "attributes": {"shared": True}, + "entity_id": "sensor.valid", + "last_changed": "2021-06-12T03:04:01.000323+00:00", + "last_updated": "2021-06-12T03:04:01.000323+00:00", + "state": "off", + } + lstate.last_updated = datetime(2020, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) + assert lstate.as_dict() == { + "attributes": {"shared": True}, + "entity_id": "sensor.valid", + "last_changed": "2021-06-12T03:04:01.000323+00:00", + "last_updated": "2020-06-12T03:04:01.000323+00:00", + "state": "off", + } + lstate.last_changed = datetime(2020, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC) + assert lstate.as_dict() == { + "attributes": {"shared": True}, + "entity_id": "sensor.valid", + "last_changed": "2020-06-12T03:04:01.000323+00:00", + "last_updated": "2020-06-12T03:04:01.000323+00:00", + "state": "off", + } diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 77b5ad3d191..1bdb391541e 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -13,13 +13,14 @@ from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE from homeassistant.components.recorder.models import ( Events, RecorderRuns, + StateAttributes, States, StatisticsRuns, StatisticsShortTerm, ) from homeassistant.components.recorder.purge import purge_old_data from homeassistant.components.recorder.util import session_scope -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -44,9 +45,12 @@ async def test_purge_old_states( # make sure we start with 6 states with session_scope(hass=hass) as session: states = session.query(States) + state_attributes = session.query(StateAttributes) + assert states.count() == 6 assert states[0].old_state_id is None assert states[-1].old_state_id == states[-2].state_id + assert state_attributes.count() == 3 events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 6 @@ -58,6 +62,8 @@ async def test_purge_old_states( finished = purge_old_data(instance, purge_before, repack=False) assert not finished assert states.count() == 2 + assert state_attributes.count() == 1 + assert "test.recorder2" in instance._old_states states_after_purge = session.query(States) @@ -67,6 +73,8 @@ async def test_purge_old_states( finished = purge_old_data(instance, purge_before, repack=False) assert finished assert states.count() == 2 + assert state_attributes.count() == 1 + assert "test.recorder2" in instance._old_states # run purge_old_data again @@ -74,6 +82,8 @@ async def test_purge_old_states( finished = purge_old_data(instance, purge_before, repack=False) assert not finished assert states.count() == 0 + assert state_attributes.count() == 0 + assert "test.recorder2" not in instance._old_states # Add some more states @@ -90,6 +100,9 @@ async def test_purge_old_states( assert events.count() == 6 assert "test.recorder2" in instance._old_states + state_attributes = session.query(StateAttributes) + assert state_attributes.count() == 3 + async def test_purge_old_states_encouters_database_corruption( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT @@ -368,6 +381,14 @@ async def test_purge_edge_case( last_changed=timestamp, last_updated=timestamp, event_id=1001, + attributes_id=1002, + ) + ) + session.add( + StateAttributes( + shared_attrs="{}", + hash=1234, + attributes_id=1002, ) ) @@ -382,6 +403,9 @@ async def test_purge_edge_case( states = session.query(States) assert states.count() == 1 + state_attributes = session.query(StateAttributes) + assert state_attributes.count() == 1 + events = session.query(Events).filter(Events.event_type == "EVENT_TEST_PURGE") assert events.count() == 1 @@ -426,6 +450,14 @@ async def test_purge_cutoff_date( last_changed=timestamp_keep, last_updated=timestamp_keep, event_id=1000, + attributes_id=1000, + ) + ) + session.add( + StateAttributes( + shared_attrs="{}", + hash=1234, + attributes_id=1000, ) ) for row in range(1, rows): @@ -447,6 +479,14 @@ async def test_purge_cutoff_date( last_changed=timestamp_purge, last_updated=timestamp_purge, event_id=1000 + row, + attributes_id=1000 + row, + ) + ) + session.add( + StateAttributes( + shared_attrs="{}", + hash=1234, + attributes_id=1000 + row, ) ) @@ -462,9 +502,18 @@ async def test_purge_cutoff_date( with session_scope(hass=hass) as session: states = session.query(States) + state_attributes = session.query(StateAttributes) events = session.query(Events) assert states.filter(States.state == "purge").count() == rows - 1 assert states.filter(States.state == "keep").count() == 1 + assert ( + state_attributes.outerjoin( + States, StateAttributes.attributes_id == States.attributes_id + ) + .filter(States.state == "keep") + .count() + == 1 + ) assert events.filter(Events.event_type == "PURGE").count() == rows - 1 assert events.filter(Events.event_type == "KEEP").count() == 1 @@ -474,12 +523,47 @@ async def test_purge_cutoff_date( await async_wait_purge_done(hass, instance) states = session.query(States) + state_attributes = session.query(StateAttributes) events = session.query(Events) assert states.filter(States.state == "purge").count() == 0 + assert ( + state_attributes.outerjoin( + States, StateAttributes.attributes_id == States.attributes_id + ) + .filter(States.state == "purge") + .count() + == 0 + ) assert states.filter(States.state == "keep").count() == 1 + assert ( + state_attributes.outerjoin( + States, StateAttributes.attributes_id == States.attributes_id + ) + .filter(States.state == "keep") + .count() + == 1 + ) assert events.filter(Events.event_type == "PURGE").count() == 0 assert events.filter(Events.event_type == "KEEP").count() == 1 + # Make sure we can purge everything + instance.queue.put( + PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False) + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + assert states.count() == 0 + assert state_attributes.count() == 0 + + # Make sure we can purge everything when the db is already empty + instance.queue.put( + PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False) + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + assert states.count() == 0 + assert state_attributes.count() == 0 + async def test_purge_filtered_states( hass: HomeAssistant, @@ -527,6 +611,12 @@ async def test_purge_filtered_states( ) # Add states with linked old_state_ids that need to be handled timestamp = dt_util.utcnow() - timedelta(days=0) + state_attrs = StateAttributes( + hash=0, + shared_attrs=json.dumps( + {"sensor.linked_old_state_id": "sensor.linked_old_state_id"} + ), + ) state_1 = States( entity_id="sensor.linked_old_state_id", domain="sensor", @@ -535,6 +625,7 @@ async def test_purge_filtered_states( last_changed=timestamp, last_updated=timestamp, old_state_id=1, + state_attributes=state_attrs, ) timestamp = dt_util.utcnow() - timedelta(days=4) state_2 = States( @@ -545,6 +636,7 @@ async def test_purge_filtered_states( last_changed=timestamp, last_updated=timestamp, old_state_id=2, + state_attributes=state_attrs, ) state_3 = States( entity_id="sensor.linked_old_state_id", @@ -554,8 +646,9 @@ async def test_purge_filtered_states( last_changed=timestamp, last_updated=timestamp, old_state_id=62, # keep + state_attributes=state_attrs, ) - session.add_all((state_1, state_2, state_3)) + session.add_all((state_attrs, state_1, state_2, state_3)) # Add event that should be keeped session.add( Events( @@ -617,8 +710,154 @@ async def test_purge_filtered_states( assert states_sensor_excluded.count() == 0 assert session.query(States).get(72).old_state_id is None + assert session.query(States).get(72).attributes_id is None assert session.query(States).get(73).old_state_id is None - assert session.query(States).get(74).old_state_id == 62 # should have been kept + assert session.query(States).get(73).attributes_id is None + + final_keep_state = session.query(States).get(74) + assert final_keep_state.old_state_id == 62 # should have been kept + assert final_keep_state.attributes_id == 71 + + assert session.query(StateAttributes).count() == 11 + + # Do it again to make sure nothing changes + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, service_data + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + final_keep_state = session.query(States).get(74) + assert final_keep_state.old_state_id == 62 # should have been kept + assert final_keep_state.attributes_id == 71 + + assert session.query(StateAttributes).count() == 11 + + # Finally make sure we can delete them all except for the ones missing an event_id + service_data = {"keep_days": 0} + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, service_data + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + remaining = list(session.query(States)) + for state in remaining: + assert state.event_id is None + assert len(remaining) == 3 + assert session.query(StateAttributes).count() == 1 + + +async def test_purge_filtered_states_to_empty( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test filtered states are purged all the way to an empty db.""" + config: ConfigType = {"exclude": {"entities": ["sensor.excluded"]}} + instance = await async_setup_recorder_instance(hass, config) + assert instance.entity_filter("sensor.excluded") is False + + def _add_db_entries(hass: HomeAssistant) -> None: + with recorder.session_scope(hass=hass) as session: + # Add states and state_changed events that should be purged + for days in range(1, 4): + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(1000, 1020): + _add_state_and_state_changed_event( + session, + "sensor.excluded", + "purgeme", + timestamp, + event_id * days, + ) + + service_data = {"keep_days": 10} + _add_db_entries(hass) + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) + assert states.count() == 60 + assert state_attributes.count() == 60 + + # Test with 'apply_filter' = True + service_data["apply_filter"] = True + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, service_data + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + assert states.count() == 0 + assert state_attributes.count() == 0 + + # Do it again to make sure nothing changes + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, service_data + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + + +async def test_purge_without_state_attributes_filtered_states_to_empty( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test filtered legacy states without state attributes are purged all the way to an empty db.""" + config: ConfigType = {"exclude": {"entities": ["sensor.old_format"]}} + instance = await async_setup_recorder_instance(hass, config) + assert instance.entity_filter("sensor.old_format") is False + + def _add_db_entries(hass: HomeAssistant) -> None: + with recorder.session_scope(hass=hass) as session: + # Add states and state_changed events that should be purged + # in the legacy format + timestamp = dt_util.utcnow() - timedelta(days=5) + event_id = 1021 + session.add( + States( + entity_id="sensor.old_format", + domain="sensor", + state=STATE_ON, + attributes=json.dumps({"old": "not_using_state_attributes"}), + last_changed=timestamp, + last_updated=timestamp, + event_id=event_id, + state_attributes=None, + ) + ) + session.add( + Events( + event_id=event_id, + event_type=EVENT_STATE_CHANGED, + event_data="{}", + origin="LOCAL", + time_fired=timestamp, + ) + ) + + service_data = {"keep_days": 10} + _add_db_entries(hass) + + with session_scope(hass=hass) as session: + states = session.query(States) + state_attributes = session.query(StateAttributes) + assert states.count() == 1 + assert state_attributes.count() == 0 + + # Test with 'apply_filter' = True + service_data["apply_filter"] = True + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, service_data + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + assert states.count() == 0 + assert state_attributes.count() == 0 + + # Do it again to make sure nothing changes + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE, service_data + ) + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) async def test_purge_filtered_events( @@ -923,7 +1162,7 @@ async def _add_test_states(hass: HomeAssistant, instance: recorder.Recorder): utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) eleven_days_ago = utcnow - timedelta(days=11) - attributes = {"test_attr": 5, "test_attr_10": "nice"} + base_attributes = {"test_attr": 5, "test_attr_10": "nice"} async def set_state(entity_id, state, **kwargs): """Set the state.""" @@ -935,12 +1174,15 @@ async def _add_test_states(hass: HomeAssistant, instance: recorder.Recorder): if event_id < 2: timestamp = eleven_days_ago state = f"autopurgeme_{event_id}" + attributes = {"autopurgeme": True, **base_attributes} elif event_id < 4: timestamp = five_days_ago state = f"purgeme_{event_id}" + attributes = {"purgeme": True, **base_attributes} else: timestamp = utcnow state = f"dontpurgeme_{event_id}" + attributes = {"dontpurgeme": True, **base_attributes} with patch( "homeassistant.components.recorder.dt_util.utcnow", return_value=timestamp @@ -1069,15 +1311,20 @@ def _add_state_and_state_changed_event( event_id: int, ) -> None: """Add state and state_changed event to database for testing.""" + state_attrs = StateAttributes( + hash=event_id, shared_attrs=json.dumps({entity_id: entity_id}) + ) + session.add(state_attrs) session.add( States( entity_id=entity_id, domain="sensor", state=state, - attributes="{}", + attributes=None, last_changed=timestamp, last_updated=timestamp, event_id=event_id, + state_attributes=state_attrs, ) ) session.add( From b284eddc631ed484c8ef1739fcd85a690c301729 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 00:49:11 -1000 Subject: [PATCH 0519/1054] Fix statistics doing I/O in the event loop (#68315) --- homeassistant/components/statistics/sensor.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 6d73f8ea6e7..d632e9710cf 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -12,6 +12,7 @@ from typing import Any, Literal, cast import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import StateAttributes, States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.sensor import ( @@ -470,17 +471,8 @@ class StatisticsSensor(SensorEntity): self.hass, _scheduled_update, next_to_purge_timestamp ) - async def _initialize_from_database(self) -> None: - """Initialize the list of states from the database. - - The query will get the list of states in DESCENDING order so that we - can limit the result to self._sample_size. Afterwards reverse the - list so that we get it in the right order again. - - If MaxAge is provided then query will restrict to entries younger then - current datetime - MaxAge. - """ - + def _fetch_states_from_database(self) -> list[State]: + """Fetch the states from the database.""" _LOGGER.debug("%s: initializing values from the database", self.entity_id) states = [] @@ -512,8 +504,21 @@ class StatisticsSensor(SensorEntity): if not native.attributes: native.attributes = attributes.to_native() states.append(native) + return states - if states: + async def _initialize_from_database(self) -> None: + """Initialize the list of states from the database. + + The query will get the list of states in DESCENDING order so that we + can limit the result to self._sample_size. Afterwards reverse the + list so that we get it in the right order again. + + If MaxAge is provided then query will restrict to entries younger then + current datetime - MaxAge. + """ + if states := await get_instance(self.hass).async_add_executor_job( + self._fetch_states_from_database + ): for state in reversed(states): self._add_state_to_queue(state) From 5bb271c9fb7a33b0c7b21869d0fbd2ce9b7cff58 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Mar 2022 12:14:38 +0100 Subject: [PATCH 0520/1054] Add config flow for derivative sensor (#68268) --- .../components/derivative/__init__.py | 24 ++- .../components/derivative/config_flow.py | 93 +++++++++++ homeassistant/components/derivative/const.py | 9 ++ .../components/derivative/manifest.json | 7 +- homeassistant/components/derivative/sensor.py | 65 ++++++-- .../components/derivative/strings.json | 33 ++++ .../derivative/translations/en.json | 33 ++++ homeassistant/generated/config_flows.py | 1 + .../components/derivative/test_config_flow.py | 149 ++++++++++++++++++ tests/components/derivative/test_init.py | 61 +++++++ 10 files changed, 458 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/derivative/config_flow.py create mode 100644 homeassistant/components/derivative/const.py create mode 100644 homeassistant/components/derivative/strings.json create mode 100644 homeassistant/components/derivative/translations/en.json create mode 100644 tests/components/derivative/test_config_flow.py create mode 100644 tests/components/derivative/test_init.py diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index afee8d5d175..e3fe9d85f41 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -1 +1,23 @@ -"""The derivative component.""" +"""The Derivative integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Derivative from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py new file mode 100644 index 00000000000..ccb44b1963b --- /dev/null +++ b/homeassistant/components/derivative/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for Derivative integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, + CONF_SOURCE, + CONF_UNIT_OF_MEASUREMENT, + TIME_DAYS, + TIME_HOURS, + TIME_MINUTES, + TIME_SECONDS, +) +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowStep, +) + +from .const import ( + CONF_ROUND_DIGITS, + CONF_TIME_WINDOW, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, + DOMAIN, +) + +UNIT_PREFIXES = [ + {"value": "none", "label": "none"}, + {"value": "n", "label": "n (nano)"}, + {"value": "µ", "label": "µ (micro)"}, + {"value": "m", "label": "m (milli)"}, + {"value": "k", "label": "k (kilo)"}, + {"value": "M", "label": "M (mega)"}, + {"value": "G", "label": "T (tera)"}, + {"value": "T", "label": "P (peta)"}, +] +TIME_UNITS = [ + {"value": TIME_SECONDS, "label": "s (seconds)"}, + {"value": TIME_MINUTES, "label": "min (minutes)"}, + {"value": TIME_HOURS, "label": "h (hours)"}, + {"value": TIME_DAYS, "label": "d (days)"}, +] + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + { + "number": { + "min": 0, + "max": 6, + "mode": "box", + CONF_UNIT_OF_MEASUREMENT: "decimals", + } + } + ), + vol.Required(CONF_TIME_WINDOW): selector.selector({"duration": {}}), + vol.Required(CONF_UNIT_PREFIX, default="none"): selector.selector( + {"select": {"options": UNIT_PREFIXES}} + ), + vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector( + {"select": {"options": TIME_UNITS}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_SOURCE): selector.selector( + {"entity": {"domain": "sensor"}}, + ), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} + +OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} + + +class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Derivative.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options[CONF_NAME]) diff --git a/homeassistant/components/derivative/const.py b/homeassistant/components/derivative/const.py new file mode 100644 index 00000000000..32f2777dc80 --- /dev/null +++ b/homeassistant/components/derivative/const.py @@ -0,0 +1,9 @@ +"""Constants for the Derivative integration.""" + +DOMAIN = "derivative" + +CONF_ROUND_DIGITS = "round" +CONF_TIME_WINDOW = "time_window" +CONF_UNIT = "unit" +CONF_UNIT_PREFIX = "unit_prefix" +CONF_UNIT_TIME = "unit_time" diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json index 2b86c07cfe4..bed23d33e15 100644 --- a/homeassistant/components/derivative/manifest.json +++ b/homeassistant/components/derivative/manifest.json @@ -2,6 +2,9 @@ "domain": "derivative", "name": "Derivative", "documentation": "https://www.home-assistant.io/integrations/derivative", - "codeowners": ["@afaucogney"], - "iot_class": "calculated" + "codeowners": [ + "@afaucogney" + ], + "iot_class": "calculated", + "config_flow": true } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 9be01f27e4d..25bc3450dcb 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -20,24 +21,26 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + CONF_ROUND_DIGITS, + CONF_TIME_WINDOW, + CONF_UNIT, + CONF_UNIT_PREFIX, + CONF_UNIT_TIME, +) + # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" -CONF_ROUND_DIGITS = "round" -CONF_UNIT_PREFIX = "unit_prefix" -CONF_UNIT_TIME = "unit_time" -CONF_UNIT = "unit" -CONF_TIME_WINDOW = "time_window" - # SI Metric prefixes UNIT_PREFIXES = { None: 1, @@ -76,6 +79,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Derivative config entry.""" + registry = er.async_get(hass) + # Validate + resolve entity registry id to entity_id + source_entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_SOURCE] + ) + + unit_prefix = config_entry.options[CONF_UNIT_PREFIX] + if unit_prefix == "none": + unit_prefix = None + + derivative_sensor = DerivativeSensor( + name=config_entry.title, + round_digits=int(config_entry.options[CONF_ROUND_DIGITS]), + source_entity=source_entity_id, + time_window=cv.time_period_dict(config_entry.options[CONF_TIME_WINDOW]), + unique_id=config_entry.entry_id, + unit_of_measurement=None, + unit_prefix=unit_prefix, + unit_time=config_entry.options[CONF_UNIT_TIME], + ) + + async_add_entities([derivative_sensor]) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -84,13 +117,14 @@ async def async_setup_platform( ) -> None: """Set up the derivative sensor.""" derivative = DerivativeSensor( - source_entity=config[CONF_SOURCE], name=config.get(CONF_NAME), round_digits=config[CONF_ROUND_DIGITS], + source_entity=config[CONF_SOURCE], + time_window=config[CONF_TIME_WINDOW], + unit_of_measurement=config.get(CONF_UNIT), unit_prefix=config[CONF_UNIT_PREFIX], unit_time=config[CONF_UNIT_TIME], - unit_of_measurement=config.get(CONF_UNIT), - time_window=config[CONF_TIME_WINDOW], + unique_id=None, ) async_add_entities([derivative]) @@ -101,15 +135,18 @@ class DerivativeSensor(RestoreEntity, SensorEntity): def __init__( self, - source_entity, + *, name, round_digits, + source_entity, + time_window, + unit_of_measurement, unit_prefix, unit_time, - unit_of_measurement, - time_window, + unique_id, ): """Initialize the derivative sensor.""" + self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 @@ -214,7 +251,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): self.async_write_ha_state() async_track_state_change_event( - self.hass, [self._sensor_source_id], calc_derivative + self.hass, self._sensor_source_id, calc_derivative ) @property diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json new file mode 100644 index 00000000000..c21a486d039 --- /dev/null +++ b/homeassistant/components/derivative/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "title": "New Derivative sensor", + "description": "Precision controls the number of decimal digits in the output.\nIf the time window is not 0, the sensor's value is a time weighted moving average of derivatives within the window.\nThe derivative will be scaled according to the selected metric prefix and time unit of the derivative.", + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + } + } + } + }, + "options": { + "step": { + "options": { + "description": "[%key:component::derivative::config::step::user::description%]", + "data": { + "name": "[%key:component::derivative::config::step::user::data::name%]", + "round": "[%key:component::derivative::config::step::user::data::round%]", + "source": "[%key:component::derivative::config::step::user::data::source%]", + "time_window": "[%key:component::derivative::config::step::user::data::time_window%]", + "unit_prefix": "[%key:component::derivative::config::step::user::data::unit_prefix%]", + "unit_time": "[%key:component::derivative::config::step::user::data::unit_time%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json new file mode 100644 index 00000000000..b1fa702fe3c --- /dev/null +++ b/homeassistant/components/derivative/translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "description": "Precision controls the number of decimal digits in the output.\nIf the time window is not 0, the sensor's value is a time weighted moving average of derivatives within the window.\nThe derivative will be scaled according to the selected metric prefix and time unit of the derivative.", + "title": "New Derivative sensor" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "description": "Precision controls the number of decimal digits in the output.\nIf the time window is not 0, the sensor's value is a time weighted moving average of derivatives within the window.\nThe derivative will be scaled according to the selected metric prefix and time unit of the derivative." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a6591cc693c..89cd4495326 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -67,6 +67,7 @@ FLOWS = [ "daikin", "deconz", "denonavr", + "derivative", "devolo_home_control", "devolo_home_network", "dexcom", diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py new file mode 100644 index 00000000000..61ab7251f8a --- /dev/null +++ b/tests/components/derivative/test_config_flow.py @@ -0,0 +1,149 @@ +"""Test the Derivative config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.derivative.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensor_entity_id = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.derivative.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "My derivative", + "round": 1, + "source": input_sensor_entity_id, + "time_window": {"seconds": 0}, + "unit_prefix": "none", + "unit_time": "min", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My derivative" + assert result["data"] == {} + assert result["options"] == { + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "none", + "unit_time": "min", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "none", + "unit_time": "min", + } + assert config_entry.title == "My derivative" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My derivative", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "round") == 1.0 + assert get_suggested(schema, "time_window") == {"seconds": 0.0} + assert get_suggested(schema, "unit_prefix") == "k" + assert get_suggested(schema, "unit_time") == "min" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "round": 2.0, + "time_window": {"seconds": 10.0}, + "unit_prefix": "none", + "unit_time": "h", + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "name": "My derivative", + "round": 2.0, + "source": "sensor.input", + "time_window": {"seconds": 10.0}, + "unit_prefix": "none", + "unit_time": "h", + } + assert config_entry.data == {} + assert config_entry.options == { + "name": "My derivative", + "round": 2.0, + "source": "sensor.input", + "time_window": {"seconds": 10.0}, + "unit_prefix": "none", + "unit_time": "h", + } + assert config_entry.title == "My derivative" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + hass.states.async_set("sensor.input", 10, {"unit_of_measurement": "cat"}) + hass.states.async_set("sensor.input", 11, {"unit_of_measurement": "cat"}) + await hass.async_block_till_done() + state = hass.states.get(f"{platform}.my_derivative") + assert state.attributes["unit_of_measurement"] == "cat/h" diff --git a/tests/components/derivative/test_init.py b/tests/components/derivative/test_init.py new file mode 100644 index 00000000000..fef13109007 --- /dev/null +++ b/tests/components/derivative/test_init.py @@ -0,0 +1,61 @@ +"""Test the Derivative integration.""" +import pytest + +from homeassistant.components.derivative.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + input_sensor_entity_id = "sensor.input" + registry = er.async_get(hass) + derivative_entity_id = f"{platform}.my_derivative" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "name": "My derivative", + "round": 1.0, + "source": "sensor.input", + "time_window": {"seconds": 0.0}, + "unit_prefix": "k", + "unit_time": "min", + }, + title="My derivative", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(derivative_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(derivative_entity_id) + assert state.state == "0" + assert "unit_of_measurement" not in state.attributes + assert state.attributes["source"] == "sensor.input" + + hass.states.async_set(input_sensor_entity_id, 10, {"unit_of_measurement": "dog"}) + hass.states.async_set(input_sensor_entity_id, 11, {"unit_of_measurement": "dog"}) + await hass.async_block_till_done() + state = hass.states.get(derivative_entity_id) + assert state.state != "0" + assert state.attributes["unit_of_measurement"] == "kdog/min" + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(derivative_entity_id) is None + assert registry.async_get(derivative_entity_id) is None From 35261a908971eafbf138087bdd03c3285d859944 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 18 Mar 2022 12:18:19 +0100 Subject: [PATCH 0521/1054] Add switch platform to UptimeRobot (#65394) * Add switch platfor mto UptimeRobot * Add tests * Apply review comment * review comments part 2 * review comments part 3 * Fix tests after swapping logic on/off * Fix reauth test * Check for read-only key * Fix reauth for switch platform * mypy * cleanup * cleanup part 2 * Fixes + review comments * Tests * Apply more review comments * Required changes * fix test * Remove if * 100% tests coverage * Check readonly key in config_flow * Fix strings & translation * Add guard for 'monitor' keys * allign tests * Wrong API key message reworded --- .../components/uptimerobot/__init__.py | 10 +- .../components/uptimerobot/binary_sensor.py | 22 ++- .../components/uptimerobot/config_flow.py | 9 +- homeassistant/components/uptimerobot/const.py | 2 +- .../components/uptimerobot/entity.py | 10 +- .../components/uptimerobot/sensor.py | 24 ++- .../components/uptimerobot/strings.json | 5 +- .../components/uptimerobot/switch.py | 75 ++++++++ .../uptimerobot/translations/en.json | 5 +- tests/components/uptimerobot/common.py | 19 ++- .../uptimerobot/test_config_flow.py | 24 +++ tests/components/uptimerobot/test_init.py | 34 ++++ tests/components/uptimerobot/test_switch.py | 160 ++++++++++++++++++ 13 files changed, 357 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/uptimerobot/switch.py create mode 100644 tests/components/uptimerobot/test_switch.py diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 06da8a1b4b1..6d9be1b2364 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -26,9 +26,12 @@ from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLA async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UptimeRobot from a config entry.""" hass.data.setdefault(DOMAIN, {}) - uptime_robot_api = UptimeRobot( - entry.data[CONF_API_KEY], async_get_clientsession(hass) - ) + key: str = entry.data[CONF_API_KEY] + if key.startswith("ur") or key.startswith("m"): + raise ConfigEntryAuthFailed( + "Wrong API key type detected, use the 'main' API key" + ) + uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass)) dev_reg = await async_get_registry(hass) hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( @@ -58,6 +61,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): """Data update coordinator for UptimeRobot.""" data: list[UptimeRobotMonitor] + config_entry: ConfigEntry def __init__( self, diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 40f850c5376..248212a8345 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -23,18 +23,16 @@ async def async_setup_entry( """Set up the UptimeRobot binary_sensors.""" coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - UptimeRobotBinarySensor( - coordinator, - BinarySensorEntityDescription( - key=str(monitor.id), - name=monitor.friendly_name, - device_class=BinarySensorDeviceClass.CONNECTIVITY, - ), - monitor=monitor, - ) - for monitor in coordinator.data - ], + UptimeRobotBinarySensor( + coordinator, + BinarySensorEntityDescription( + key=str(monitor.id), + name=monitor.friendly_name, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + monitor=monitor, + ) + for monitor in coordinator.data ) diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 3f08e7e692e..5b6ac1d4880 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -34,9 +34,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate the user input allows us to connect.""" errors: dict[str, str] = {} response: UptimeRobotApiResponse | UptimeRobotApiError | None = None - uptime_robot_api = UptimeRobot( - data[CONF_API_KEY], async_get_clientsession(self.hass) - ) + key: str = data[CONF_API_KEY] + if key.startswith("ur") or key.startswith("m"): + LOGGER.error("Wrong API key type detected, use the 'main' API key") + errors["base"] = "not_main_key" + return errors, None + uptime_robot_api = UptimeRobot(key, async_get_clientsession(self.hass)) try: response = await uptime_robot_api.async_get_account_details() diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index e98ae7b514c..e89c1c38e0e 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -13,7 +13,7 @@ LOGGER: Logger = getLogger(__package__) COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" -PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] ATTRIBUTION: Final = "Data provided by UptimeRobot" diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 318e5a5094e..6f7c616b7a4 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -5,11 +5,9 @@ from pyuptimerobot import UptimeRobotMonitor from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import UptimeRobotDataUpdateCoordinator from .const import ATTR_TARGET, ATTRIBUTION, DOMAIN @@ -17,10 +15,11 @@ class UptimeRobotEntity(CoordinatorEntity): """Base UptimeRobot entity.""" _attr_attribution = ATTRIBUTION + coordinator: UptimeRobotDataUpdateCoordinator def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: UptimeRobotDataUpdateCoordinator, description: EntityDescription, monitor: UptimeRobotMonitor, ) -> None: @@ -40,6 +39,7 @@ class UptimeRobotEntity(CoordinatorEntity): ATTR_TARGET: self.monitor.url, } self._attr_unique_id = str(self.monitor.id) + self.api = coordinator.api @property def _monitors(self) -> list[UptimeRobotMonitor]: diff --git a/homeassistant/components/uptimerobot/sensor.py b/homeassistant/components/uptimerobot/sensor.py index 77f32e20b4b..0e450bf24b7 100644 --- a/homeassistant/components/uptimerobot/sensor.py +++ b/homeassistant/components/uptimerobot/sensor.py @@ -38,19 +38,17 @@ async def async_setup_entry( """Set up the UptimeRobot sensors.""" coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - UptimeRobotSensor( - coordinator, - SensorEntityDescription( - key=str(monitor.id), - name=monitor.friendly_name, - entity_category=EntityCategory.DIAGNOSTIC, - device_class="uptimerobot__monitor_status", - ), - monitor=monitor, - ) - for monitor in coordinator.data - ], + UptimeRobotSensor( + coordinator, + SensorEntityDescription( + key=str(monitor.id), + name=monitor.friendly_name, + entity_category=EntityCategory.DIAGNOSTIC, + device_class="uptimerobot__monitor_status", + ), + monitor=monitor, + ) + for monitor in coordinator.data ) diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index 2946f2e2d5d..5ead21a50cd 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -2,14 +2,14 @@ "config": { "step": { "user": { - "description": "You need to supply a read-only API key from UptimeRobot", + "description": "You need to supply the 'main' API key from UptimeRobot", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "You need to supply a new read-only API key from UptimeRobot", + "description": "You need to supply a new 'main' API key from UptimeRobot", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } @@ -19,6 +19,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", "unknown": "[%key:common::config_flow::error::unknown%]", + "not_main_key": "Wrong API key type detected, use the 'main' API key", "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration." }, "abort": { diff --git a/homeassistant/components/uptimerobot/switch.py b/homeassistant/components/uptimerobot/switch.py new file mode 100644 index 00000000000..619f72ae47f --- /dev/null +++ b/homeassistant/components/uptimerobot/switch.py @@ -0,0 +1,75 @@ +"""UptimeRobot switch platform.""" +from __future__ import annotations + +from typing import Any + +from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import UptimeRobotDataUpdateCoordinator +from .const import API_ATTR_OK, DOMAIN, LOGGER +from .entity import UptimeRobotEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the UptimeRobot switches.""" + coordinator: UptimeRobotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + UptimeRobotSwitch( + coordinator, + SwitchEntityDescription( + key=str(monitor.id), + name=f"{monitor.friendly_name} Active", + device_class=SwitchDeviceClass.SWITCH, + ), + monitor=monitor, + ) + for monitor in coordinator.data + ) + + +class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity): + """Representation of a UptimeRobot switch.""" + + _attr_icon = "mdi:cog" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return bool(self.monitor.status != 0) + + async def _async_edit_monitor(self, **kwargs: Any) -> None: + """Edit monitor status.""" + try: + response = await self.api.async_edit_monitor(**kwargs) + except UptimeRobotAuthenticationException: + LOGGER.debug("API authentication error, calling reauth") + self.coordinator.config_entry.async_start_reauth(self.hass) + return + except UptimeRobotException as exception: + LOGGER.error("API exception: %s", exception) + return + + if response.status != API_ATTR_OK: + LOGGER.error("API exception: %s", response.error.message, exc_info=True) + return + + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn on switch.""" + await self._async_edit_monitor(id=self.monitor.id, status=0) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn off switch.""" + await self._async_edit_monitor(id=self.monitor.id, status=1) diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index a78af34102d..f4fe398a195 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", + "not_main_key": "Wrong API key type detected, use the 'main' API key", "reauth_failed_matching_account": "The API key you provided does not match the account ID for existing configuration.", "unknown": "Unexpected error" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API Key" }, - "description": "You need to supply a new read-only API key from UptimeRobot", + "description": "You need to supply a new 'main' API key from UptimeRobot", "title": "Reauthenticate Integration" }, "user": { "data": { "api_key": "API Key" }, - "description": "You need to supply a read-only API key from UptimeRobot" + "description": "You need to supply the 'main' API key from UptimeRobot" } } } diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index 6ec0e7aef7d..2003f411358 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -20,7 +20,8 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -MOCK_UPTIMEROBOT_API_KEY = "0242ac120003" +MOCK_UPTIMEROBOT_API_KEY = "u0242ac120003" +MOCK_UPTIMEROBOT_API_KEY_READ_ONLY = "ur0242ac120003" MOCK_UPTIMEROBOT_EMAIL = "test@test.test" MOCK_UPTIMEROBOT_UNIQUE_ID = "1234567890" @@ -37,6 +38,14 @@ MOCK_UPTIMEROBOT_MONITOR = { "type": 1, "url": "http://example.com", } +MOCK_UPTIMEROBOT_MONITOR_PAUSED = { + "id": 1234, + "friendly_name": "Test monitor", + "status": 0, + "type": 1, + "url": "http://example.com", +} + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { "domain": DOMAIN, @@ -45,11 +54,19 @@ MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA = { "unique_id": MOCK_UPTIMEROBOT_UNIQUE_ID, "source": config_entries.SOURCE_USER, } +MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA_KEY_READ_ONLY = { + "domain": DOMAIN, + "title": MOCK_UPTIMEROBOT_EMAIL, + "data": {"platform": DOMAIN, "api_key": MOCK_UPTIMEROBOT_API_KEY_READ_ONLY}, + "unique_id": MOCK_UPTIMEROBOT_UNIQUE_ID, + "source": config_entries.SOURCE_USER, +} STATE_UP = "up" UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY = "binary_sensor.test_monitor" UPTIMEROBOT_SENSOR_TEST_ENTITY = "sensor.test_monitor" +UPTIMEROBOT_SWITCH_TEST_ENTITY = "switch.test_monitor_active" class MockApiResponseKey(str, Enum): diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 7daa59df111..c477e1bbc65 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.data_entry_flow import ( from .common import ( MOCK_UPTIMEROBOT_ACCOUNT, MOCK_UPTIMEROBOT_API_KEY, + MOCK_UPTIMEROBOT_API_KEY_READ_ONLY, MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, MOCK_UPTIMEROBOT_UNIQUE_ID, MockApiResponseKey, @@ -56,6 +57,29 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_read_only(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "pyuptimerobot.UptimeRobot.async_get_account_details", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY_READ_ONLY}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"]["base"] == "not_main_key" + + @pytest.mark.parametrize( "exception,error_key", [ diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 94dd7e8af4c..8efa51e05a6 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -19,6 +19,7 @@ from homeassistant.util import dt from .common import ( MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA_KEY_READ_ONLY, MOCK_UPTIMEROBOT_MONITOR, UPTIMEROBOT_BINARY_SENSOR_TEST_ENTITY, MockApiResponseKey, @@ -62,6 +63,39 @@ async def test_reauthentication_trigger_in_setup( ) +async def test_reauthentication_trigger_key_read_only( + hass: HomeAssistant, caplog: LogCaptureFixture +): + """Test reauthentication trigger.""" + mock_config_entry = MockConfigEntry( + **MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA_KEY_READ_ONLY + ) + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + + assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_ERROR + assert ( + mock_config_entry.reason + == "Wrong API key type detected, use the 'main' API key" + ) + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH + assert flow["context"]["entry_id"] == mock_config_entry.entry_id + + assert ( + "Config entry 'test@test.test' for uptimerobot integration could not authenticate" + in caplog.text + ) + + async def test_reauthentication_trigger_after_setup( hass: HomeAssistant, caplog: LogCaptureFixture ): diff --git a/tests/components/uptimerobot/test_switch.py b/tests/components/uptimerobot/test_switch.py new file mode 100644 index 00000000000..82ea06ce836 --- /dev/null +++ b/tests/components/uptimerobot/test_switch.py @@ -0,0 +1,160 @@ +"""Test UptimeRobot switch.""" + +from unittest.mock import patch + +from pyuptimerobot import UptimeRobotAuthenticationException + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant + +from .common import ( + MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, + MOCK_UPTIMEROBOT_MONITOR, + MOCK_UPTIMEROBOT_MONITOR_PAUSED, + UPTIMEROBOT_SWITCH_TEST_ENTITY, + MockApiResponseKey, + mock_uptimerobot_api_response, + setup_uptimerobot_integration, +) + +from tests.common import MockConfigEntry + + +async def test_presentation(hass: HomeAssistant) -> None: + """Test the presenstation of UptimeRobot sensors.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + + assert entity.state == STATE_ON + assert entity.attributes["icon"] == "mdi:cog" + assert entity.attributes["target"] == MOCK_UPTIMEROBOT_MONITOR["url"] + + +async def test_switch_off(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + + mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response( + data=[MOCK_UPTIMEROBOT_MONITOR_PAUSED] + ), + ), patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_OFF + + +async def test_switch_on(hass: HomeAssistant) -> None: + """Test entity unaviable on update failure.""" + + mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "pyuptimerobot.UptimeRobot.async_get_monitors", + return_value=mock_uptimerobot_api_response(data=[MOCK_UPTIMEROBOT_MONITOR]), + ), patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(), + ): + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + +async def test_authentication_error(hass: HomeAssistant, caplog) -> None: + """Test authentication error turning switch on/off.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + side_effect=UptimeRobotAuthenticationException, + ), patch( + "homeassistant.config_entries.ConfigEntry.async_start_reauth" + ) as config_entry_reauth: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + assert config_entry_reauth.assert_called + + +async def test_refresh_data(hass: HomeAssistant, caplog) -> None: + """Test authentication error turning switch on/off.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "homeassistant.helpers.update_coordinator.DataUpdateCoordinator.async_request_refresh" + ) as coordinator_refresh: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + assert coordinator_refresh.assert_called + + +async def test_switch_api_failure(hass: HomeAssistant, caplog) -> None: + """Test general exception turning switch on/off.""" + await setup_uptimerobot_integration(hass) + + entity = hass.states.get(UPTIMEROBOT_SWITCH_TEST_ENTITY) + assert entity.state == STATE_ON + + with patch( + "pyuptimerobot.UptimeRobot.async_edit_monitor", + return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ERROR), + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: UPTIMEROBOT_SWITCH_TEST_ENTITY}, + blocking=True, + ) + + assert "API exception" in caplog.text From 2686be921c88232bd8a2971e779bff0f56411601 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Mar 2022 13:09:10 +0100 Subject: [PATCH 0522/1054] Remove deprecated (old) Z-Wave integration (#67221) * Remove deprecated (old) Z-Wave integration * Mark migration tests as skip, for later cleanup --- .coveragerc | 1 - .github/workflows/wheels.yml | 1 - CODEOWNERS | 2 - Dockerfile | 3 +- homeassistant/components/config/__init__.py | 16 +- homeassistant/components/config/zwave.py | 259 --- homeassistant/components/zwave/__init__.py | 1355 ----------- .../components/zwave/binary_sensor.py | 106 - homeassistant/components/zwave/climate.py | 619 ------ homeassistant/components/zwave/config_flow.py | 95 - homeassistant/components/zwave/const.py | 395 ---- homeassistant/components/zwave/cover.py | 216 -- .../components/zwave/discovery_schemas.py | 416 ---- homeassistant/components/zwave/fan.py | 86 - homeassistant/components/zwave/light.py | 407 ---- homeassistant/components/zwave/lock.py | 390 ---- homeassistant/components/zwave/manifest.json | 9 - homeassistant/components/zwave/migration.py | 167 -- homeassistant/components/zwave/node_entity.py | 381 ---- homeassistant/components/zwave/sensor.py | 124 -- homeassistant/components/zwave/services.yaml | 411 ---- homeassistant/components/zwave/strings.json | 32 - homeassistant/components/zwave/switch.py | 64 - .../components/zwave/translations/af.json | 14 - .../components/zwave/translations/ar.json | 14 - .../components/zwave/translations/bg.json | 31 - .../components/zwave/translations/bs.json | 14 - .../components/zwave/translations/ca.json | 32 - .../components/zwave/translations/cs.json | 32 - .../components/zwave/translations/cy.json | 14 - .../components/zwave/translations/da.json | 31 - .../components/zwave/translations/de.json | 32 - .../components/zwave/translations/el.json | 32 - .../components/zwave/translations/en.json | 32 - .../components/zwave/translations/es-419.json | 31 - .../components/zwave/translations/es.json | 32 - .../components/zwave/translations/et.json | 32 - .../components/zwave/translations/eu.json | 14 - .../components/zwave/translations/fa.json | 14 - .../components/zwave/translations/fi.json | 23 - .../components/zwave/translations/fr.json | 32 - .../components/zwave/translations/gsw.json | 14 - .../components/zwave/translations/he.json | 27 - .../components/zwave/translations/hi.json | 14 - .../components/zwave/translations/hr.json | 14 - .../components/zwave/translations/hu.json | 32 - .../components/zwave/translations/hy.json | 14 - .../components/zwave/translations/id.json | 32 - .../components/zwave/translations/is.json | 14 - .../components/zwave/translations/it.json | 32 - .../components/zwave/translations/ja.json | 32 - .../components/zwave/translations/ko.json | 32 - .../components/zwave/translations/lb.json | 32 - .../components/zwave/translations/lt.json | 8 - .../components/zwave/translations/lv.json | 14 - .../components/zwave/translations/nb.json | 14 - .../components/zwave/translations/nl.json | 32 - .../components/zwave/translations/nn.json | 21 - .../components/zwave/translations/no.json | 32 - .../components/zwave/translations/pl.json | 32 - .../components/zwave/translations/pt-BR.json | 32 - .../components/zwave/translations/pt.json | 32 - .../components/zwave/translations/ro.json | 31 - .../components/zwave/translations/ru.json | 32 - .../components/zwave/translations/sk.json | 19 - .../components/zwave/translations/sl.json | 31 - .../zwave/translations/sr-Latn.json | 8 - .../components/zwave/translations/sr.json | 11 - .../components/zwave/translations/sv.json | 31 - .../components/zwave/translations/ta.json | 14 - .../components/zwave/translations/te.json | 14 - .../components/zwave/translations/th.json | 14 - .../components/zwave/translations/tr.json | 32 - .../components/zwave/translations/uk.json | 32 - .../components/zwave/translations/vi.json | 14 - .../zwave/translations/zh-Hans.json | 31 - .../zwave/translations/zh-Hant.json | 32 - homeassistant/components/zwave/util.py | 129 -- .../components/zwave/websocket_api.py | 90 - homeassistant/components/zwave/workaround.py | 170 -- homeassistant/generated/config_flows.py | 1 - mypy.ini | 9 - requirements_all.txt | 6 - requirements_test_all.txt | 6 - script/gen_requirements_all.py | 1 - script/hassfest/mypy_config.py | 3 - script/translations/migrate.py | 2 +- tests/components/config/test_init.py | 43 +- tests/components/config/test_zwave.py | 542 ----- tests/components/zwave/__init__.py | 1 - tests/components/zwave/conftest.py | 80 - tests/components/zwave/test_binary_sensor.py | 103 - tests/components/zwave/test_climate.py | 893 -------- tests/components/zwave/test_cover.py | 292 --- tests/components/zwave/test_fan.py | 91 - tests/components/zwave/test_init.py | 1977 ----------------- tests/components/zwave/test_light.py | 481 ---- tests/components/zwave/test_lock.py | 389 ---- tests/components/zwave/test_node_entity.py | 723 ------ tests/components/zwave/test_sensor.py | 183 -- tests/components/zwave/test_switch.py | 85 - tests/components/zwave/test_websocket_api.py | 95 - tests/components/zwave/test_workaround.py | 71 - tests/components/zwave_js/test_migrate.py | 3 +- tests/mock/__init__.py | 1 - tests/mock/zwave.py | 208 -- 106 files changed, 6 insertions(+), 13535 deletions(-) delete mode 100644 homeassistant/components/config/zwave.py delete mode 100644 homeassistant/components/zwave/__init__.py delete mode 100644 homeassistant/components/zwave/binary_sensor.py delete mode 100644 homeassistant/components/zwave/climate.py delete mode 100644 homeassistant/components/zwave/config_flow.py delete mode 100644 homeassistant/components/zwave/const.py delete mode 100644 homeassistant/components/zwave/cover.py delete mode 100644 homeassistant/components/zwave/discovery_schemas.py delete mode 100644 homeassistant/components/zwave/fan.py delete mode 100644 homeassistant/components/zwave/light.py delete mode 100644 homeassistant/components/zwave/lock.py delete mode 100644 homeassistant/components/zwave/manifest.json delete mode 100644 homeassistant/components/zwave/migration.py delete mode 100644 homeassistant/components/zwave/node_entity.py delete mode 100644 homeassistant/components/zwave/sensor.py delete mode 100644 homeassistant/components/zwave/services.yaml delete mode 100644 homeassistant/components/zwave/strings.json delete mode 100644 homeassistant/components/zwave/switch.py delete mode 100644 homeassistant/components/zwave/translations/af.json delete mode 100644 homeassistant/components/zwave/translations/ar.json delete mode 100644 homeassistant/components/zwave/translations/bg.json delete mode 100644 homeassistant/components/zwave/translations/bs.json delete mode 100644 homeassistant/components/zwave/translations/ca.json delete mode 100644 homeassistant/components/zwave/translations/cs.json delete mode 100644 homeassistant/components/zwave/translations/cy.json delete mode 100644 homeassistant/components/zwave/translations/da.json delete mode 100644 homeassistant/components/zwave/translations/de.json delete mode 100644 homeassistant/components/zwave/translations/el.json delete mode 100644 homeassistant/components/zwave/translations/en.json delete mode 100644 homeassistant/components/zwave/translations/es-419.json delete mode 100644 homeassistant/components/zwave/translations/es.json delete mode 100644 homeassistant/components/zwave/translations/et.json delete mode 100644 homeassistant/components/zwave/translations/eu.json delete mode 100644 homeassistant/components/zwave/translations/fa.json delete mode 100644 homeassistant/components/zwave/translations/fi.json delete mode 100644 homeassistant/components/zwave/translations/fr.json delete mode 100644 homeassistant/components/zwave/translations/gsw.json delete mode 100644 homeassistant/components/zwave/translations/he.json delete mode 100644 homeassistant/components/zwave/translations/hi.json delete mode 100644 homeassistant/components/zwave/translations/hr.json delete mode 100644 homeassistant/components/zwave/translations/hu.json delete mode 100644 homeassistant/components/zwave/translations/hy.json delete mode 100644 homeassistant/components/zwave/translations/id.json delete mode 100644 homeassistant/components/zwave/translations/is.json delete mode 100644 homeassistant/components/zwave/translations/it.json delete mode 100644 homeassistant/components/zwave/translations/ja.json delete mode 100644 homeassistant/components/zwave/translations/ko.json delete mode 100644 homeassistant/components/zwave/translations/lb.json delete mode 100644 homeassistant/components/zwave/translations/lt.json delete mode 100644 homeassistant/components/zwave/translations/lv.json delete mode 100644 homeassistant/components/zwave/translations/nb.json delete mode 100644 homeassistant/components/zwave/translations/nl.json delete mode 100644 homeassistant/components/zwave/translations/nn.json delete mode 100644 homeassistant/components/zwave/translations/no.json delete mode 100644 homeassistant/components/zwave/translations/pl.json delete mode 100644 homeassistant/components/zwave/translations/pt-BR.json delete mode 100644 homeassistant/components/zwave/translations/pt.json delete mode 100644 homeassistant/components/zwave/translations/ro.json delete mode 100644 homeassistant/components/zwave/translations/ru.json delete mode 100644 homeassistant/components/zwave/translations/sk.json delete mode 100644 homeassistant/components/zwave/translations/sl.json delete mode 100644 homeassistant/components/zwave/translations/sr-Latn.json delete mode 100644 homeassistant/components/zwave/translations/sr.json delete mode 100644 homeassistant/components/zwave/translations/sv.json delete mode 100644 homeassistant/components/zwave/translations/ta.json delete mode 100644 homeassistant/components/zwave/translations/te.json delete mode 100644 homeassistant/components/zwave/translations/th.json delete mode 100644 homeassistant/components/zwave/translations/tr.json delete mode 100644 homeassistant/components/zwave/translations/uk.json delete mode 100644 homeassistant/components/zwave/translations/vi.json delete mode 100644 homeassistant/components/zwave/translations/zh-Hans.json delete mode 100644 homeassistant/components/zwave/translations/zh-Hant.json delete mode 100644 homeassistant/components/zwave/util.py delete mode 100644 homeassistant/components/zwave/websocket_api.py delete mode 100644 homeassistant/components/zwave/workaround.py delete mode 100644 tests/components/config/test_zwave.py delete mode 100644 tests/components/zwave/__init__.py delete mode 100644 tests/components/zwave/conftest.py delete mode 100644 tests/components/zwave/test_binary_sensor.py delete mode 100644 tests/components/zwave/test_climate.py delete mode 100644 tests/components/zwave/test_cover.py delete mode 100644 tests/components/zwave/test_fan.py delete mode 100644 tests/components/zwave/test_init.py delete mode 100644 tests/components/zwave/test_light.py delete mode 100644 tests/components/zwave/test_lock.py delete mode 100644 tests/components/zwave/test_node_entity.py delete mode 100644 tests/components/zwave/test_sensor.py delete mode 100644 tests/components/zwave/test_switch.py delete mode 100644 tests/components/zwave/test_websocket_api.py delete mode 100644 tests/components/zwave/test_workaround.py delete mode 100644 tests/mock/__init__.py delete mode 100644 tests/mock/zwave.py diff --git a/.coveragerc b/.coveragerc index ed5aff10dd6..47400426202 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1464,7 +1464,6 @@ omit = homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* homeassistant/components/supla/* - homeassistant/components/zwave/util.py homeassistant/components/zwave_js/discovery.py homeassistant/components/zwave_js/sensor.py homeassistant/components/zwave_me/__init__.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9a561d11997..0b95a1cc96f 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -148,7 +148,6 @@ jobs: sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" ${requirement_file} done - name: Build wheels diff --git a/CODEOWNERS b/CODEOWNERS index eee2d323086..12a7b42f0d2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1179,8 +1179,6 @@ tests/components/zodiac/* @JulienTant homeassistant/components/zone/* @home-assistant/core tests/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom -homeassistant/components/zwave/* @home-assistant/z-wave -tests/components/zwave/* @home-assistant/z-wave homeassistant/components/zwave_js/* @home-assistant/z-wave tests/components/zwave_js/* @home-assistant/z-wave homeassistant/components/zwave_me/* @lawfulchaos @Z-Wave-Me diff --git a/Dockerfile b/Dockerfile index 7193d706b89..1d6ce675e74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,7 @@ RUN \ -r homeassistant/requirements.txt --use-deprecated=legacy-resolver COPY requirements_all.txt homeassistant/ RUN \ - sed -i "s|# homeassistant-pyozw|homeassistant-pyozw|g" homeassistant/requirements_all.txt \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver ## Setup Home Assistant Core diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 772f7376acc..329134e5486 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.setup import ATTR_COMPONENT @@ -29,7 +29,6 @@ SECTIONS = ( "script", "scene", ) -ON_DEMAND = ("zwave",) ACTION_CREATE_UPDATE = "create_update" ACTION_DELETE = "delete" @@ -53,21 +52,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: key = f"{DOMAIN}.{panel_name}" hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) - @callback - def component_loaded(event): - """Respond to components being loaded.""" - panel_name = event.data.get(ATTR_COMPONENT) - if panel_name in ON_DEMAND: - hass.async_create_task(setup_panel(panel_name)) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - tasks = [asyncio.create_task(setup_panel(panel_name)) for panel_name in SECTIONS] - for panel_name in ON_DEMAND: - if panel_name in hass.config.components: - tasks.append(asyncio.create_task(setup_panel(panel_name))) - if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py deleted file mode 100644 index 63b7bdf9868..00000000000 --- a/homeassistant/components/config/zwave.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Provide configuration end points for Z-Wave.""" -from collections import deque -from http import HTTPStatus -import logging - -from aiohttp.web import Response - -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const -import homeassistant.core as ha -import homeassistant.helpers.config_validation as cv - -from . import EditKeyBasedConfigView - -_LOGGER = logging.getLogger(__name__) -CONFIG_PATH = "zwave_device_config.yaml" -OZW_LOG_FILENAME = "OZW_Log.txt" - - -async def async_setup(hass): - """Set up the Z-Wave config API.""" - hass.http.register_view( - EditKeyBasedConfigView( - "zwave", - "device_config", - CONFIG_PATH, - cv.entity_id, - DEVICE_CONFIG_SCHEMA_ENTRY, - ) - ) - hass.http.register_view(ZWaveNodeValueView) - hass.http.register_view(ZWaveNodeGroupView) - hass.http.register_view(ZWaveNodeConfigView) - hass.http.register_view(ZWaveUserCodeView) - hass.http.register_view(ZWaveLogView) - hass.http.register_view(ZWaveConfigWriteView) - hass.http.register_view(ZWaveProtectionView) - - return True - - -class ZWaveLogView(HomeAssistantView): - """View to read the ZWave log file.""" - - url = "/api/zwave/ozwlog" - name = "api:zwave:ozwlog" - - # pylint: disable=no-self-use - async def get(self, request): - """Retrieve the lines from ZWave log.""" - try: - lines = int(request.query.get("lines", 0)) - except ValueError: - return Response(text="Invalid datetime", status=HTTPStatus.BAD_REQUEST) - - hass = request.app["hass"] - response = await hass.async_add_executor_job(self._get_log, hass, lines) - - return Response(text="\n".join(response)) - - def _get_log(self, hass, lines): - """Retrieve the logfile content.""" - logfilepath = hass.config.path(OZW_LOG_FILENAME) - with open(logfilepath, encoding="utf8") as logfile: - data = (line.rstrip() for line in logfile) - if lines == 0: - loglines = list(data) - else: - loglines = deque(data, lines) - return loglines - - -class ZWaveConfigWriteView(HomeAssistantView): - """View to save the ZWave configuration to zwcfg_xxxxx.xml.""" - - url = "/api/zwave/saveconfig" - name = "api:zwave:saveconfig" - - @ha.callback - def post(self, request): - """Save cache configuration to zwcfg_xxxxx.xml.""" - hass = request.app["hass"] - if (network := hass.data.get(const.DATA_NETWORK)) is None: - return self.json_message( - "No Z-Wave network data found", HTTPStatus.NOT_FOUND - ) - _LOGGER.info("Z-Wave configuration written to file") - network.write_config() - return self.json_message("Z-Wave configuration saved to file") - - -class ZWaveNodeValueView(HomeAssistantView): - """View to return the node values.""" - - url = r"/api/zwave/values/{node_id:\d+}" - name = "api:zwave:values" - - @ha.callback - def get(self, request, node_id): - """Retrieve groups of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - values_list = hass.data[const.DATA_ENTITY_VALUES] - - values_data = {} - # Return a list of values for this node that are used as a - # primary value for an entity - for entity_values in values_list: - if entity_values.primary.node.node_id != nodeid: - continue - - values_data[entity_values.primary.value_id] = { - "label": entity_values.primary.label, - "index": entity_values.primary.index, - "instance": entity_values.primary.instance, - "poll_intensity": entity_values.primary.poll_intensity, - } - return self.json(values_data) - - -class ZWaveNodeGroupView(HomeAssistantView): - """View to return the nodes group configuration.""" - - url = r"/api/zwave/groups/{node_id:\d+}" - name = "api:zwave:groups" - - @ha.callback - def get(self, request, node_id): - """Retrieve groups of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - groupdata = node.groups - groups = {} - for key, value in groupdata.items(): - groups[key] = { - "associations": value.associations, - "association_instances": value.associations_instances, - "label": value.label, - "max_associations": value.max_associations, - } - return self.json(groups) - - -class ZWaveNodeConfigView(HomeAssistantView): - """View to return the nodes configuration options.""" - - url = r"/api/zwave/config/{node_id:\d+}" - name = "api:zwave:config" - - @ha.callback - def get(self, request, node_id): - """Retrieve configurations of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - config = {} - for value in node.get_values( - class_id=const.COMMAND_CLASS_CONFIGURATION - ).values(): - config[value.index] = { - "label": value.label, - "type": value.type, - "help": value.help, - "data_items": value.data_items, - "data": value.data, - "max": value.max, - "min": value.min, - } - return self.json(config) - - -class ZWaveUserCodeView(HomeAssistantView): - """View to return the nodes usercode configuration.""" - - url = r"/api/zwave/usercodes/{node_id:\d+}" - name = "api:zwave:usercodes" - - @ha.callback - def get(self, request, node_id): - """Retrieve usercodes of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - usercodes = {} - if not node.has_command_class(const.COMMAND_CLASS_USER_CODE): - return self.json(usercodes) - for value in node.get_values(class_id=const.COMMAND_CLASS_USER_CODE).values(): - if value.genre != const.GENRE_USER: - continue - usercodes[value.index] = { - "code": value.data, - "label": value.label, - "length": len(value.data), - } - return self.json(usercodes) - - -class ZWaveProtectionView(HomeAssistantView): - """View for the protection commandclass of a node.""" - - url = r"/api/zwave/protection/{node_id:\d+}" - name = "api:zwave:protection" - - async def get(self, request, node_id): - """Retrieve the protection commandclass options of node.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - - def _fetch_protection(): - """Get protection data.""" - if (node := network.nodes.get(nodeid)) is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - protection_options = {} - if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): - return self.json(protection_options) - protections = node.get_protections() - protection_options = { - "value_id": f"{list(protections)[0]:d}", - "selected": node.get_protection_item(list(protections)[0]), - "options": node.get_protection_items(list(protections)[0]), - } - return self.json(protection_options) - - return await hass.async_add_executor_job(_fetch_protection) - - async def post(self, request, node_id): - """Change the selected option in protection commandclass.""" - nodeid = int(node_id) - hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - protection_data = await request.json() - - def _set_protection(): - """Set protection data.""" - node = network.nodes.get(nodeid) - selection = protection_data["selection"] - value_id = int(protection_data[const.ATTR_VALUE_ID]) - if node is None: - return self.json_message("Node not found", HTTPStatus.NOT_FOUND) - if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): - return self.json_message( - "No protection commandclass on this node", HTTPStatus.NOT_FOUND - ) - state = node.set_protection(value_id, selection) - if not state: - return self.json_message( - "Protection setting did not complete", HTTPStatus.ACCEPTED - ) - return self.json_message("Protection setting successfully set") - - return await hass.async_add_executor_job(_set_protection) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py deleted file mode 100644 index 3424aa11a87..00000000000 --- a/homeassistant/components/zwave/__init__.py +++ /dev/null @@ -1,1355 +0,0 @@ -"""Support for Z-Wave.""" -# pylint: disable=import-error -# pylint: disable=import-outside-toplevel -from __future__ import annotations - -import asyncio -import copy -from importlib import import_module -import logging -from pprint import pprint - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_NAME, - ATTR_VIA_DEVICE, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall, callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_device_registry, -) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import DeviceInfo, generate_entity_id -from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL -from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) -from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import async_track_time_change -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert -import homeassistant.util.dt as dt_util - -from . import const, websocket_api as wsapi, workaround -from .const import ( - CONF_AUTOHEAL, - CONF_CONFIG_PATH, - CONF_DEBUG, - CONF_NETWORK_KEY, - CONF_POLLING_INTERVAL, - CONF_USB_STICK_PATH, - DATA_DEVICES, - DATA_ENTITY_VALUES, - DATA_NETWORK, - DATA_ZWAVE_CONFIG, - DEFAULT_CONF_AUTOHEAL, - DEFAULT_CONF_USB_STICK_PATH, - DEFAULT_DEBUG, - DEFAULT_POLLING_INTERVAL, - DOMAIN, -) -from .discovery_schemas import DISCOVERY_SCHEMAS -from .migration import ( # noqa: F401 - async_add_migration_entity_value, - async_get_migration_data, - async_is_ozw_migrated, - async_is_zwave_js_migrated, -) -from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity -from .util import ( - check_has_unique_id, - check_node_schema, - check_value_schema, - compute_value_unique_id, - is_node_parsed, - node_device_id_and_name, - node_name, -) - -_LOGGER = logging.getLogger(__name__) - -CLASS_ID = "class_id" - -ATTR_POWER = "power_consumption" - -CONF_POLLING_INTENSITY = "polling_intensity" -CONF_IGNORED = "ignored" -CONF_INVERT_OPENCLOSE_BUTTONS = "invert_openclose_buttons" -CONF_INVERT_PERCENT = "invert_percent" -CONF_REFRESH_VALUE = "refresh_value" -CONF_REFRESH_DELAY = "delay" -CONF_DEVICE_CONFIG = "device_config" -CONF_DEVICE_CONFIG_GLOB = "device_config_glob" -CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" - -DATA_ZWAVE_CONFIG_YAML_PRESENT = "zwave_config_yaml_present" - -DEFAULT_CONF_IGNORED = False -DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False -DEFAULT_CONF_INVERT_PERCENT = False -DEFAULT_CONF_REFRESH_VALUE = False -DEFAULT_CONF_REFRESH_DELAY = 5 - -PLATFORMS = [ - Platform.BINARY_SENSOR, - Platform.CLIMATE, - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SENSOR, - Platform.SWITCH, -] - -RENAME_NODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_NAME): cv.string, - vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, - } -) - -RENAME_VALUE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - vol.Required(ATTR_NAME): cv.string, - vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, - } -) - -SET_CONFIG_PARAMETER_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string), - vol.Optional(const.ATTR_CONFIG_SIZE, default=2): vol.Coerce(int), - } -) - -SET_NODE_VALUE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Any(vol.Coerce(int), cv.string), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string), - } -) - -REFRESH_NODE_VALUE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - } -) - -SET_POLL_INTENSITY_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), - vol.Required(const.ATTR_POLL_INTENSITY): vol.Coerce(int), - } -) - -PRINT_CONFIG_PARAMETER_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - } -) - -NODE_SERVICE_SCHEMA = vol.Schema({vol.Required(const.ATTR_NODE_ID): vol.Coerce(int)}) - -REFRESH_ENTITY_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id}) - -RESET_NODE_METERS_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Optional(const.ATTR_INSTANCE, default=1): vol.Coerce(int), - } -) - -CHANGE_ASSOCIATION_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_ASSOCIATION): cv.string, - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_TARGET_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_GROUP): vol.Coerce(int), - vol.Optional(const.ATTR_INSTANCE, default=0x00): vol.Coerce(int), - } -) - -SET_WAKEUP_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.All( - vol.Coerce(int), cv.positive_int - ), - } -) - -HEAL_NODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Optional(const.ATTR_RETURN_ROUTES, default=False): cv.boolean, - } -) - -TEST_NODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Optional(const.ATTR_MESSAGES, default=1): cv.positive_int, - } -) - - -DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( - { - vol.Optional(CONF_POLLING_INTENSITY): cv.positive_int, - vol.Optional(CONF_IGNORED, default=DEFAULT_CONF_IGNORED): cv.boolean, - vol.Optional( - CONF_INVERT_OPENCLOSE_BUTTONS, default=DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS - ): cv.boolean, - vol.Optional( - CONF_INVERT_PERCENT, default=DEFAULT_CONF_INVERT_PERCENT - ): cv.boolean, - vol.Optional( - CONF_REFRESH_VALUE, default=DEFAULT_CONF_REFRESH_VALUE - ): cv.boolean, - vol.Optional( - CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY - ): cv.positive_int, - } -) - -SIGNAL_REFRESH_ENTITY_FORMAT = "zwave_refresh_entity_{}" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_AUTOHEAL, default=DEFAULT_CONF_AUTOHEAL): cv.boolean, - vol.Optional(CONF_CONFIG_PATH): cv.string, - vol.Optional(CONF_NETWORK_KEY): vol.All( - cv.string, vol.Match(r"(0x\w\w,\s?){15}0x\w\w") - ), - vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( - {cv.entity_id: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_DEVICE_CONFIG_GLOB, default={}): vol.Schema( - {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_DEVICE_CONFIG_DOMAIN, default={}): vol.Schema( - {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} - ), - vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, - vol.Optional( - CONF_POLLING_INTERVAL, default=DEFAULT_POLLING_INTERVAL - ): cv.positive_int, - vol.Optional(CONF_USB_STICK_PATH): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def _obj_to_dict(obj): - """Convert an object into a hash for debug.""" - return { - key: getattr(obj, key) - for key in dir(obj) - if key[0] != "_" and not callable(getattr(obj, key)) - } - - -def _value_name(value): - """Return the name of the value.""" - return f"{node_name(value.node)} {value.label}".strip() - - -def nice_print_node(node): - """Print a nice formatted node to the output (debug method).""" - node_dict = _obj_to_dict(node) - node_dict["values"] = { - value_id: _obj_to_dict(value) for value_id, value in node.values.items() - } - - _LOGGER.info("FOUND NODE %s \n%s", node.product_name, node_dict) - - -def get_config_value(node, value_index, tries=5): - """Return the current configuration value for a specific index.""" - try: - for value in node.values.values(): - if ( - value.command_class == const.COMMAND_CLASS_CONFIGURATION - and value.index == value_index - ): - return value.data - except RuntimeError: - # If we get a runtime error the dict has changed while - # we was looking for a value, just do it again - return ( - None if tries <= 0 else get_config_value(node, value_index, tries=tries - 1) - ) - return None - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Z-Wave platform (generic part).""" - if discovery_info is None or DATA_NETWORK not in hass.data: - return False - - device = hass.data[DATA_DEVICES].get(discovery_info[const.DISCOVERY_DEVICE]) - if device is None: - return False - - async_add_entities([device]) - return True - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Z-Wave components.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - hass.data[DATA_ZWAVE_CONFIG] = conf - hass.data[DATA_ZWAVE_CONFIG_YAML_PRESENT] = True - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_USB_STICK_PATH: conf.get( - CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH - ), - CONF_NETWORK_KEY: conf.get(CONF_NETWORK_KEY), - }, - ) - ) - - return True - - -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, config_entry: config_entries.ConfigEntry -) -> bool: - """Set up Z-Wave from a config entry. - - Will automatically load components to support devices found on the network. - """ - from openzwave.group import ZWaveGroup - from openzwave.network import ZWaveNetwork - from openzwave.option import ZWaveOption - from pydispatch import dispatcher - - if async_is_ozw_migrated(hass) or async_is_zwave_js_migrated(hass): - - if hass.data.get(DATA_ZWAVE_CONFIG_YAML_PRESENT): - config_yaml_message = ( - ", and remove %s from configuration.yaml " - "to avoid setting up this integration on restart ", - DOMAIN, - ) - else: - config_yaml_message = "" - - _LOGGER.error( - "Migration away from legacy Z-Wave has been done. " - "Please remove the %s integration%s", - DOMAIN, - config_yaml_message, - ) - return False - - # Merge config entry and yaml config - config = config_entry.data - if DATA_ZWAVE_CONFIG in hass.data: - config = {**config, **hass.data[DATA_ZWAVE_CONFIG]} - - # Update hass.data with merged config so we can access it elsewhere - hass.data[DATA_ZWAVE_CONFIG] = config - - # Load configuration - use_debug = config.get(CONF_DEBUG, DEFAULT_DEBUG) - autoheal = config.get(CONF_AUTOHEAL, DEFAULT_CONF_AUTOHEAL) - device_config = EntityValues( - config.get(CONF_DEVICE_CONFIG), - config.get(CONF_DEVICE_CONFIG_DOMAIN), - config.get(CONF_DEVICE_CONFIG_GLOB), - ) - - usb_path = config[CONF_USB_STICK_PATH] - - _LOGGER.info("Z-Wave USB path is %s", usb_path) - - # Setup options - options = ZWaveOption( - usb_path, - user_path=hass.config.config_dir, - config_path=config.get(CONF_CONFIG_PATH), - ) - - options.set_console_output(use_debug) - - if config.get(CONF_NETWORK_KEY): - options.addOption("NetworkKey", config[CONF_NETWORK_KEY]) - - await hass.async_add_executor_job(options.lock) - network = hass.data[DATA_NETWORK] = ZWaveNetwork(options, autostart=False) - hass.data[DATA_DEVICES] = {} - hass.data[DATA_ENTITY_VALUES] = [] - - registry = await async_get_entity_registry(hass) - - wsapi.async_load_websocket_api(hass) - - if use_debug: # pragma: no cover - - def log_all(signal, value=None): - """Log all the signals.""" - print("") - print("SIGNAL *****", signal) - if value and signal in ( - ZWaveNetwork.SIGNAL_VALUE_CHANGED, - ZWaveNetwork.SIGNAL_VALUE_ADDED, - ZWaveNetwork.SIGNAL_SCENE_EVENT, - ZWaveNetwork.SIGNAL_NODE_EVENT, - ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD, - ): - pprint(_obj_to_dict(value)) - - print("") - - dispatcher.connect(log_all, weak=False) - - def value_added(node, value): - """Handle new added value to a node on the network.""" - # Check if this value should be tracked by an existing entity - for values in hass.data[DATA_ENTITY_VALUES]: - values.check_value(value) - - for schema in DISCOVERY_SCHEMAS: - if not check_node_schema(node, schema): - continue - if not check_value_schema( - value, schema[const.DISC_VALUES][const.DISC_PRIMARY] - ): - continue - - values = ZWaveDeviceEntityValues( - hass, schema, value, config, device_config, registry - ) - - # We create a new list and update the reference here so that - # the list can be safely iterated over in the main thread - new_values = hass.data[DATA_ENTITY_VALUES] + [values] - hass.data[DATA_ENTITY_VALUES] = new_values - - platform = EntityPlatform( - hass=hass, - logger=_LOGGER, - domain=DOMAIN, - platform_name=DOMAIN, - platform=None, - scan_interval=DEFAULT_SCAN_INTERVAL, - entity_namespace=None, - ) - platform.config_entry = config_entry - - def node_added(node): - """Handle a new node on the network.""" - entity = ZWaveNodeEntity(node, network) - - async def _add_node_to_component(): - if hass.data[DATA_DEVICES].get(entity.unique_id): - return - - name = node_name(node) - generated_id = generate_entity_id(DOMAIN + ".{}", name, []) - node_config = device_config.get(generated_id) - if node_config.get(CONF_IGNORED): - _LOGGER.info( - "Ignoring node entity %s due to device settings", generated_id - ) - return - - hass.data[DATA_DEVICES][entity.unique_id] = entity - await platform.async_add_entities([entity]) - - if entity.unique_id: - hass.create_task(_add_node_to_component()) - return - - @callback - def _on_ready(sec): - _LOGGER.info("Z-Wave node %d ready after %d seconds", entity.node_id, sec) - hass.async_add_job(_add_node_to_component) - - @callback - def _on_timeout(sec): - _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, continuing anyway", - entity.node_id, - sec, - ) - hass.async_add_job(_add_node_to_component) - - hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout) - - def node_removed(node): - node_id = node.node_id - node_key = f"node-{node_id}" - for key in list(hass.data[DATA_DEVICES]): - if key is None: - continue - if not key.startswith(f"{node_id}-"): - continue - - entity = hass.data[DATA_DEVICES][key] - _LOGGER.debug( - "Removing Entity - value: %s - entity_id: %s", key, entity.entity_id - ) - hass.add_job(entity.node_removed()) - del hass.data[DATA_DEVICES][key] - - entity = hass.data[DATA_DEVICES][node_key] - hass.add_job(entity.node_removed()) - del hass.data[DATA_DEVICES][node_key] - - hass.add_job(_remove_device(node)) - - async def _remove_device(node): - dev_reg = await async_get_device_registry(hass) - identifier, name = node_device_id_and_name(node) - device = dev_reg.async_get_device(identifiers={identifier}) - if device is not None: - _LOGGER.debug("Removing Device - %s - %s", device.id, name) - dev_reg.async_remove_device(device.id) - - def network_ready(): - """Handle the query of all awake nodes.""" - _LOGGER.info( - "Z-Wave network is ready for use. All awake nodes " - "have been queried. Sleeping nodes will be " - "queried when they awake" - ) - hass.bus.fire(const.EVENT_NETWORK_READY) - - def network_complete(): - """Handle the querying of all nodes on network.""" - _LOGGER.info( - "Z-Wave network is complete. All nodes on the network have been queried" - ) - hass.bus.fire(const.EVENT_NETWORK_COMPLETE) - - def network_complete_some_dead(): - """Handle the querying of all nodes on network.""" - _LOGGER.info( - "Z-Wave network is complete. All nodes on the network " - "have been queried, but some nodes are marked dead" - ) - hass.bus.fire(const.EVENT_NETWORK_COMPLETE_SOME_DEAD) - - dispatcher.connect(value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) - dispatcher.connect(node_added, ZWaveNetwork.SIGNAL_NODE_ADDED, weak=False) - dispatcher.connect(node_removed, ZWaveNetwork.SIGNAL_NODE_REMOVED, weak=False) - dispatcher.connect( - network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False - ) - dispatcher.connect( - network_complete, ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, weak=False - ) - dispatcher.connect( - network_complete_some_dead, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD, - weak=False, - ) - - def add_node(service: ServiceCall) -> None: - """Switch into inclusion mode.""" - _LOGGER.info("Z-Wave add_node have been initialized") - network.controller.add_node() - - def add_node_secure(service: ServiceCall) -> None: - """Switch into secure inclusion mode.""" - _LOGGER.info("Z-Wave add_node_secure have been initialized") - network.controller.add_node(True) - - def remove_node(service: ServiceCall) -> None: - """Switch into exclusion mode.""" - _LOGGER.info("Z-Wave remove_node have been initialized") - network.controller.remove_node() - - def cancel_command(service: ServiceCall) -> None: - """Cancel a running controller command.""" - _LOGGER.info("Cancel running Z-Wave command") - network.controller.cancel_command() - - def heal_network(service: ServiceCall) -> None: - """Heal the network.""" - _LOGGER.info("Z-Wave heal running") - network.heal() - - def soft_reset(service: ServiceCall) -> None: - """Soft reset the controller.""" - _LOGGER.info("Z-Wave soft_reset have been initialized") - network.controller.soft_reset() - - def test_network(service: ServiceCall) -> None: - """Test the network by sending commands to all the nodes.""" - _LOGGER.info("Z-Wave test_network have been initialized") - network.test() - - def stop_network(_service_or_event: Event | ServiceCall) -> None: - """Stop Z-Wave network.""" - _LOGGER.info("Stopping Z-Wave network") - network.stop() - if hass.state == CoreState.running: - hass.bus.fire(const.EVENT_NETWORK_STOP) - - async def rename_node(service: ServiceCall) -> None: - """Rename a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - name = service.data.get(ATTR_NAME) - node.name = name - _LOGGER.info("Renamed Z-Wave node %d to %s", node_id, name) - update_ids = service.data.get(const.ATTR_UPDATE_IDS) - # We want to rename the device, the node entity, - # and all the contained entities - node_key = f"node-{node_id}" - entity = hass.data[DATA_DEVICES][node_key] - await entity.node_renamed(update_ids) - for key in list(hass.data[DATA_DEVICES]): - if not key.startswith(f"{node_id}-"): - continue - entity = hass.data[DATA_DEVICES][key] - await entity.value_renamed(update_ids) - - async def rename_value(service: ServiceCall) -> None: - """Rename a node value.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - value = node.values[value_id] - name = service.data.get(ATTR_NAME) - value.label = name - _LOGGER.info( - "Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name - ) - update_ids = service.data.get(const.ATTR_UPDATE_IDS) - value_key = f"{node_id}-{value_id}" - entity = hass.data[DATA_DEVICES][value_key] - await entity.value_renamed(update_ids) - - def set_poll_intensity(service: ServiceCall) -> None: - """Set the polling intensity of a node value.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - value = node.values[value_id] - intensity = service.data.get(const.ATTR_POLL_INTENSITY) - if intensity == 0: - if value.disable_poll(): - _LOGGER.info("Polling disabled (Node %d Value %d)", node_id, value_id) - return - _LOGGER.info( - "Polling disabled failed (Node %d Value %d)", node_id, value_id - ) - else: - if value.enable_poll(intensity): - _LOGGER.info( - "Set polling intensity (Node %d Value %d) to %s", - node_id, - value_id, - intensity, - ) - return - _LOGGER.info( - "Set polling intensity failed (Node %d Value %d)", node_id, value_id - ) - - def remove_failed_node(service: ServiceCall) -> None: - """Remove failed node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - _LOGGER.info("Trying to remove zwave node %d", node_id) - network.controller.remove_failed_node(node_id) - - def replace_failed_node(service: ServiceCall) -> None: - """Replace failed node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - _LOGGER.info("Trying to replace zwave node %d", node_id) - network.controller.replace_failed_node(node_id) - - def set_config_parameter(service: ServiceCall) -> None: - """Set a config parameter to a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - param = service.data.get(const.ATTR_CONFIG_PARAMETER) - selection = service.data.get(const.ATTR_CONFIG_VALUE) - size = service.data.get(const.ATTR_CONFIG_SIZE) - for value in node.get_values( - class_id=const.COMMAND_CLASS_CONFIGURATION - ).values(): - if value.index != param: - continue - if value.type == const.TYPE_BOOL: - value.data = int(selection == "True") - _LOGGER.info( - "Setting configuration parameter %s on Node %s with bool selection %s", - param, - node_id, - str(selection), - ) - return - if value.type == const.TYPE_LIST: - value.data = str(selection) - _LOGGER.info( - "Setting configuration parameter %s on Node %s with list selection %s", - param, - node_id, - str(selection), - ) - return - if value.type == const.TYPE_BUTTON: - network.manager.pressButton(value.value_id) - network.manager.releaseButton(value.value_id) - _LOGGER.info( - "Setting configuration parameter %s on Node %s " - "with button selection %s", - param, - node_id, - selection, - ) - return - value.data = int(selection) - _LOGGER.info( - "Setting configuration parameter %s on Node %s with selection %s", - param, - node_id, - selection, - ) - return - node.set_config_param(param, selection, size) - _LOGGER.info( - "Setting unknown configuration parameter %s on Node %s with selection %s", - param, - node_id, - selection, - ) - - def refresh_node_value(service: ServiceCall) -> None: - """Refresh the specified value from a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - node.values[value_id].refresh() - _LOGGER.info("Node %s value %s refreshed", node_id, value_id) - - def set_node_value(service: ServiceCall) -> None: - """Set the specified value on a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - value_id = service.data.get(const.ATTR_VALUE_ID) - value = service.data.get(const.ATTR_CONFIG_VALUE) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - node.values[value_id].data = value - _LOGGER.info("Node %s value %s set to %s", node_id, value_id, value) - - def print_config_parameter(service: ServiceCall) -> None: - """Print a config parameter from a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - param = service.data.get(const.ATTR_CONFIG_PARAMETER) - _LOGGER.info( - "Config parameter %s on Node %s: %s", - param, - node_id, - get_config_value(node, param), - ) - - def print_node(service: ServiceCall) -> None: - """Print all information about z-wave node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - nice_print_node(node) - - def set_wakeup(service: ServiceCall) -> None: - """Set wake-up interval of a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - value = service.data.get(const.ATTR_CONFIG_VALUE) - if node.can_wake_up(): - for value_id in node.get_values(class_id=const.COMMAND_CLASS_WAKE_UP): - node.values[value_id].data = value - _LOGGER.info("Node %s wake-up set to %d", node_id, value) - else: - _LOGGER.info("Node %s is not wakeable", node_id) - - def change_association(service: ServiceCall) -> None: - """Change an association in the zwave network.""" - association_type = service.data.get(const.ATTR_ASSOCIATION) - node_id = service.data.get(const.ATTR_NODE_ID) - target_node_id = service.data.get(const.ATTR_TARGET_NODE_ID) - group = service.data.get(const.ATTR_GROUP) - instance = service.data.get(const.ATTR_INSTANCE) - - node = ZWaveGroup(group, network, node_id) - if association_type == "add": - node.add_association(target_node_id, instance) - _LOGGER.info( - "Adding association for node:%s in group:%s " - "target node:%s, instance=%s", - node_id, - group, - target_node_id, - instance, - ) - if association_type == "remove": - node.remove_association(target_node_id, instance) - _LOGGER.info( - "Removing association for node:%s in group:%s " - "target node:%s, instance=%s", - node_id, - group, - target_node_id, - instance, - ) - - async def async_refresh_entity(service: ServiceCall) -> None: - """Refresh values that specific entity depends on.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - async_dispatcher_send(hass, SIGNAL_REFRESH_ENTITY_FORMAT.format(entity_id)) - - def refresh_node(service: ServiceCall) -> None: - """Refresh all node info.""" - node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - node.refresh_info() - - def reset_node_meters(service: ServiceCall) -> None: - """Reset meter counters of a node.""" - node_id = service.data.get(const.ATTR_NODE_ID) - instance = service.data.get(const.ATTR_INSTANCE) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - for value in node.get_values(class_id=const.COMMAND_CLASS_METER).values(): - if value.index != const.INDEX_METER_RESET: - continue - if value.instance != instance: - continue - network.manager.pressButton(value.value_id) - network.manager.releaseButton(value.value_id) - _LOGGER.info("Resetting meters on node %s instance %s", node_id, instance) - return - _LOGGER.info( - "Node %s on instance %s does not have resettable meters", node_id, instance - ) - - def heal_node(service: ServiceCall) -> None: - """Heal a node on the network.""" - node_id = service.data.get(const.ATTR_NODE_ID) - update_return_routes = service.data.get(const.ATTR_RETURN_ROUTES) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - _LOGGER.info("Z-Wave node heal running for node %s", node_id) - node.heal(update_return_routes) - - def test_node(service: ServiceCall) -> None: - """Send test messages to a node on the network.""" - node_id = service.data.get(const.ATTR_NODE_ID) - messages = service.data.get(const.ATTR_MESSAGES) - node = network.nodes[node_id] # pylint: disable=unsubscriptable-object - _LOGGER.info("Sending %s test-messages to node %s", messages, node_id) - node.test(messages) - - def start_zwave(_service_or_event: ServiceCall | Event) -> None: - """Startup Z-Wave network.""" - _LOGGER.info("Starting Z-Wave network") - network.start() - hass.bus.fire(const.EVENT_NETWORK_START) - - async def _check_awaked(): - """Wait for Z-wave awaked state (or timeout) and finalize start.""" - _LOGGER.debug("network state: %d %s", network.state, network.state_str) - - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow() - start_time).total_seconds()) - - if network.state >= network.STATE_AWAKED: - # Need to be in STATE_AWAKED before talking to nodes. - _LOGGER.info("Z-Wave ready after %d seconds", waited) - break - - if waited >= const.NETWORK_READY_WAIT_SECS: - # Wait up to NETWORK_READY_WAIT_SECS seconds for the Z-Wave - # network to be ready. - _LOGGER.warning( - "Z-Wave not ready after %d seconds, continuing anyway", waited - ) - _LOGGER.info( - "Final network state: %d %s", network.state, network.state_str - ) - break - - await asyncio.sleep(1) - - hass.async_add_job(_finalize_start) - - hass.add_job(_check_awaked) - - def _finalize_start(): - """Perform final initializations after Z-Wave network is awaked.""" - polling_interval = convert(config.get(CONF_POLLING_INTERVAL), int) - if polling_interval is not None: - network.set_poll_interval(polling_interval, False) - - poll_interval = network.get_poll_interval() - _LOGGER.info("Z-Wave polling interval set to %d ms", poll_interval) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_network) - - # Register node services for Z-Wave network - hass.services.register(DOMAIN, const.SERVICE_ADD_NODE, add_node) - hass.services.register(DOMAIN, const.SERVICE_ADD_NODE_SECURE, add_node_secure) - hass.services.register(DOMAIN, const.SERVICE_REMOVE_NODE, remove_node) - hass.services.register(DOMAIN, const.SERVICE_CANCEL_COMMAND, cancel_command) - hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) - hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) - hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) - hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_network) - hass.services.register( - DOMAIN, const.SERVICE_RENAME_NODE, rename_node, schema=RENAME_NODE_SCHEMA - ) - hass.services.register( - DOMAIN, const.SERVICE_RENAME_VALUE, rename_value, schema=RENAME_VALUE_SCHEMA - ) - hass.services.register( - DOMAIN, - const.SERVICE_SET_CONFIG_PARAMETER, - set_config_parameter, - schema=SET_CONFIG_PARAMETER_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_SET_NODE_VALUE, - set_node_value, - schema=SET_NODE_VALUE_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_REFRESH_NODE_VALUE, - refresh_node_value, - schema=REFRESH_NODE_VALUE_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_PRINT_CONFIG_PARAMETER, - print_config_parameter, - schema=PRINT_CONFIG_PARAMETER_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_REMOVE_FAILED_NODE, - remove_failed_node, - schema=NODE_SERVICE_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_REPLACE_FAILED_NODE, - replace_failed_node, - schema=NODE_SERVICE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - const.SERVICE_CHANGE_ASSOCIATION, - change_association, - schema=CHANGE_ASSOCIATION_SCHEMA, - ) - hass.services.register( - DOMAIN, const.SERVICE_SET_WAKEUP, set_wakeup, schema=SET_WAKEUP_SCHEMA - ) - hass.services.register( - DOMAIN, const.SERVICE_PRINT_NODE, print_node, schema=NODE_SERVICE_SCHEMA - ) - hass.services.register( - DOMAIN, - const.SERVICE_REFRESH_ENTITY, - async_refresh_entity, - schema=REFRESH_ENTITY_SCHEMA, - ) - hass.services.register( - DOMAIN, const.SERVICE_REFRESH_NODE, refresh_node, schema=NODE_SERVICE_SCHEMA - ) - hass.services.register( - DOMAIN, - const.SERVICE_RESET_NODE_METERS, - reset_node_meters, - schema=RESET_NODE_METERS_SCHEMA, - ) - hass.services.register( - DOMAIN, - const.SERVICE_SET_POLL_INTENSITY, - set_poll_intensity, - schema=SET_POLL_INTENSITY_SCHEMA, - ) - hass.services.register( - DOMAIN, const.SERVICE_HEAL_NODE, heal_node, schema=HEAL_NODE_SCHEMA - ) - hass.services.register( - DOMAIN, const.SERVICE_TEST_NODE, test_node, schema=TEST_NODE_SCHEMA - ) - - # Setup autoheal - if autoheal: - _LOGGER.info("Z-Wave network autoheal is enabled") - async_track_time_change(hass, heal_network, hour=0, minute=0, second=0) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) - - hass.services.async_register(DOMAIN, const.SERVICE_START_NETWORK, start_zwave) - - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) - - return True - - -class ZWaveDeviceEntityValues: - """Manages entity access to the underlying zwave value objects.""" - - def __init__( - self, hass, schema, primary_value, zwave_config, device_config, registry - ): - """Initialize the values object with the passed entity schema.""" - self._hass = hass - self._zwave_config = zwave_config - self._device_config = device_config - self._schema = copy.deepcopy(schema) - self._values = {} - self._entity = None - self._workaround_ignore = False - self._registry = registry - - for name in self._schema[const.DISC_VALUES].keys(): - self._values[name] = None - self._schema[const.DISC_VALUES][name][const.DISC_INSTANCE] = [ - primary_value.instance - ] - - self._values[const.DISC_PRIMARY] = primary_value - self._node = primary_value.node - self._schema[const.DISC_NODE_ID] = [self._node.node_id] - - # Check values that have already been discovered for node - for value in self._node.values.values(): - self.check_value(value) - - self._check_entity_ready() - - def __getattr__(self, name): - """Get the specified value for this entity.""" - return self._values[name] - - def __iter__(self): - """Allow iteration over all values.""" - return iter(self._values.values()) - - def check_value(self, value): - """Check if the new value matches a missing value for this entity. - - If a match is found, it is added to the values mapping. - """ - if not check_node_schema(value.node, self._schema): - return - for name, name_value in self._values.items(): - if name_value is not None: - continue - if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): - continue - self._values[name] = value - if self._entity: - self._entity.value_added() - self._entity.value_changed() - - self._check_entity_ready() - - def _check_entity_ready(self): - """Check if all required values are discovered and create entity.""" - if self._workaround_ignore: - return - if self._entity is not None: - return - - for name in self._schema[const.DISC_VALUES]: - if self._values[name] is None and not self._schema[const.DISC_VALUES][ - name - ].get(const.DISC_OPTIONAL): - return - - component = self._schema[const.DISC_COMPONENT] - - workaround_component = workaround.get_device_component_mapping(self.primary) - if workaround_component and workaround_component != component: - if workaround_component == workaround.WORKAROUND_IGNORE: - _LOGGER.info( - "Ignoring Node %d Value %d due to workaround", - self.primary.node.node_id, - self.primary.value_id, - ) - # No entity will be created for this value - self._workaround_ignore = True - return - _LOGGER.debug("Using %s instead of %s", workaround_component, component) - component = workaround_component - - entity_id = self._registry.async_get_entity_id( - component, DOMAIN, compute_value_unique_id(self._node, self.primary) - ) - if entity_id is None: - value_name = _value_name(self.primary) - entity_id = generate_entity_id(component + ".{}", value_name, []) - node_config = self._device_config.get(entity_id) - - # Configure node - _LOGGER.debug( - "Adding Node_id=%s Generic_command_class=%s, " - "Specific_command_class=%s, " - "Command_class=%s, Value type=%s, " - "Genre=%s as %s", - self._node.node_id, - self._node.generic, - self._node.specific, - self.primary.command_class, - self.primary.type, - self.primary.genre, - component, - ) - - if node_config.get(CONF_IGNORED): - _LOGGER.info("Ignoring entity %s due to device settings", entity_id) - # No entity will be created for this value - self._workaround_ignore = True - return - - polling_intensity = convert(node_config.get(CONF_POLLING_INTENSITY), int) - if polling_intensity: - self.primary.enable_poll(polling_intensity) - - platform = import_module(f".{component}", __name__) - - device = platform.get_device( - node=self._node, values=self, node_config=node_config, hass=self._hass - ) - if device is None: - # No entity will be created for this value - self._workaround_ignore = True - return - - self._entity = device - - @callback - def _on_ready(sec): - _LOGGER.info( - "Z-Wave entity %s (node_id: %d) ready after %d seconds", - device.name, - self._node.node_id, - sec, - ) - self._hass.async_add_job(discover_device, component, device) - - @callback - def _on_timeout(sec): - _LOGGER.warning( - "Z-Wave entity %s (node_id: %d) not ready after %d seconds, " - "continuing anyway", - device.name, - self._node.node_id, - sec, - ) - self._hass.async_add_job(discover_device, component, device) - - async def discover_device(component, device): - """Put device in a dictionary and call discovery on it.""" - if self._hass.data[DATA_DEVICES].get(device.unique_id): - return - - self._hass.data[DATA_DEVICES][device.unique_id] = device - if component in PLATFORMS: - async_dispatcher_send(self._hass, f"zwave_new_{component}", device) - else: - await discovery.async_load_platform( - self._hass, - component, - DOMAIN, - {const.DISCOVERY_DEVICE: device.unique_id}, - self._zwave_config, - ) - - if device.unique_id: - self._hass.add_job(discover_device, component, device) - else: - self._hass.add_job(check_has_unique_id, device, _on_ready, _on_timeout) - - -class ZWaveDeviceEntity(ZWaveBaseEntity): - """Representation of a Z-Wave node entity.""" - - def __init__(self, values, domain): - """Initialize the z-Wave device.""" - super().__init__() - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - - self.values = values - self.node = values.primary.node - self.values.primary.set_change_verified(False) - - self._name = _value_name(self.values.primary) - self._unique_id = self._compute_unique_id() - self._update_attributes() - - dispatcher.connect( - self.network_value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED - ) - - def network_value_changed(self, value): - """Handle a value change on the network.""" - if value.value_id in [v.value_id for v in self.values if v]: - return self.value_changed() - - def value_added(self): - """Handle a new value of this entity.""" - - def value_changed(self): - """Handle a changed value for this entity's node.""" - self._update_attributes() - self.update_properties() - self.maybe_schedule_update() - - async def value_renamed(self, update_ids=False): - """Rename the node and update any IDs.""" - self._name = _value_name(self.values.primary) - if update_ids: - # Update entity ID. - ent_reg = await async_get_entity_registry(self.hass) - new_entity_id = ent_reg.async_generate_entity_id( - self.platform.domain, - self._name, - self.platform.entities.keys() - {self.entity_id}, - ) - if new_entity_id != self.entity_id: - # Don't change the name attribute, it will be None unless - # customised and if it's been customised, keep the - # customisation. - ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id) - return - # else for the above two ifs, update if not using update_entity - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Add device to dict.""" - async_dispatcher_connect( - self.hass, - SIGNAL_REFRESH_ENTITY_FORMAT.format(self.entity_id), - self.refresh_from_network, - ) - - # Add legacy Z-Wave migration data. - await async_add_migration_entity_value(self.hass, self.entity_id, self.values) - - def _update_attributes(self): - """Update the node attributes. May only be used inside callback.""" - self.node_id = self.node.node_id - self._name = _value_name(self.values.primary) - if not self._unique_id: - self._unique_id = self._compute_unique_id() - if self._unique_id: - self.try_remove_and_add() - - if self.values.power: - self.power_consumption = round( - self.values.power.data, self.values.power.precision - ) - else: - self.power_consumption = None - - def update_properties(self): - """Update on data changes for node values.""" - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - identifier, name = node_device_id_and_name( - self.node, self.values.primary.instance - ) - info = DeviceInfo( - name=name, - identifiers={identifier}, - manufacturer=self.node.manufacturer_name, - model=self.node.product_name, - ) - if self.values.primary.instance > 1: - info[ATTR_VIA_DEVICE] = (DOMAIN, self.node_id) - elif self.node_id > 1: - info[ATTR_VIA_DEVICE] = (DOMAIN, 1) - return info - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attrs = { - const.ATTR_NODE_ID: self.node_id, - const.ATTR_VALUE_INDEX: self.values.primary.index, - const.ATTR_VALUE_INSTANCE: self.values.primary.instance, - const.ATTR_VALUE_ID: str(self.values.primary.value_id), - } - - if self.power_consumption is not None: - attrs[ATTR_POWER] = self.power_consumption - - return attrs - - def refresh_from_network(self): - """Refresh all dependent values from zwave network.""" - for value in self.values: - if value is not None: - self.node.refresh_value(value.value_id) - - def _compute_unique_id(self): - if ( - is_node_parsed(self.node) and self.values.primary.label != "Unknown" - ) or self.node.is_ready: - return compute_value_unique_id(self.node, self.values.primary) - return None diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py deleted file mode 100644 index 26944b6661d..00000000000 --- a/homeassistant/components/zwave/binary_sensor.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Support for Z-Wave binary sensors.""" -import datetime -import logging - -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import track_point_in_time -import homeassistant.util.dt as dt_util - -from . import ZWaveDeviceEntity, workaround -from .const import COMMAND_CLASS_SENSOR_BINARY - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave binary sensors from Config Entry.""" - - @callback - def async_add_binary_sensor(binary_sensor): - """Add Z-Wave binary sensor.""" - async_add_entities([binary_sensor]) - - async_dispatcher_connect(hass, "zwave_new_binary_sensor", async_add_binary_sensor) - - -def get_device(values, **kwargs): - """Create Z-Wave entity device.""" - device_mapping = workaround.get_device_mapping(values.primary) - if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT: - return ZWaveTriggerSensor(values, "motion") - - if workaround.get_device_component_mapping(values.primary) == DOMAIN: - return ZWaveBinarySensor(values, None) - - if values.primary.command_class == COMMAND_CLASS_SENSOR_BINARY: - return ZWaveBinarySensor(values, None) - return None - - -class ZWaveBinarySensor(BinarySensorEntity, ZWaveDeviceEntity): - """Representation of a binary sensor within Z-Wave.""" - - def __init__(self, values, device_class): - """Initialize the sensor.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._sensor_type = device_class - self._state = self.values.primary.data - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the class of this sensor, from BinarySensorDeviceClass.""" - return self._sensor_type - - -class ZWaveTriggerSensor(ZWaveBinarySensor): - """Representation of a stateless sensor within Z-Wave.""" - - def __init__(self, values, device_class): - """Initialize the sensor.""" - super().__init__(values, device_class) - # Set default off delay to 60 sec - self.re_arm_sec = 60 - self.invalidate_after = None - - def update_properties(self): - """Handle value changes for this entity's node.""" - self._state = self.values.primary.data - _LOGGER.debug("off_delay=%s", self.values.off_delay) - # Set re_arm_sec if off_delay is provided from the sensor - if self.values.off_delay: - _LOGGER.debug("off_delay.data=%s", self.values.off_delay.data) - self.re_arm_sec = self.values.off_delay.data * 8 - # only allow this value to be true for re_arm secs - if not self.hass: - return - - self.invalidate_after = dt_util.utcnow() + datetime.timedelta( - seconds=self.re_arm_sec - ) - track_point_in_time( - self.hass, self.async_update_ha_state, self.invalidate_after - ) - - @property - def is_on(self): - """Return true if movement has happened within the rearm time.""" - return self._state and ( - self.invalidate_after is None or self.invalidate_after > dt_util.utcnow() - ) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py deleted file mode 100644 index d56910e1b74..00000000000 --- a/homeassistant/components/zwave/climate.py +++ /dev/null @@ -1,619 +0,0 @@ -"""Support for Z-Wave climate devices.""" -# Because we do not compile openzwave on CI -from __future__ import annotations - -import logging - -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_FAN, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - DOMAIN, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - PRESET_AWAY, - PRESET_BOOST, - PRESET_NONE, - SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, const - -_LOGGER = logging.getLogger(__name__) - -CONF_NAME = "name" -DEFAULT_NAME = "Z-Wave Climate" - -REMOTEC = 0x5254 -REMOTEC_ZXT_120 = 0x8377 -REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) -ATTR_OPERATING_STATE = "operating_state" -ATTR_FAN_STATE = "fan_state" -ATTR_FAN_ACTION = "fan_action" -AUX_HEAT_ZWAVE_MODE = "Aux Heat" - -# Device is in manufacturer specific mode (e.g. setting the valve manually) -PRESET_MANUFACTURER_SPECIFIC = "Manufacturer Specific" - -WORKAROUND_ZXT_120 = "zxt_120" - -DEVICE_MAPPINGS = {REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120} - -HVAC_STATE_MAPPINGS = { - "off": HVAC_MODE_OFF, - "heat": HVAC_MODE_HEAT, - "heat mode": HVAC_MODE_HEAT, - "heat (default)": HVAC_MODE_HEAT, - "furnace": HVAC_MODE_HEAT, - "fan only": HVAC_MODE_FAN_ONLY, - "dry air": HVAC_MODE_DRY, - "moist air": HVAC_MODE_DRY, - "cool": HVAC_MODE_COOL, - "heat_cool": HVAC_MODE_HEAT_COOL, - "auto": HVAC_MODE_HEAT_COOL, - "auto changeover": HVAC_MODE_HEAT_COOL, -} - -MODE_SETPOINT_MAPPINGS = { - "off": (), - "heat": ("setpoint_heating",), - "cool": ("setpoint_cooling",), - "auto": ("setpoint_heating", "setpoint_cooling"), - "aux heat": ("setpoint_heating",), - "furnace": ("setpoint_furnace",), - "dry air": ("setpoint_dry_air",), - "moist air": ("setpoint_moist_air",), - "auto changeover": ("setpoint_auto_changeover",), - "heat econ": ("setpoint_eco_heating",), - "cool econ": ("setpoint_eco_cooling",), - "away": ("setpoint_away_heating", "setpoint_away_cooling"), - "full power": ("setpoint_full_power",), - # aliases found in xml configs - "comfort": ("setpoint_heating",), - "heat mode": ("setpoint_heating",), - "heat (default)": ("setpoint_heating",), - "dry floor": ("setpoint_dry_air",), - "heat eco": ("setpoint_eco_heating",), - "energy saving": ("setpoint_eco_heating",), - "energy heat": ("setpoint_eco_heating",), - "vacation": ("setpoint_away_heating", "setpoint_away_cooling"), - # for tests - "heat_cool": ("setpoint_heating", "setpoint_cooling"), -} - -HVAC_CURRENT_MAPPINGS = { - "idle": CURRENT_HVAC_IDLE, - "heat": CURRENT_HVAC_HEAT, - "pending heat": CURRENT_HVAC_IDLE, - "heating": CURRENT_HVAC_HEAT, - "cool": CURRENT_HVAC_COOL, - "pending cool": CURRENT_HVAC_IDLE, - "cooling": CURRENT_HVAC_COOL, - "fan only": CURRENT_HVAC_FAN, - "vent / economiser": CURRENT_HVAC_FAN, - "off": CURRENT_HVAC_OFF, -} - -PRESET_MAPPINGS = { - "away": PRESET_AWAY, - "full power": PRESET_BOOST, - "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, -} - -DEFAULT_HVAC_MODES = [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_DRY, - HVAC_MODE_OFF, - HVAC_MODE_AUTO, -] - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Climate device from Config Entry.""" - - @callback - def async_add_climate(climate): - """Add Z-Wave Climate Device.""" - async_add_entities([climate]) - - async_dispatcher_connect(hass, "zwave_new_climate", async_add_climate) - - -def get_device(hass, values, **kwargs): - """Create Z-Wave entity device.""" - temp_unit = hass.config.units.temperature_unit - if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_SETPOINT: - return ZWaveClimateSingleSetpoint(values, temp_unit) - if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_MODE: - return ZWaveClimateMultipleSetpoint(values, temp_unit) - return None - - -class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): - """Representation of a Z-Wave Climate device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._target_temperature = None - self._target_temperature_range = (None, None) - self._current_temperature = None - self._hvac_action = None - self._hvac_list = None # [zwave_mode] - self._hvac_mapping = None # {ha_mode:zwave_mode} - self._hvac_mode = None # ha_mode - self._aux_heat = None - self._default_hvac_mode = None # ha_mode - self._preset_mapping = None # {ha_mode:zwave_mode} - self._preset_list = None # [zwave_mode] - self._preset_mode = None # ha_mode if exists, else zwave_mode - self._current_fan_mode = None - self._fan_modes = None - self._fan_action = None - self._current_swing_mode = None - self._swing_modes = None - self._unit = temp_unit - _LOGGER.debug("temp_unit is %s", self._unit) - self._zxt_120 = None - # Make sure that we have values for the key before converting to int - if self.node.manufacturer_id.strip() and self.node.product_id.strip(): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16), - ) - if ( - specific_sensor_key in DEVICE_MAPPINGS - and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120 - ): - _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat workaround") - self._zxt_120 = 1 - self.update_properties() - - def _mode(self) -> None: - """Return thermostat mode Z-Wave value.""" - raise NotImplementedError() - - def _current_mode_setpoints(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - raise NotImplementedError() - - @property - def supported_features(self): - """Return the list of supported features.""" - support = SUPPORT_TARGET_TEMPERATURE - if self._hvac_list and HVAC_MODE_HEAT_COOL in self._hvac_list: - support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self._preset_list and PRESET_AWAY in self._preset_list: - support |= SUPPORT_TARGET_TEMPERATURE_RANGE - - if self.values.fan_mode: - support |= SUPPORT_FAN_MODE - if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: - support |= SUPPORT_SWING_MODE - if self._aux_heat: - support |= SUPPORT_AUX_HEAT - if self._preset_list: - support |= SUPPORT_PRESET_MODE - return support - - def update_properties(self): - """Handle the data changes for node values.""" - # Operation Mode - self._update_operation_mode() - - # Current Temp - self._update_current_temp() - - # Fan Mode - self._update_fan_mode() - - # Swing mode - self._update_swing_mode() - - # Set point - self._update_target_temp() - - # Operating state - self._update_operating_state() - - # Fan operating state - self._update_fan_state() - - def _update_operation_mode(self): - """Update hvac and preset modes.""" - if self._mode(): - self._hvac_list = [] - self._hvac_mapping = {} - self._preset_list = [] - self._preset_mapping = {} - - if mode_list := self._mode().data_items: - for mode in mode_list: - ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) - ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) - if mode == AUX_HEAT_ZWAVE_MODE: - # Aux Heat should not be included in any mapping - self._aux_heat = True - elif ha_mode and ha_mode not in self._hvac_mapping: - self._hvac_mapping[ha_mode] = mode - self._hvac_list.append(ha_mode) - elif ha_preset and ha_preset not in self._preset_mapping: - self._preset_mapping[ha_preset] = mode - self._preset_list.append(ha_preset) - else: - # If nothing matches - self._preset_list.append(mode) - - # Default operation mode - for mode in DEFAULT_HVAC_MODES: - if mode in self._hvac_mapping: - self._default_hvac_mode = mode - break - - if self._preset_list: - # Presets are supported - self._preset_list.append(PRESET_NONE) - - current_mode = self._mode().data - _LOGGER.debug("current_mode=%s", current_mode) - _hvac_temp = next( - ( - key - for key, value in self._hvac_mapping.items() - if value == current_mode - ), - None, - ) - - if _hvac_temp is None: - # The current mode is not a hvac mode - if ( - "heat" in current_mode.lower() - and HVAC_MODE_HEAT in self._hvac_mapping - ): - # The current preset modes maps to HVAC_MODE_HEAT - _LOGGER.debug("Mapped to HEAT") - self._hvac_mode = HVAC_MODE_HEAT - elif ( - "cool" in current_mode.lower() - and HVAC_MODE_COOL in self._hvac_mapping - ): - # The current preset modes maps to HVAC_MODE_COOL - _LOGGER.debug("Mapped to COOL") - self._hvac_mode = HVAC_MODE_COOL - else: - # The current preset modes maps to self._default_hvac_mode - _LOGGER.debug("Mapped to DEFAULT") - self._hvac_mode = self._default_hvac_mode - self._preset_mode = next( - ( - key - for key, value in self._preset_mapping.items() - if value == current_mode - ), - current_mode, - ) - else: - # The current mode is a hvac mode - self._hvac_mode = _hvac_temp - self._preset_mode = PRESET_NONE - - _LOGGER.debug("self._hvac_mapping=%s", self._hvac_mapping) - _LOGGER.debug("self._hvac_list=%s", self._hvac_list) - _LOGGER.debug("self._hvac_mode=%s", self._hvac_mode) - _LOGGER.debug("self._default_hvac_mode=%s", self._default_hvac_mode) - _LOGGER.debug("self._hvac_action=%s", self._hvac_action) - _LOGGER.debug("self._aux_heat=%s", self._aux_heat) - _LOGGER.debug("self._preset_mapping=%s", self._preset_mapping) - _LOGGER.debug("self._preset_list=%s", self._preset_list) - _LOGGER.debug("self._preset_mode=%s", self._preset_mode) - - def _update_current_temp(self): - """Update current temperature.""" - if self.values.temperature: - self._current_temperature = self.values.temperature.data - device_unit = self.values.temperature.units - if device_unit is not None: - self._unit = device_unit - - def _update_fan_mode(self): - """Update fan mode.""" - if self.values.fan_mode: - self._current_fan_mode = self.values.fan_mode.data - if fan_modes := self.values.fan_mode.data_items: - self._fan_modes = list(fan_modes) - - _LOGGER.debug("self._fan_modes=%s", self._fan_modes) - _LOGGER.debug("self._current_fan_mode=%s", self._current_fan_mode) - - def _update_swing_mode(self): - """Update swing mode.""" - if self._zxt_120 == 1: - if self.values.zxt_120_swing_mode: - self._current_swing_mode = self.values.zxt_120_swing_mode.data - swing_modes = self.values.zxt_120_swing_mode.data_items - if swing_modes: - self._swing_modes = list(swing_modes) - _LOGGER.debug("self._swing_modes=%s", self._swing_modes) - _LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode) - - def _update_target_temp(self): - """Update target temperature.""" - current_setpoints = self._current_mode_setpoints() - self._target_temperature = None - self._target_temperature_range = (None, None) - if len(current_setpoints) == 1: - (setpoint,) = current_setpoints - if setpoint is not None: - self._target_temperature = round((float(setpoint.data)), 1) - elif len(current_setpoints) == 2: - (setpoint_low, setpoint_high) = current_setpoints - target_low, target_high = None, None - if setpoint_low is not None: - target_low = round((float(setpoint_low.data)), 1) - if setpoint_high is not None: - target_high = round((float(setpoint_high.data)), 1) - self._target_temperature_range = (target_low, target_high) - - def _update_operating_state(self): - """Update operating state.""" - if self.values.operating_state: - mode = self.values.operating_state.data - self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) - - def _update_fan_state(self): - """Update fan state.""" - if self.values.fan_action: - self._fan_action = self.values.fan_action.data - - @property - def fan_mode(self): - """Return the fan speed set.""" - return self._current_fan_mode - - @property - def fan_modes(self): - """Return a list of available fan modes.""" - return self._fan_modes - - @property - def swing_mode(self): - """Return the swing mode set.""" - return self._current_swing_mode - - @property - def swing_modes(self): - """Return a list of available swing modes.""" - return self._swing_modes - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - if self._unit == "C": - return TEMP_CELSIUS - if self._unit == "F": - return TEMP_FAHRENHEIT - return self._unit - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature - - @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if self._mode(): - return self._hvac_mode - return self._default_hvac_mode - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - if self._mode(): - return self._hvac_list - return [] - - @property - def hvac_action(self): - """Return the current running hvac operation if supported. - - Need to be one of CURRENT_HVAC_*. - """ - return self._hvac_action - - @property - def is_aux_heat(self): - """Return true if aux heater.""" - if not self._aux_heat: - return None - if self._mode().data == AUX_HEAT_ZWAVE_MODE: - return True - return False - - @property - def preset_mode(self): - """Return preset operation ie. eco, away. - - Need to be one of PRESET_*. - """ - if self._mode(): - return self._preset_mode - return PRESET_NONE - - @property - def preset_modes(self): - """Return the list of available preset operation modes. - - Need to be a subset of PRESET_MODES. - """ - if self._mode(): - return self._preset_list - return [] - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def target_temperature_low(self) -> float | None: - """Return the lowbound target temperature we try to reach.""" - return self._target_temperature_range[0] - - @property - def target_temperature_high(self) -> float | None: - """Return the highbound target temperature we try to reach.""" - return self._target_temperature_range[1] - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - current_setpoints = self._current_mode_setpoints() - if len(current_setpoints) == 1: - (setpoint,) = current_setpoints - target_temp = kwargs.get(ATTR_TEMPERATURE) - if setpoint is not None and target_temp is not None: - _LOGGER.debug("Set temperature to %s", target_temp) - setpoint.data = target_temp - elif len(current_setpoints) == 2: - (setpoint_low, setpoint_high) = current_setpoints - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if setpoint_low is not None and target_temp_low is not None: - _LOGGER.debug("Set low temperature to %s", target_temp_low) - setpoint_low.data = target_temp_low - if setpoint_high is not None and target_temp_high is not None: - _LOGGER.debug("Set high temperature to %s", target_temp_high) - setpoint_high.data = target_temp_high - - def set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - _LOGGER.debug("Set fan mode to %s", fan_mode) - if not self.values.fan_mode: - return - self.values.fan_mode.data = fan_mode - - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - _LOGGER.debug("Set hvac_mode to %s", hvac_mode) - if not self._mode(): - return - operation_mode = self._hvac_mapping.get(hvac_mode) - _LOGGER.debug("Set operation_mode to %s", operation_mode) - self._mode().data = operation_mode - - def turn_aux_heat_on(self): - """Turn auxiliary heater on.""" - if not self._aux_heat: - return - operation_mode = AUX_HEAT_ZWAVE_MODE - _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self._mode().data = operation_mode - - def turn_aux_heat_off(self): - """Turn auxiliary heater off.""" - if not self._aux_heat: - return - if HVAC_MODE_HEAT in self._hvac_mapping: - operation_mode = self._hvac_mapping.get(HVAC_MODE_HEAT) - else: - operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) - _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self._mode().data = operation_mode - - def set_preset_mode(self, preset_mode): - """Set new target preset mode.""" - _LOGGER.debug("Set preset_mode to %s", preset_mode) - if not self._mode(): - return - if preset_mode == PRESET_NONE: - # Activate the current hvac mode - self._update_operation_mode() - operation_mode = self._hvac_mapping.get(self.hvac_mode) - _LOGGER.debug("Set operation_mode to %s", operation_mode) - self._mode().data = operation_mode - else: - operation_mode = self._preset_mapping.get(preset_mode, preset_mode) - _LOGGER.debug("Set operation_mode to %s", operation_mode) - self._mode().data = operation_mode - - def set_swing_mode(self, swing_mode): - """Set new target swing mode.""" - _LOGGER.debug("Set swing_mode to %s", swing_mode) - if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: - self.values.zxt_120_swing_mode.data = swing_mode - - @property - def extra_state_attributes(self): - """Return the optional state attributes.""" - data = super().extra_state_attributes - if self._fan_action: - data[ATTR_FAN_ACTION] = self._fan_action - return data - - -class ZWaveClimateSingleSetpoint(ZWaveClimateBase): - """Representation of a single setpoint Z-Wave thermostat device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveClimateBase.__init__(self, values, temp_unit) - - def _mode(self) -> None: - """Return thermostat mode Z-Wave value.""" - return self.values.mode - - def _current_mode_setpoints(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - return (self.values.primary,) - - -class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): - """Representation of a multiple setpoint Z-Wave thermostat device.""" - - def __init__(self, values, temp_unit): - """Initialize the Z-Wave climate device.""" - ZWaveClimateBase.__init__(self, values, temp_unit) - - def _mode(self) -> None: - """Return thermostat mode Z-Wave value.""" - return self.values.primary - - def _current_mode_setpoints(self) -> tuple: - """Return a tuple of current setpoint Z-Wave value(s).""" - current_mode = str(self.values.primary.data).lower() - setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - return tuple(getattr(self.values, name, None) for name in setpoints_names) diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py deleted file mode 100644 index f29f2e6f6d0..00000000000 --- a/homeassistant/components/zwave/config_flow.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Config flow to configure Z-Wave.""" -# pylint: disable=import-error -# pylint: disable=import-outside-toplevel -from collections import OrderedDict - -import voluptuous as vol - -from homeassistant import config_entries - -from .const import ( - CONF_NETWORK_KEY, - CONF_USB_STICK_PATH, - DEFAULT_CONF_USB_STICK_PATH, - DOMAIN, -) - - -class ZwaveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a Z-Wave config flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize the Z-Wave config flow.""" - self.usb_path = CONF_USB_STICK_PATH - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - errors = {} - - fields = OrderedDict() - fields[ - vol.Required(CONF_USB_STICK_PATH, default=DEFAULT_CONF_USB_STICK_PATH) - ] = str - fields[vol.Optional(CONF_NETWORK_KEY)] = str - - if user_input is not None: - # Check if USB path is valid - from openzwave.object import ZWaveException - from openzwave.option import ZWaveOption - - try: - from functools import partial - - option = await self.hass.async_add_executor_job( # noqa: F841 pylint: disable=unused-variable - partial( - ZWaveOption, - user_input[CONF_USB_STICK_PATH], - user_path=self.hass.config.config_dir, - ) - ) - except ZWaveException: - errors["base"] = "option_error" - return self.async_show_form( - step_id="user", data_schema=vol.Schema(fields), errors=errors - ) - - if user_input.get(CONF_NETWORK_KEY) is None: - # Generate a random key - from random import choice - - key = "" - for i in range(16): - key += "0x" - key += choice("1234567890ABCDEF") - key += choice("1234567890ABCDEF") - if i < 15: - key += ", " - user_input[CONF_NETWORK_KEY] = key - - return self.async_create_entry( - title="Z-Wave", - data={ - CONF_USB_STICK_PATH: user_input[CONF_USB_STICK_PATH], - CONF_NETWORK_KEY: user_input[CONF_NETWORK_KEY], - }, - ) - - return self.async_show_form(step_id="user", data_schema=vol.Schema(fields)) - - async def async_step_import(self, info): - """Import existing configuration from Z-Wave.""" - if self._async_current_entries(): - return self.async_abort(reason="already_setup") - - return self.async_create_entry( - title="Z-Wave (import from configuration.yaml)", - data={ - CONF_USB_STICK_PATH: info.get(CONF_USB_STICK_PATH), - CONF_NETWORK_KEY: info.get(CONF_NETWORK_KEY), - }, - ) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py deleted file mode 100644 index d11d308c490..00000000000 --- a/homeassistant/components/zwave/const.py +++ /dev/null @@ -1,395 +0,0 @@ -"""Z-Wave Constants.""" -DOMAIN = "zwave" - -ATTR_NODE_ID = "node_id" -ATTR_TARGET_NODE_ID = "target_node_id" -ATTR_ASSOCIATION = "association" -ATTR_INSTANCE = "instance" -ATTR_GROUP = "group" -ATTR_VALUE_ID = "value_id" -ATTR_MESSAGES = "messages" -ATTR_RETURN_ROUTES = "return_routes" -ATTR_SCENE_ID = "scene_id" -ATTR_SCENE_DATA = "scene_data" -ATTR_BASIC_LEVEL = "basic_level" -ATTR_CONFIG_PARAMETER = "parameter" -ATTR_CONFIG_SIZE = "size" -ATTR_CONFIG_VALUE = "value" -ATTR_POLL_INTENSITY = "poll_intensity" -ATTR_VALUE_INDEX = "value_index" -ATTR_VALUE_INSTANCE = "value_instance" -ATTR_UPDATE_IDS = "update_ids" -NETWORK_READY_WAIT_SECS = 300 -NODE_READY_WAIT_SECS = 30 - -CONF_AUTOHEAL = "autoheal" -CONF_DEBUG = "debug" -CONF_POLLING_INTERVAL = "polling_interval" -CONF_USB_STICK_PATH = "usb_path" -CONF_CONFIG_PATH = "config_path" -CONF_NETWORK_KEY = "network_key" - -DEFAULT_CONF_AUTOHEAL = False -DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" -DEFAULT_POLLING_INTERVAL = 60000 -DEFAULT_DEBUG = False - -DISCOVERY_DEVICE = "device" - -DATA_DEVICES = "zwave_devices" -DATA_NETWORK = "zwave_network" -DATA_ENTITY_VALUES = "zwave_entity_values" -DATA_ZWAVE_CONFIG = "zwave_config" - -SERVICE_CHANGE_ASSOCIATION = "change_association" -SERVICE_ADD_NODE = "add_node" -SERVICE_ADD_NODE_SECURE = "add_node_secure" -SERVICE_REMOVE_NODE = "remove_node" -SERVICE_CANCEL_COMMAND = "cancel_command" -SERVICE_HEAL_NETWORK = "heal_network" -SERVICE_HEAL_NODE = "heal_node" -SERVICE_SOFT_RESET = "soft_reset" -SERVICE_TEST_NODE = "test_node" -SERVICE_TEST_NETWORK = "test_network" -SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" -SERVICE_SET_NODE_VALUE = "set_node_value" -SERVICE_REFRESH_NODE_VALUE = "refresh_node_value" -SERVICE_PRINT_CONFIG_PARAMETER = "print_config_parameter" -SERVICE_PRINT_NODE = "print_node" -SERVICE_REMOVE_FAILED_NODE = "remove_failed_node" -SERVICE_REPLACE_FAILED_NODE = "replace_failed_node" -SERVICE_SET_POLL_INTENSITY = "set_poll_intensity" -SERVICE_SET_WAKEUP = "set_wakeup" -SERVICE_STOP_NETWORK = "stop_network" -SERVICE_START_NETWORK = "start_network" -SERVICE_RENAME_NODE = "rename_node" -SERVICE_RENAME_VALUE = "rename_value" -SERVICE_REFRESH_ENTITY = "refresh_entity" -SERVICE_REFRESH_NODE = "refresh_node" -SERVICE_RESET_NODE_METERS = "reset_node_meters" - -EVENT_SCENE_ACTIVATED = "zwave.scene_activated" -EVENT_NODE_EVENT = "zwave.node_event" -EVENT_NETWORK_READY = "zwave.network_ready" -EVENT_NETWORK_COMPLETE = "zwave.network_complete" -EVENT_NETWORK_COMPLETE_SOME_DEAD = "zwave.network_complete_some_dead" -EVENT_NETWORK_START = "zwave.network_start" -EVENT_NETWORK_STOP = "zwave.network_stop" - -COMMAND_CLASS_ALARM = 113 -COMMAND_CLASS_ANTITHEFT = 93 -COMMAND_CLASS_APPLICATION_CAPABILITY = 87 -COMMAND_CLASS_APPLICATION_STATUS = 34 -COMMAND_CLASS_ASSOCIATION = 133 -COMMAND_CLASS_ASSOCIATION_COMMAND_CONFIGURATION = 155 -COMMAND_CLASS_ASSOCIATION_GRP_INFO = 89 -COMMAND_CLASS_BARRIER_OPERATOR = 102 -COMMAND_CLASS_BASIC = 32 -COMMAND_CLASS_BASIC_TARIFF_INFO = 54 -COMMAND_CLASS_BASIC_WINDOW_COVERING = 80 -COMMAND_CLASS_BATTERY = 128 -COMMAND_CLASS_CENTRAL_SCENE = 91 -COMMAND_CLASS_CLIMATE_CONTROL_SCHEDULE = 70 -COMMAND_CLASS_CLOCK = 129 -COMMAND_CLASS_CONFIGURATION = 112 -COMMAND_CLASS_CONTROLLER_REPLICATION = 33 -COMMAND_CLASS_CRC_16_ENCAP = 86 -COMMAND_CLASS_DCP_CONFIG = 58 -COMMAND_CLASS_DCP_MONITOR = 59 -COMMAND_CLASS_DEVICE_RESET_LOCALLY = 90 -COMMAND_CLASS_DOOR_LOCK = 98 -COMMAND_CLASS_DOOR_LOCK_LOGGING = 76 -COMMAND_CLASS_ENERGY_PRODUCTION = 144 -COMMAND_CLASS_ENTRY_CONTROL = 111 -COMMAND_CLASS_FIRMWARE_UPDATE_MD = 122 -COMMAND_CLASS_GEOGRAPHIC_LOCATION = 140 -COMMAND_CLASS_GROUPING_NAME = 123 -COMMAND_CLASS_HAIL = 130 -COMMAND_CLASS_HRV_CONTROL = 57 -COMMAND_CLASS_HRV_STATUS = 55 -COMMAND_CLASS_HUMIDITY_CONTROL_MODE = 109 -COMMAND_CLASS_HUMIDITY_CONTROL_OPERATING_STATE = 110 -COMMAND_CLASS_HUMIDITY_CONTROL_SETPOINT = 100 -COMMAND_CLASS_INDICATOR = 135 -COMMAND_CLASS_IP_ASSOCIATION = 92 -COMMAND_CLASS_IP_CONFIGURATION = 14 -COMMAND_CLASS_IRRIGATION = 107 -COMMAND_CLASS_LANGUAGE = 137 -COMMAND_CLASS_LOCK = 118 -COMMAND_CLASS_MAILBOX = 105 -COMMAND_CLASS_MANUFACTURER_PROPRIETARY = 145 -COMMAND_CLASS_MANUFACTURER_SPECIFIC = 114 -COMMAND_CLASS_MARK = 239 -COMMAND_CLASS_METER = 50 -COMMAND_CLASS_METER_PULSE = 53 -COMMAND_CLASS_METER_TBL_CONFIG = 60 -COMMAND_CLASS_METER_TBL_MONITOR = 61 -COMMAND_CLASS_METER_TBL_PUSH = 62 -COMMAND_CLASS_MTP_WINDOW_COVERING = 81 -COMMAND_CLASS_MULTI_CHANNEL = 96 -COMMAND_CLASS_MULTI_CHANNEL_ASSOCIATION = 142 -COMMAND_CLASS_MULTI_COMMAND = 143 -COMMAND_CLASS_NETWORK_MANAGEMENT_BASIC = 77 -COMMAND_CLASS_NETWORK_MANAGEMENT_INCLUSION = 52 -COMMAND_CLASS_NETWORK_MANAGEMENT_PRIMARY = 84 -COMMAND_CLASS_NETWORK_MANAGEMENT_PROXY = 82 -COMMAND_CLASS_NO_OPERATION = 0 -COMMAND_CLASS_NODE_NAMING = 119 -COMMAND_CLASS_NON_INTEROPERABLE = 240 -COMMAND_CLASS_NOTIFICATION = 113 -COMMAND_CLASS_POWERLEVEL = 115 -COMMAND_CLASS_PREPAYMENT = 63 -COMMAND_CLASS_PREPAYMENT_ENCAPSULATION = 65 -COMMAND_CLASS_PROPRIETARY = 136 -COMMAND_CLASS_PROTECTION = 117 -COMMAND_CLASS_RATE_TBL_CONFIG = 72 -COMMAND_CLASS_RATE_TBL_MONITOR = 73 -COMMAND_CLASS_REMOTE_ASSOCIATION_ACTIVATE = 124 -COMMAND_CLASS_REMOTE_ASSOCIATION = 125 -COMMAND_CLASS_SCENE_ACTIVATION = 43 -COMMAND_CLASS_SCENE_ACTUATOR_CONF = 44 -COMMAND_CLASS_SCENE_CONTROLLER_CONF = 45 -COMMAND_CLASS_SCHEDULE = 83 -COMMAND_CLASS_SCHEDULE_ENTRY_LOCK = 78 -COMMAND_CLASS_SCREEN_ATTRIBUTES = 147 -COMMAND_CLASS_SCREEN_MD = 146 -COMMAND_CLASS_SECURITY = 152 -COMMAND_CLASS_SECURITY_SCHEME0_MARK = 61696 -COMMAND_CLASS_SENSOR_ALARM = 156 -COMMAND_CLASS_SENSOR_BINARY = 48 -COMMAND_CLASS_SENSOR_CONFIGURATION = 158 -COMMAND_CLASS_SENSOR_MULTILEVEL = 49 -COMMAND_CLASS_SILENCE_ALARM = 157 -COMMAND_CLASS_SIMPLE_AV_CONTROL = 148 -COMMAND_CLASS_SUPERVISION = 108 -COMMAND_CLASS_SWITCH_ALL = 39 -COMMAND_CLASS_SWITCH_BINARY = 37 -COMMAND_CLASS_SWITCH_COLOR = 51 -COMMAND_CLASS_SWITCH_MULTILEVEL = 38 -COMMAND_CLASS_SWITCH_TOGGLE_BINARY = 40 -COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL = 41 -COMMAND_CLASS_TARIFF_TBL_CONFIG = 74 -COMMAND_CLASS_TARIFF_TBL_MONITOR = 75 -COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 -COMMAND_CLASS_THERMOSTAT_FAN_ACTION = 69 -COMMAND_CLASS_THERMOSTAT_MODE = 64 -COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 66 -COMMAND_CLASS_THERMOSTAT_SETBACK = 71 -COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 -COMMAND_CLASS_TIME = 138 -COMMAND_CLASS_TIME_PARAMETERS = 139 -COMMAND_CLASS_TRANSPORT_SERVICE = 85 -COMMAND_CLASS_USER_CODE = 99 -COMMAND_CLASS_VERSION = 134 -COMMAND_CLASS_WAKE_UP = 132 -COMMAND_CLASS_ZIP = 35 -COMMAND_CLASS_ZIP_NAMING = 104 -COMMAND_CLASS_ZIP_ND = 88 -COMMAND_CLASS_ZIP_6LOWPAN = 79 -COMMAND_CLASS_ZIP_GATEWAY = 95 -COMMAND_CLASS_ZIP_PORTAL = 97 -COMMAND_CLASS_ZWAVEPLUS_INFO = 94 -COMMAND_CLASS_WHATEVER = None # Match ALL -COMMAND_CLASS_WINDOW_COVERING = 106 - -GENERIC_TYPE_WHATEVER = None # Match ALL -SPECIFIC_TYPE_WHATEVER = None # Match ALL -SPECIFIC_TYPE_NOT_USED = 0 # Available in all Generic types - -GENERIC_TYPE_AV_CONTROL_POINT = 3 -SPECIFIC_TYPE_DOORBELL = 18 -SPECIFIC_TYPE_SATELLITE_RECEIVER = 4 -SPECIFIC_TYPE_SATELLITE_RECEIVER_V2 = 17 - -GENERIC_TYPE_DISPLAY = 4 -SPECIFIC_TYPE_SIMPLE_DISPLAY = 1 - -GENERIC_TYPE_ENTRY_CONTROL = 64 -SPECIFIC_TYPE_DOOR_LOCK = 1 -SPECIFIC_TYPE_ADVANCED_DOOR_LOCK = 2 -SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK = 3 -SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK_DEADBOLT = 4 -SPECIFIC_TYPE_SECURE_DOOR = 5 -SPECIFIC_TYPE_SECURE_GATE = 6 -SPECIFIC_TYPE_SECURE_BARRIER_ADDON = 7 -SPECIFIC_TYPE_SECURE_BARRIER_OPEN_ONLY = 8 -SPECIFIC_TYPE_SECURE_BARRIER_CLOSE_ONLY = 9 -SPECIFIC_TYPE_SECURE_LOCKBOX = 10 -SPECIFIC_TYPE_SECURE_KEYPAD = 11 - -GENERIC_TYPE_GENERIC_CONTROLLER = 1 -SPECIFIC_TYPE_PORTABLE_CONTROLLER = 1 -SPECIFIC_TYPE_PORTABLE_SCENE_CONTROLLER = 2 -SPECIFIC_TYPE_PORTABLE_INSTALLER_TOOL = 3 -SPECIFIC_TYPE_REMOTE_CONTROL_AV = 4 -SPECIFIC_TYPE_REMOTE_CONTROL_SIMPLE = 6 - -GENERIC_TYPE_METER = 49 -SPECIFIC_TYPE_SIMPLE_METER = 1 -SPECIFIC_TYPE_ADV_ENERGY_CONTROL = 2 -SPECIFIC_TYPE_WHOLE_HOME_METER_SIMPLE = 3 - -GENERIC_TYPE_METER_PULSE = 48 - -GENERIC_TYPE_NON_INTEROPERABLE = 255 - -GENERIC_TYPE_REPEATER_SLAVE = 15 -SPECIFIC_TYPE_REPEATER_SLAVE = 1 -SPECIFIC_TYPE_VIRTUAL_NODE = 2 - -GENERIC_TYPE_SECURITY_PANEL = 23 -SPECIFIC_TYPE_ZONED_SECURITY_PANEL = 1 - -GENERIC_TYPE_SEMI_INTEROPERABLE = 80 -SPECIFIC_TYPE_ENERGY_PRODUCTION = 1 - -GENERIC_TYPE_SENSOR_ALARM = 161 -SPECIFIC_TYPE_ADV_ZENSOR_NET_ALARM_SENSOR = 5 -SPECIFIC_TYPE_ADV_ZENSOR_NET_SMOKE_SENSOR = 10 -SPECIFIC_TYPE_BASIC_ROUTING_ALARM_SENSOR = 1 -SPECIFIC_TYPE_BASIC_ROUTING_SMOKE_SENSOR = 6 -SPECIFIC_TYPE_BASIC_ZENSOR_NET_ALARM_SENSOR = 3 -SPECIFIC_TYPE_BASIC_ZENSOR_NET_SMOKE_SENSOR = 8 -SPECIFIC_TYPE_ROUTING_ALARM_SENSOR = 2 -SPECIFIC_TYPE_ROUTING_SMOKE_SENSOR = 7 -SPECIFIC_TYPE_ZENSOR_NET_ALARM_SENSOR = 4 -SPECIFIC_TYPE_ZENSOR_NET_SMOKE_SENSOR = 9 -SPECIFIC_TYPE_ALARM_SENSOR = 11 - -GENERIC_TYPE_SENSOR_BINARY = 32 -SPECIFIC_TYPE_ROUTING_SENSOR_BINARY = 1 - -GENERIC_TYPE_SENSOR_MULTILEVEL = 33 -SPECIFIC_TYPE_ROUTING_SENSOR_MULTILEVEL = 1 -SPECIFIC_TYPE_CHIMNEY_FAN = 2 - -GENERIC_TYPE_STATIC_CONTROLLER = 2 -SPECIFIC_TYPE_PC_CONTROLLER = 1 -SPECIFIC_TYPE_SCENE_CONTROLLER = 2 -SPECIFIC_TYPE_STATIC_INSTALLER_TOOL = 3 -SPECIFIC_TYPE_SET_TOP_BOX = 4 -SPECIFIC_TYPE_SUB_SYSTEM_CONTROLLER = 5 -SPECIFIC_TYPE_TV = 6 -SPECIFIC_TYPE_GATEWAY = 7 - -GENERIC_TYPE_SWITCH_BINARY = 16 -SPECIFIC_TYPE_POWER_SWITCH_BINARY = 1 -SPECIFIC_TYPE_SCENE_SWITCH_BINARY = 3 -SPECIFIC_TYPE_POWER_STRIP = 4 -SPECIFIC_TYPE_SIREN = 5 -SPECIFIC_TYPE_VALVE_OPEN_CLOSE = 6 -SPECIFIC_TYPE_COLOR_TUNABLE_BINARY = 2 -SPECIFIC_TYPE_IRRIGATION_CONTROLLER = 7 - -GENERIC_TYPE_SWITCH_MULTILEVEL = 17 -SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL = 5 -SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL = 6 -SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL = 7 -SPECIFIC_TYPE_MOTOR_MULTIPOSITION = 3 -SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL = 1 -SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL = 4 -SPECIFIC_TYPE_FAN_SWITCH = 8 -SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL = 2 - -GENERIC_TYPE_SWITCH_REMOTE = 18 -SPECIFIC_TYPE_REMOTE_BINARY = 1 -SPECIFIC_TYPE_REMOTE_MULTILEVEL = 2 -SPECIFIC_TYPE_REMOTE_TOGGLE_BINARY = 3 -SPECIFIC_TYPE_REMOTE_TOGGLE_MULTILEVEL = 4 - -GENERIC_TYPE_SWITCH_TOGGLE = 19 -SPECIFIC_TYPE_SWITCH_TOGGLE_BINARY = 1 -SPECIFIC_TYPE_SWITCH_TOGGLE_MULTILEVEL = 2 - -GENERIC_TYPE_THERMOSTAT = 8 -SPECIFIC_TYPE_SETBACK_SCHEDULE_THERMOSTAT = 3 -SPECIFIC_TYPE_SETBACK_THERMOSTAT = 5 -SPECIFIC_TYPE_SETPOINT_THERMOSTAT = 4 -SPECIFIC_TYPE_THERMOSTAT_GENERAL = 2 -SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2 = 6 -SPECIFIC_TYPE_THERMOSTAT_HEATING = 1 - -GENERIC_TYPE_VENTILATION = 22 -SPECIFIC_TYPE_RESIDENTIAL_HRV = 1 - -GENERIC_TYPE_WINDOWS_COVERING = 9 -SPECIFIC_TYPE_SIMPLE_WINDOW_COVERING = 1 - -GENERIC_TYPE_ZIP_NODE = 21 -SPECIFIC_TYPE_ZIP_ADV_NODE = 2 -SPECIFIC_TYPE_ZIP_TUN_NODE = 1 - -GENERIC_TYPE_WALL_CONTROLLER = 24 -SPECIFIC_TYPE_BASIC_WALL_CONTROLLER = 1 - -GENERIC_TYPE_NETWORK_EXTENDER = 5 -SPECIFIC_TYPE_SECURE_EXTENDER = 1 - -GENERIC_TYPE_APPLIANCE = 6 -SPECIFIC_TYPE_GENERAL_APPLIANCE = 1 -SPECIFIC_TYPE_KITCHEN_APPLIANCE = 2 -SPECIFIC_TYPE_LAUNDRY_APPLIANCE = 3 - -GENERIC_TYPE_SENSOR_NOTIFICATION = 7 -SPECIFIC_TYPE_NOTIFICATION_SENSOR = 1 - -GENRE_WHATEVER = None -GENRE_USER = "User" -GENRE_SYSTEM = "System" - -TYPE_WHATEVER = None -TYPE_BYTE = "Byte" -TYPE_BOOL = "Bool" -TYPE_DECIMAL = "Decimal" -TYPE_INT = "Int" -TYPE_LIST = "List" -TYPE_STRING = "String" -TYPE_BUTTON = "Button" - -DISC_COMMAND_CLASS = "command_class" -DISC_COMPONENT = "component" -DISC_GENERIC_DEVICE_CLASS = "generic_device_class" -DISC_GENRE = "genre" -DISC_INDEX = "index" -DISC_INSTANCE = "instance" -DISC_NODE_ID = "node_id" -DISC_OPTIONAL = "optional" -DISC_PRIMARY = "primary" -DISC_SCHEMAS = "schemas" -DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" -DISC_TYPE = "type" -DISC_VALUES = "values" - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L49 -# See also: -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L275 -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L278 -INDEX_ALARM_TYPE = 0 -INDEX_ALARM_LEVEL = 1 -INDEX_ALARM_ACCESS_CONTROL = 9 - -# https://github.com/OpenZWave/open-zwave/blob/de1c0e60edf1d1bee81f1ae54b1f58e66c6fd8ed/cpp/src/command_classes/BarrierOperator.cpp#L69 -INDEX_BARRIER_OPERATOR_LABEL = 1 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/DoorLock.cpp#L77 -INDEX_DOOR_LOCK_LOCK = 0 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Meter.cpp#L114 -# See also: -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Meter.cpp#L279 -INDEX_METER_POWER = 8 -INDEX_METER_RESET = 33 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/SensorMultilevel.cpp#L50 -INDEX_SENSOR_MULTILEVEL_TEMPERATURE = 1 -INDEX_SENSOR_MULTILEVEL_POWER = 4 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Color.cpp#L109 -INDEX_SWITCH_COLOR_COLOR = 0 -INDEX_SWITCH_COLOR_CHANNELS = 2 - -# https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/SwitchMultilevel.cpp#L54 -INDEX_SWITCH_MULTILEVEL_LEVEL = 0 -INDEX_SWITCH_MULTILEVEL_BRIGHT = 1 -INDEX_SWITCH_MULTILEVEL_DIM = 2 -INDEX_SWITCH_MULTILEVEL_DURATION = 5 diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py deleted file mode 100644 index 2a49a34554b..00000000000 --- a/homeassistant/components/zwave/cover.py +++ /dev/null @@ -1,216 +0,0 @@ -"""Support for Z-Wave covers.""" -import logging - -from homeassistant.components.cover import ( - ATTR_POSITION, - DOMAIN, - SUPPORT_CLOSE, - SUPPORT_OPEN, - CoverDeviceClass, - CoverEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ( - CONF_INVERT_OPENCLOSE_BUTTONS, - CONF_INVERT_PERCENT, - ZWaveDeviceEntity, - workaround, -) -from .const import ( - COMMAND_CLASS_BARRIER_OPERATOR, - COMMAND_CLASS_SWITCH_BINARY, - COMMAND_CLASS_SWITCH_MULTILEVEL, - DATA_NETWORK, -) - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Cover from Config Entry.""" - - @callback - def async_add_cover(cover): - """Add Z-Wave Cover.""" - async_add_entities([cover]) - - async_dispatcher_connect(hass, "zwave_new_cover", async_add_cover) - - -def get_device(hass, values, node_config, **kwargs): - """Create Z-Wave entity device.""" - invert_buttons = node_config.get(CONF_INVERT_OPENCLOSE_BUTTONS) - invert_percent = node_config.get(CONF_INVERT_PERCENT) - if ( - values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL - and values.primary.index == 0 - ): - return ZwaveRollershutter(hass, values, invert_buttons, invert_percent) - if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY: - return ZwaveGarageDoorSwitch(values) - if values.primary.command_class == COMMAND_CLASS_BARRIER_OPERATOR: - return ZwaveGarageDoorBarrier(values) - return None - - -class ZwaveRollershutter(ZWaveDeviceEntity, CoverEntity): - """Representation of an Z-Wave cover.""" - - def __init__(self, hass, values, invert_buttons, invert_percent): - """Initialize the Z-Wave rollershutter.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._network = hass.data[DATA_NETWORK] - self._open_id = None - self._close_id = None - self._current_position = None - self._invert_buttons = invert_buttons - self._invert_percent = invert_percent - - self._workaround = workaround.get_device_mapping(values.primary) - if self._workaround: - _LOGGER.debug("Using workaround %s", self._workaround) - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - # Position value - self._current_position = self.values.primary.data - - if ( - self.values.open - and self.values.close - and self._open_id is None - and self._close_id is None - ): - if self._invert_buttons: - self._open_id = self.values.close.value_id - self._close_id = self.values.open.value_id - else: - self._open_id = self.values.open.value_id - self._close_id = self.values.close.value_id - - @property - def is_closed(self): - """Return if the cover is closed.""" - if self.current_cover_position is None: - return None - if self.current_cover_position > 0: - return False - return True - - @property - def current_cover_position(self): - """Return the current position of Zwave roller shutter.""" - if self._workaround == workaround.WORKAROUND_NO_POSITION: - return None - - if self._current_position is not None: - if self._current_position <= 5: - return 100 if self._invert_percent else 0 - if self._current_position >= 95: - return 0 if self._invert_percent else 100 - return ( - 100 - self._current_position - if self._invert_percent - else self._current_position - ) - - def open_cover(self, **kwargs): - """Move the roller shutter up.""" - self._network.manager.pressButton(self._open_id) - - def close_cover(self, **kwargs): - """Move the roller shutter down.""" - self._network.manager.pressButton(self._close_id) - - def set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position.""" - self.node.set_dimmer( - self.values.primary.value_id, - (100 - kwargs.get(ATTR_POSITION)) - if self._invert_percent - else kwargs.get(ATTR_POSITION), - ) - - def stop_cover(self, **kwargs): - """Stop the roller shutter.""" - self._network.manager.releaseButton(self._open_id) - - -class ZwaveGarageDoorBase(ZWaveDeviceEntity, CoverEntity): - """Base class for a Zwave garage door device.""" - - def __init__(self, values): - """Initialize the zwave garage door.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._state = None - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - _LOGGER.debug("self._state=%s", self._state) - - @property - def device_class(self): - """Return the class of this device, from CoverDeviceClass.""" - return CoverDeviceClass.GARAGE - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_GARAGE - - -class ZwaveGarageDoorSwitch(ZwaveGarageDoorBase): - """Representation of a switch based Zwave garage door device.""" - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return not self._state - - def close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.data = False - - def open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.data = True - - -class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): - """Representation of a barrier operator Zwave garage door device.""" - - @property - def is_opening(self): - """Return true if cover is in an opening state.""" - return self._state == "Opening" - - @property - def is_closing(self): - """Return true if cover is in a closing state.""" - return self._state == "Closing" - - @property - def is_closed(self): - """Return the current position of Zwave garage door.""" - return self._state == "Closed" - - def close_cover(self, **kwargs): - """Close the garage door.""" - self.values.primary.data = "Closed" - - def open_cover(self, **kwargs): - """Open the garage door.""" - self.values.primary.data = "Opened" diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py deleted file mode 100644 index f8674a48a32..00000000000 --- a/homeassistant/components/zwave/discovery_schemas.py +++ /dev/null @@ -1,416 +0,0 @@ -"""Z-Wave discovery schemas.""" -from . import const - -DEFAULT_VALUES_SCHEMA = { - "power": { - const.DISC_SCHEMAS: [ - { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_POWER], - }, - { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_METER], - const.DISC_INDEX: [const.INDEX_METER_POWER], - }, - ], - const.DISC_OPTIONAL: True, - } -} - -DISCOVERY_SCHEMAS = [ - { - const.DISC_COMPONENT: "binary_sensor", - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_ENTRY_CONTROL, - const.GENERIC_TYPE_SENSOR_ALARM, - const.GENERIC_TYPE_SENSOR_BINARY, - const.GENERIC_TYPE_SWITCH_BINARY, - const.GENERIC_TYPE_METER, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_SENSOR_NOTIFICATION, - const.GENERIC_TYPE_THERMOSTAT, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_BINARY], - const.DISC_TYPE: const.TYPE_BOOL, - const.DISC_GENRE: const.GENRE_USER, - }, - "off_delay": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_CONFIGURATION], - const.DISC_INDEX: [9], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "climate", # thermostat without COMMAND_CLASS_THERMOSTAT_MODE - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_THERMOSTAT, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_THERMOSTAT_HEATING, - const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - const.SPECIFIC_TYPE_NOT_USED, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] - }, - "temperature": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], - const.DISC_OPTIONAL: True, - }, - "fan_mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE - ], - const.DISC_OPTIONAL: True, - }, - "fan_action": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION - ], - const.DISC_OPTIONAL: True, - }, - "mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "climate", # thermostat with COMMAND_CLASS_THERMOSTAT_MODE - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_THERMOSTAT, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_THERMOSTAT_GENERAL, - const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, - const.SPECIFIC_TYPE_SETBACK_THERMOSTAT, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE] - }, - "setpoint_heating": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [1], - const.DISC_OPTIONAL: True, - }, - "setpoint_cooling": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [2], - const.DISC_OPTIONAL: True, - }, - "setpoint_furnace": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [7], - const.DISC_OPTIONAL: True, - }, - "setpoint_dry_air": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [8], - const.DISC_OPTIONAL: True, - }, - "setpoint_moist_air": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [9], - const.DISC_OPTIONAL: True, - }, - "setpoint_auto_changeover": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [10], - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_heating": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [11], - const.DISC_OPTIONAL: True, - }, - "setpoint_eco_cooling": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [12], - const.DISC_OPTIONAL: True, - }, - "setpoint_away_heating": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [13], - const.DISC_OPTIONAL: True, - }, - "setpoint_away_cooling": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [14], - const.DISC_OPTIONAL: True, - }, - "setpoint_full_power": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], - const.DISC_INDEX: [15], - const.DISC_OPTIONAL: True, - }, - "temperature": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], - const.DISC_OPTIONAL: True, - }, - "fan_mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], - const.DISC_OPTIONAL: True, - }, - "operating_state": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE - ], - const.DISC_OPTIONAL: True, - }, - "fan_action": { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION - ], - const.DISC_OPTIONAL: True, - }, - "zxt_120_swing_mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_CONFIGURATION], - const.DISC_INDEX: [33], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "cover", # Rollershutter - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_ENTRY_CONTROL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const.SPECIFIC_TYPE_SECURE_DOOR, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_GENRE: const.GENRE_USER, - }, - "open": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_BRIGHT], - const.DISC_OPTIONAL: True, - }, - "close": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_DIM], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "cover", # Garage Door Switch - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_ENTRY_CONTROL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const.SPECIFIC_TYPE_SECURE_DOOR, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY], - const.DISC_GENRE: const.GENRE_USER, - } - }, - ), - }, - { - const.DISC_COMPONENT: "cover", # Garage Door Barrier - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_ENTRY_CONTROL, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, - const.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, - const.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, - const.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, - const.SPECIFIC_TYPE_SECURE_DOOR, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_BARRIER_OPERATOR], - const.DISC_INDEX: [const.INDEX_BARRIER_OPERATOR_LABEL], - } - }, - ), - }, - { - const.DISC_COMPONENT: "fan", - const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_SWITCH_MULTILEVEL], - const.DISC_SPECIFIC_DEVICE_CLASS: [const.SPECIFIC_TYPE_FAN_SWITCH], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_LEVEL], - const.DISC_TYPE: const.TYPE_BYTE, - } - }, - ), - }, - { - const.DISC_COMPONENT: "light", - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_SWITCH_REMOTE, - ], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL, - const.SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL, - const.SPECIFIC_TYPE_NOT_USED, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_LEVEL], - const.DISC_TYPE: const.TYPE_BYTE, - }, - "dimming_duration": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_MULTILEVEL], - const.DISC_INDEX: [const.INDEX_SWITCH_MULTILEVEL_DURATION], - const.DISC_OPTIONAL: True, - }, - "color": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_COLOR], - const.DISC_INDEX: [const.INDEX_SWITCH_COLOR_COLOR], - const.DISC_OPTIONAL: True, - }, - "color_channels": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_COLOR], - const.DISC_INDEX: [const.INDEX_SWITCH_COLOR_CHANNELS], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "lock", - const.DISC_GENERIC_DEVICE_CLASS: [const.GENERIC_TYPE_ENTRY_CONTROL], - const.DISC_SPECIFIC_DEVICE_CLASS: [ - const.SPECIFIC_TYPE_DOOR_LOCK, - const.SPECIFIC_TYPE_ADVANCED_DOOR_LOCK, - const.SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK, - const.SPECIFIC_TYPE_SECURE_LOCKBOX, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_DOOR_LOCK], - const.DISC_INDEX: [const.INDEX_DOOR_LOCK_LOCK], - }, - "access_control": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_ALARM], - const.DISC_INDEX: [const.INDEX_ALARM_ACCESS_CONTROL], - const.DISC_OPTIONAL: True, - }, - "alarm_type": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_ALARM], - const.DISC_INDEX: [const.INDEX_ALARM_TYPE], - const.DISC_OPTIONAL: True, - }, - "alarm_level": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_ALARM], - const.DISC_INDEX: [const.INDEX_ALARM_LEVEL], - const.DISC_OPTIONAL: True, - }, - "v2btze_advanced": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_CONFIGURATION], - const.DISC_INDEX: [12], - const.DISC_OPTIONAL: True, - }, - }, - ), - }, - { - const.DISC_COMPONENT: "sensor", - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - const.COMMAND_CLASS_ALARM, - const.COMMAND_CLASS_SENSOR_ALARM, - const.COMMAND_CLASS_INDICATOR, - const.COMMAND_CLASS_BATTERY, - ], - const.DISC_GENRE: const.GENRE_USER, - } - }, - ), - }, - { - const.DISC_COMPONENT: "switch", - const.DISC_GENERIC_DEVICE_CLASS: [ - const.GENERIC_TYPE_METER, - const.GENERIC_TYPE_SENSOR_ALARM, - const.GENERIC_TYPE_SENSOR_BINARY, - const.GENERIC_TYPE_SWITCH_BINARY, - const.GENERIC_TYPE_ENTRY_CONTROL, - const.GENERIC_TYPE_SENSOR_MULTILEVEL, - const.GENERIC_TYPE_SWITCH_MULTILEVEL, - const.GENERIC_TYPE_SENSOR_NOTIFICATION, - const.GENERIC_TYPE_GENERIC_CONTROLLER, - const.GENERIC_TYPE_SWITCH_REMOTE, - const.GENERIC_TYPE_REPEATER_SLAVE, - const.GENERIC_TYPE_THERMOSTAT, - const.GENERIC_TYPE_WALL_CONTROLLER, - ], - const.DISC_VALUES: dict( - DEFAULT_VALUES_SCHEMA, - **{ - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY], - const.DISC_TYPE: const.TYPE_BOOL, - const.DISC_GENRE: const.GENRE_USER, - } - }, - ), - }, -] diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py deleted file mode 100644 index ba758844c69..00000000000 --- a/homeassistant/components/zwave/fan.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Support for Z-Wave fans.""" -import math - -from homeassistant.components.fan import DOMAIN, SUPPORT_SET_SPEED, FanEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.percentage import ( - int_states_in_range, - percentage_to_ranged_value, - ranged_value_to_percentage, -) - -from . import ZWaveDeviceEntity - -SUPPORTED_FEATURES = SUPPORT_SET_SPEED - -SPEED_RANGE = (1, 99) # off is not included - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Fan from Config Entry.""" - - @callback - def async_add_fan(fan): - """Add Z-Wave Fan.""" - async_add_entities([fan]) - - async_dispatcher_connect(hass, "zwave_new_fan", async_add_fan) - - -def get_device(values, **kwargs): - """Create Z-Wave entity device.""" - return ZwaveFan(values) - - -class ZwaveFan(ZWaveDeviceEntity, FanEntity): - """Representation of a Z-Wave fan.""" - - def __init__(self, values): - """Initialize the Z-Wave fan device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - - def set_percentage(self, percentage): - """Set the speed percentage of the fan.""" - if percentage is None: - # Value 255 tells device to return to previous value - zwave_speed = 255 - elif percentage == 0: - zwave_speed = 0 - else: - zwave_speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) - self.node.set_dimmer(self.values.primary.value_id, zwave_speed) - - def turn_on(self, percentage=None, preset_mode=None, **kwargs): - """Turn the device on.""" - self.set_percentage(percentage) - - def turn_off(self, **kwargs): - """Turn the device off.""" - self.node.set_dimmer(self.values.primary.value_id, 0) - - @property - def percentage(self): - """Return the current speed percentage.""" - return ranged_value_to_percentage(SPEED_RANGE, self._state) - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE) - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py deleted file mode 100644 index ea2b34a874f..00000000000 --- a/homeassistant/components/zwave/light.py +++ /dev/null @@ -1,407 +0,0 @@ -"""Support for Z-Wave lights.""" -import logging -from threading import Timer - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, - ATTR_RGBW_COLOR, - ATTR_TRANSITION, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - DOMAIN, - SUPPORT_TRANSITION, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const - -_LOGGER = logging.getLogger(__name__) - -COLOR_CHANNEL_WARM_WHITE = 0x01 -COLOR_CHANNEL_COLD_WHITE = 0x02 -COLOR_CHANNEL_RED = 0x04 -COLOR_CHANNEL_GREEN = 0x08 -COLOR_CHANNEL_BLUE = 0x10 - -# Some bulbs have an independent warm and cool white light LEDs. These need -# to be treated differently, aka the zw098 workaround. Ensure these are added -# to DEVICE_MAPPINGS below. -# (Manufacturer ID, Product ID) from -# https://github.com/OpenZWave/open-zwave/blob/master/config/manufacturer_specific.xml -AEOTEC_ZW098_LED_BULB_LIGHT = (0x86, 0x62) -AEOTEC_ZWA001_LED_BULB_LIGHT = (0x371, 0x1) -AEOTEC_ZWA002_LED_BULB_LIGHT = (0x371, 0x2) -HANK_HKZW_RGB01_LED_BULB_LIGHT = (0x208, 0x4) -ZIPATO_RGB_BULB_2_LED_BULB_LIGHT = (0x131, 0x3) - -WORKAROUND_ZW098 = "zw098" - -DEVICE_MAPPINGS = { - AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098, - AEOTEC_ZWA001_LED_BULB_LIGHT: WORKAROUND_ZW098, - AEOTEC_ZWA002_LED_BULB_LIGHT: WORKAROUND_ZW098, - HANK_HKZW_RGB01_LED_BULB_LIGHT: WORKAROUND_ZW098, - ZIPATO_RGB_BULB_2_LED_BULB_LIGHT: WORKAROUND_ZW098, -} - -# Generate midpoint color temperatures for bulbs that have limited -# support for white light colors -TEMP_COLOR_MAX = 500 # mireds (inverted) -TEMP_COLOR_MIN = 154 -TEMP_MID_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 2 + TEMP_COLOR_MIN -TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN -TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Light from Config Entry.""" - - @callback - def async_add_light(light): - """Add Z-Wave Light.""" - async_add_entities([light]) - - async_dispatcher_connect(hass, "zwave_new_light", async_add_light) - - -def get_device(node, values, node_config, **kwargs): - """Create Z-Wave entity device.""" - refresh = node_config.get(CONF_REFRESH_VALUE) - delay = node_config.get(CONF_REFRESH_DELAY) - _LOGGER.debug( - "node=%d value=%d node_config=%s CONF_REFRESH_VALUE=%s" - " CONF_REFRESH_DELAY=%s", - node.node_id, - values.primary.value_id, - node_config, - refresh, - delay, - ) - - if node.has_command_class(const.COMMAND_CLASS_SWITCH_COLOR): - return ZwaveColorLight(values, refresh, delay) - return ZwaveDimmer(values, refresh, delay) - - -def brightness_state(value): - """Return the brightness and state.""" - if value.data > 0: - return round((value.data / 99) * 255), STATE_ON - return 0, STATE_OFF - - -def byte_to_zwave_brightness(value): - """Convert brightness in 0-255 scale to 0-99 scale. - - `value` -- (int) Brightness byte value from 0-255. - """ - if value > 0: - return max(1, round((value / 255) * 99)) - return 0 - - -class ZwaveDimmer(ZWaveDeviceEntity, LightEntity): - """Representation of a Z-Wave dimmer.""" - - def __init__(self, values, refresh, delay): - """Initialize the light.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._brightness = None - self._state = None - self._color_mode = None - self._supported_color_modes = set() - self._supported_features = 0 - self._delay = delay - self._refresh_value = refresh - self._zw098 = None - - # Enable appropriate workaround flags for our device - # Make sure that we have values for the key before converting to int - if self.node.manufacturer_id.strip() and self.node.product_id.strip(): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16), - ) - if ( - specific_sensor_key in DEVICE_MAPPINGS - and DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098 - ): - _LOGGER.debug("AEOTEC ZW098 workaround enabled") - self._zw098 = 1 - - # Used for value change event handling - self._refreshing = False - self._timer = None - _LOGGER.debug( - "self._refreshing=%s self.delay=%s", self._refresh_value, self._delay - ) - self.value_added() - self.update_properties() - - def update_properties(self): - """Update internal properties based on zwave values.""" - # Brightness - self._brightness, self._state = brightness_state(self.values.primary) - - def value_added(self): - """Call when a new value is added to this entity.""" - self._supported_color_modes = {COLOR_MODE_BRIGHTNESS} - self._color_mode = COLOR_MODE_BRIGHTNESS - if self.values.dimming_duration is not None: - self._supported_features = SUPPORT_TRANSITION - - def value_changed(self): - """Call when a value for this entity's node has changed.""" - if self._refresh_value: - if self._refreshing: - self._refreshing = False - else: - - def _refresh_value(): - """Use timer callback for delayed value refresh.""" - self._refreshing = True - self.values.primary.refresh() - - if self._timer is not None and self._timer.is_alive(): - self._timer.cancel() - - self._timer = Timer(self._delay, _refresh_value) - self._timer.start() - return - super().value_changed() - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return true if device is on.""" - return self._state == STATE_ON - - @property - def color_mode(self): - """Return the current color mode.""" - return self._color_mode - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - - def _set_duration(self, **kwargs): - """Set the transition time for the brightness value. - - Zwave Dimming Duration values: - 0x00 = instant - 0x01-0x7F = 1 second to 127 seconds - 0x80-0xFE = 1 minute to 127 minutes - 0xFF = factory default - """ - if self.values.dimming_duration is None: - if ATTR_TRANSITION in kwargs: - _LOGGER.debug("Dimming not supported by %s", self.entity_id) - return - - if ATTR_TRANSITION not in kwargs: - self.values.dimming_duration.data = 0xFF - return - - transition = kwargs[ATTR_TRANSITION] - if transition <= 127: - self.values.dimming_duration.data = int(transition) - elif transition > 7620: - self.values.dimming_duration.data = 0xFE - _LOGGER.warning("Transition clipped to 127 minutes for %s", self.entity_id) - else: - minutes = int(transition / 60) - _LOGGER.debug( - "Transition rounded to %d minutes for %s", minutes, self.entity_id - ) - self.values.dimming_duration.data = minutes + 0x7F - - def turn_on(self, **kwargs): - """Turn the device on.""" - self._set_duration(**kwargs) - - # Zwave multilevel switches use a range of [0, 99] to control - # brightness. Level 255 means to set it to previous value. - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - brightness = byte_to_zwave_brightness(self._brightness) - else: - brightness = 255 - - if self.node.set_dimmer(self.values.primary.value_id, brightness): - self._state = STATE_ON - - def turn_off(self, **kwargs): - """Turn the device off.""" - self._set_duration(**kwargs) - - if self.node.set_dimmer(self.values.primary.value_id, 0): - self._state = STATE_OFF - - -class ZwaveColorLight(ZwaveDimmer): - """Representation of a Z-Wave color changing light.""" - - def __init__(self, values, refresh, delay): - """Initialize the light.""" - self._color_channels = None - self._rgb = None - self._ct = None - self._white = None - - super().__init__(values, refresh, delay) - - def value_added(self): - """Call when a new value is added to this entity.""" - if self.values.dimming_duration is not None: - self._supported_features = SUPPORT_TRANSITION - - self._supported_color_modes = {COLOR_MODE_RGB} - self._color_mode = COLOR_MODE_RGB - if self._zw098: - self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) - elif self._color_channels is not None and self._color_channels & ( - COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE - ): - self._supported_color_modes = {COLOR_MODE_RGBW} - self._color_mode = COLOR_MODE_RGBW - - def update_properties(self): - """Update internal properties based on zwave values.""" - super().update_properties() - - if self.values.color is None: - return - if self.values.color_channels is None: - return - - # Color Channels - self._color_channels = self.values.color_channels.data - - # Color Data String - data = self.values.color.data - - # RGB is always present in the openzwave color data string. - self._rgb = (int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)) - - # Parse remaining color channels. Openzwave appends white channels - # that are present. - index = 7 - - # Warm white - if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - warm_white = int(data[index : index + 2], 16) - index += 2 - else: - warm_white = 0 - - # Cold white - if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - cold_white = int(data[index : index + 2], 16) - index += 2 - else: - cold_white = 0 - - # Color temperature. With the AEOTEC ZW098 bulb, only two color - # temperatures are supported. The warm and cold channel values - # indicate brightness for warm/cold color temperature. - if self._zw098: - if warm_white > 0: - self._ct = TEMP_WARM_HASS - self._color_mode = COLOR_MODE_COLOR_TEMP - elif cold_white > 0: - self._ct = TEMP_COLD_HASS - self._color_mode = COLOR_MODE_COLOR_TEMP - else: - self._color_mode = COLOR_MODE_RGB - - elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._white = warm_white - - elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._white = cold_white - - # If no rgb channels supported, report None. - if not ( - self._color_channels & COLOR_CHANNEL_RED - or self._color_channels & COLOR_CHANNEL_GREEN - or self._color_channels & COLOR_CHANNEL_BLUE - ): - self._rgb = None - - @property - def rgb_color(self): - """Return the rgb color.""" - return self._rgb - - @property - def rgbw_color(self): - """Return the rgbw color.""" - if self._rgb is None: - return None - return (*self._rgb, self._white) - - @property - def color_temp(self): - """Return the color temperature.""" - return self._ct - - def turn_on(self, **kwargs): - """Turn the device on.""" - rgbw = None - - if ATTR_COLOR_TEMP in kwargs: - # Color temperature. With the AEOTEC ZW098 bulb, only two color - # temperatures are supported. The warm and cold channel values - # indicate brightness for warm/cold color temperature. - if self._zw098: - self._color_mode = COLOR_MODE_COLOR_TEMP - if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: - self._ct = TEMP_WARM_HASS - rgbw = "#000000ff00" - else: - self._ct = TEMP_COLD_HASS - rgbw = "#00000000ff" - elif ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] - self._white = 0 - elif ATTR_RGBW_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGBW_COLOR][0:3] - self._white = kwargs[ATTR_RGBW_COLOR][3] - - if ATTR_RGB_COLOR in kwargs or ATTR_RGBW_COLOR in kwargs: - rgbw = "#" - for colorval in self._rgb: - rgbw += format(colorval, "02x") - if self._white is not None: - rgbw += format(self._white, "02x") + "00" - else: - rgbw += "0000" - - if rgbw and self.values.color: - self.values.color.data = rgbw - - super().turn_on(**kwargs) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py deleted file mode 100644 index 06ce59a1f9e..00000000000 --- a/homeassistant/components/zwave/lock.py +++ /dev/null @@ -1,390 +0,0 @@ -"""Support for Z-Wave door locks.""" -import logging - -import voluptuous as vol - -from homeassistant.components.lock import DOMAIN, LockEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, ServiceCall, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, const - -_LOGGER = logging.getLogger(__name__) - -ATTR_NOTIFICATION = "notification" -ATTR_LOCK_STATUS = "lock_status" -ATTR_CODE_SLOT = "code_slot" -ATTR_USERCODE = "usercode" -CONFIG_ADVANCED = "Advanced" - -SERVICE_SET_USERCODE = "set_usercode" -SERVICE_GET_USERCODE = "get_usercode" -SERVICE_CLEAR_USERCODE = "clear_usercode" - -POLYCONTROL = 0x10E -DANALOCK_V2_BTZE = 0x2 -POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) -WORKAROUND_V2BTZE = 1 -WORKAROUND_DEVICE_STATE = 2 -WORKAROUND_TRACK_MESSAGE = 4 -WORKAROUND_ALARM_TYPE = 8 - -DEVICE_MAPPINGS = { - POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, - # Kwikset 914TRL ZW500 99100-078 - (0x0090, 0x440): WORKAROUND_DEVICE_STATE, - (0x0090, 0x446): WORKAROUND_DEVICE_STATE, - (0x0090, 0x238): WORKAROUND_DEVICE_STATE, - # Kwikset 888ZW500-15S Smartcode 888 - (0x0090, 0x541): WORKAROUND_DEVICE_STATE, - # Kwikset 916 - (0x0090, 0x0001): WORKAROUND_DEVICE_STATE, - # Kwikset Obsidian - (0x0090, 0x0742): WORKAROUND_DEVICE_STATE, - # Yale Locks - # Yale YRD210, YRD220, YRL220 - (0x0129, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD210, YRD220 - (0x0129, 0x0209): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRL210, YRL220 - (0x0129, 0x0409): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD256 - (0x0129, 0x0600): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD110, YRD120 - (0x0129, 0x0800): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD446 - (0x0129, 0x1000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRL220 - (0x0129, 0x2132): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - (0x0129, 0x3CAC): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD210, YRD220 - (0x0129, 0xAA00): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD220 - (0x0129, 0xFFFF): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRL256 - (0x0129, 0x0F00): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Yale YRD220 (Older Yale products with incorrect vendor ID) - (0x0109, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, - # Schlage BE469 - (0x003B, 0x5044): WORKAROUND_DEVICE_STATE | WORKAROUND_TRACK_MESSAGE, - # Schlage FE599NX - (0x003B, 0x504C): WORKAROUND_DEVICE_STATE, -} - -LOCK_NOTIFICATION = { - "1": "Manual Lock", - "2": "Manual Unlock", - "5": "Keypad Lock", - "6": "Keypad Unlock", - "11": "Lock Jammed", - "254": "Unknown Event", -} -NOTIFICATION_RF_LOCK = "3" -NOTIFICATION_RF_UNLOCK = "4" -LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] = "RF Lock" -LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] = "RF Unlock" - -LOCK_ALARM_TYPE = { - "9": "Deadbolt Jammed", - "16": "Unlocked by Bluetooth ", - "18": "Locked with Keypad by user ", - "19": "Unlocked with Keypad by user ", - "21": "Manually Locked ", - "22": "Manually Unlocked ", - "27": "Auto re-lock", - "33": "User deleted: ", - "112": "Master code changed or User added: ", - "113": "Duplicate PIN code: ", - "130": "RF module, power restored", - "144": "Unlocked by NFC Tag or Card by user ", - "161": "Tamper Alarm: ", - "167": "Low Battery", - "168": "Critical Battery Level", - "169": "Battery too low to operate", -} -ALARM_RF_LOCK = "24" -ALARM_RF_UNLOCK = "25" -LOCK_ALARM_TYPE[ALARM_RF_LOCK] = "Locked by RF" -LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] = "Unlocked by RF" - -MANUAL_LOCK_ALARM_LEVEL = { - "1": "by Key Cylinder or Inside thumb turn", - "2": "by Touch function (lock and leave)", -} - -TAMPER_ALARM_LEVEL = {"1": "Too many keypresses", "2": "Cover removed"} - -LOCK_STATUS = { - "1": True, - "2": False, - "3": True, - "4": False, - "5": True, - "6": False, - "9": False, - "18": True, - "19": False, - "21": True, - "22": False, - "24": True, - "25": False, - "27": True, -} - -ALARM_TYPE_STD = ["18", "19", "33", "112", "113", "144"] - -SET_USERCODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - vol.Required(ATTR_USERCODE): cv.string, - } -) - -GET_USERCODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - } -) - -CLEAR_USERCODE_SCHEMA = vol.Schema( - { - vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), - vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), - } -) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Lock from Config Entry.""" - - @callback - def async_add_lock(lock): - """Add Z-Wave Lock.""" - async_add_entities([lock]) - - async_dispatcher_connect(hass, "zwave_new_lock", async_add_lock) - - network = hass.data[const.DATA_NETWORK] - - def set_usercode(service: ServiceCall) -> None: - """Set the usercode to index X on the lock.""" - node_id = service.data.get(const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - usercode = service.data.get(ATTR_USERCODE) - - for value in lock_node.get_values( - class_id=const.COMMAND_CLASS_USER_CODE - ).values(): - if value.index != code_slot: - continue - if len(str(usercode)) < 4: - _LOGGER.error( - "Invalid code provided: (%s) " - "usercode must be at least 4 and at most" - " %s digits", - usercode, - len(value.data), - ) - break - value.data = str(usercode) - break - - def get_usercode(service: ServiceCall) -> None: - """Get a usercode at index X on the lock.""" - node_id = service.data.get(const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - - for value in lock_node.get_values( - class_id=const.COMMAND_CLASS_USER_CODE - ).values(): - if value.index != code_slot: - continue - _LOGGER.info("Usercode at slot %s is: %s", value.index, value.data) - break - - def clear_usercode(service: ServiceCall) -> None: - """Set usercode to slot X on the lock.""" - node_id = service.data.get(const.ATTR_NODE_ID) - lock_node = network.nodes[node_id] - code_slot = service.data.get(ATTR_CODE_SLOT) - data = "" - - for value in lock_node.get_values( - class_id=const.COMMAND_CLASS_USER_CODE - ).values(): - if value.index != code_slot: - continue - for i in range(len(value.data)): - data += "\0" - i += 1 - _LOGGER.debug("Data to clear lock: %s", data) - value.data = data - _LOGGER.info("Usercode at slot %s is cleared", value.index) - break - - hass.services.async_register( - DOMAIN, SERVICE_SET_USERCODE, set_usercode, schema=SET_USERCODE_SCHEMA - ) - hass.services.async_register( - DOMAIN, SERVICE_GET_USERCODE, get_usercode, schema=GET_USERCODE_SCHEMA - ) - hass.services.async_register( - DOMAIN, SERVICE_CLEAR_USERCODE, clear_usercode, schema=CLEAR_USERCODE_SCHEMA - ) - - -def get_device(node, values, **kwargs): - """Create Z-Wave entity device.""" - return ZwaveLock(values) - - -class ZwaveLock(ZWaveDeviceEntity, LockEntity): - """Representation of a Z-Wave Lock.""" - - def __init__(self, values): - """Initialize the Z-Wave lock device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self._state = None - self._notification = None - self._lock_status = None - self._v2btze = None - self._state_workaround = False - self._track_message_workaround = False - self._previous_message = None - self._alarm_type_workaround = False - - # Enable appropriate workaround flags for our device - # Make sure that we have values for the key before converting to int - if self.node.manufacturer_id.strip() and self.node.product_id.strip(): - specific_sensor_key = ( - int(self.node.manufacturer_id, 16), - int(self.node.product_id, 16), - ) - if specific_sensor_key in DEVICE_MAPPINGS: - workaround = DEVICE_MAPPINGS[specific_sensor_key] - if workaround & WORKAROUND_V2BTZE: - self._v2btze = 1 - _LOGGER.debug("Polycontrol Danalock v2 BTZE workaround enabled") - if workaround & WORKAROUND_DEVICE_STATE: - self._state_workaround = True - _LOGGER.debug("Notification device state workaround enabled") - if workaround & WORKAROUND_TRACK_MESSAGE: - self._track_message_workaround = True - _LOGGER.debug("Message tracking workaround enabled") - if workaround & WORKAROUND_ALARM_TYPE: - self._alarm_type_workaround = True - _LOGGER.debug("Alarm Type device state workaround enabled") - self.update_properties() - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - _LOGGER.debug("lock state set to %s", self._state) - if self.values.access_control: - notification_data = self.values.access_control.data - self._notification = LOCK_NOTIFICATION.get(str(notification_data)) - if self._state_workaround: - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug("workaround: lock state set to %s", self._state) - if ( - self._v2btze - and self.values.v2btze_advanced - and self.values.v2btze_advanced.data == CONFIG_ADVANCED - ): - self._state = LOCK_STATUS.get(str(notification_data)) - _LOGGER.debug( - "Lock state set from Access Control value and is %s, get=%s", - str(notification_data), - self.state, - ) - - if self._track_message_workaround: - this_message = self.node.stats["lastReceivedMessage"][5] - - if this_message == const.COMMAND_CLASS_DOOR_LOCK: - self._state = self.values.primary.data - _LOGGER.debug("set state to %s based on message tracking", self._state) - if self._previous_message == const.COMMAND_CLASS_DOOR_LOCK: - if self._state: - self._notification = LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] - self._lock_status = LOCK_ALARM_TYPE[ALARM_RF_LOCK] - else: - self._notification = LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] - self._lock_status = LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] - return - - self._previous_message = this_message - - if not self.values.alarm_type: - return - - alarm_type = self.values.alarm_type.data - if self.values.alarm_level: - alarm_level = self.values.alarm_level.data - else: - alarm_level = None - - if not alarm_type: - return - - if self._alarm_type_workaround: - self._state = LOCK_STATUS.get(str(alarm_type)) - _LOGGER.debug( - "workaround: lock state set to %s -- alarm type: %s", - self._state, - str(alarm_type), - ) - - if alarm_type == 21: - self._lock_status = ( - f"{LOCK_ALARM_TYPE.get(str(alarm_type))}" - f"{MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level))}" - ) - return - if str(alarm_type) in ALARM_TYPE_STD: - self._lock_status = f"{LOCK_ALARM_TYPE.get(str(alarm_type))}{alarm_level}" - return - if alarm_type == 161: - self._lock_status = ( - f"{LOCK_ALARM_TYPE.get(str(alarm_type))}" - f"{TAMPER_ALARM_LEVEL.get(str(alarm_level))}" - ) - - return - if alarm_type != 0: - self._lock_status = LOCK_ALARM_TYPE.get(str(alarm_type)) - return - - @property - def is_locked(self): - """Return true if device is locked.""" - return self._state - - def lock(self, **kwargs): - """Lock the device.""" - self.values.primary.data = True - - def unlock(self, **kwargs): - """Unlock the device.""" - self.values.primary.data = False - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - data = super().extra_state_attributes - if self._notification: - data[ATTR_NOTIFICATION] = self._notification - if self._lock_status: - data[ATTR_LOCK_STATUS] = self._lock_status - return data diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json deleted file mode 100644 index bf3a9abe77e..00000000000 --- a/homeassistant/components/zwave/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "zwave", - "name": "Z-Wave (deprecated)", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], - "codeowners": ["@home-assistant/z-wave"], - "iot_class": "local_push" -} diff --git a/homeassistant/components/zwave/migration.py b/homeassistant/components/zwave/migration.py deleted file mode 100644 index 0b151d18e4b..00000000000 --- a/homeassistant/components/zwave/migration.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Handle migration from legacy Z-Wave to OpenZWave and Z-Wave JS.""" -from __future__ import annotations - -from typing import TYPE_CHECKING, TypedDict, cast - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import async_get as async_get_device_registry -from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry -from homeassistant.helpers.singleton import singleton -from homeassistant.helpers.storage import Store - -from .const import DOMAIN -from .util import node_device_id_and_name - -if TYPE_CHECKING: - from . import ZWaveDeviceEntityValues - -LEGACY_ZWAVE_MIGRATION = f"{DOMAIN}_legacy_zwave_migration" -STORAGE_WRITE_DELAY = 30 -STORAGE_KEY = f"{DOMAIN}.legacy_zwave_migration" -STORAGE_VERSION = 1 - - -class ZWaveMigrationData(TypedDict): - """Represent the Z-Wave migration data dict.""" - - node_id: int - node_instance: int - command_class: int - command_class_label: str - value_index: int - device_id: str - domain: str - entity_id: str - unique_id: str - unit_of_measurement: str | None - - -@callback -def async_is_ozw_migrated(hass): - """Return True if migration to ozw is done.""" - ozw_config_entries = hass.config_entries.async_entries("ozw") - if not ozw_config_entries: - return False - - ozw_config_entry = ozw_config_entries[0] # only one ozw entry is allowed - migrated = bool(ozw_config_entry.data.get("migrated")) - return migrated - - -@callback -def async_is_zwave_js_migrated(hass): - """Return True if migration to Z-Wave JS is done.""" - zwave_js_config_entries = hass.config_entries.async_entries("zwave_js") - if not zwave_js_config_entries: - return False - - migrated = any( - config_entry.data.get("migrated") for config_entry in zwave_js_config_entries - ) - return migrated - - -async def async_add_migration_entity_value( - hass: HomeAssistant, - entity_id: str, - entity_values: ZWaveDeviceEntityValues, -) -> None: - """Add Z-Wave entity value for legacy Z-Wave migration.""" - migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass) - migration_handler.add_entity_value(entity_id, entity_values) - - -async def async_get_migration_data( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dict[str, ZWaveMigrationData]: - """Return Z-Wave migration data.""" - migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass) - return await migration_handler.get_data(config_entry) - - -@singleton(LEGACY_ZWAVE_MIGRATION) -async def get_legacy_zwave_migration(hass: HomeAssistant) -> LegacyZWaveMigration: - """Return legacy Z-Wave migration handler.""" - migration_handler = LegacyZWaveMigration(hass) - await migration_handler.load_data() - return migration_handler - - -class LegacyZWaveMigration: - """Handle the migration from zwave to ozw and zwave_js.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Set up migration instance.""" - self._hass = hass - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - self._data: dict[str, dict[str, ZWaveMigrationData]] = {} - - async def load_data(self) -> None: - """Load Z-Wave migration data.""" - stored = cast(dict, await self._store.async_load()) - if stored: - self._data = stored - - @callback - def save_data( - self, config_entry_id: str, entity_id: str, data: ZWaveMigrationData - ) -> None: - """Save Z-Wave migration data.""" - if config_entry_id not in self._data: - self._data[config_entry_id] = {} - self._data[config_entry_id][entity_id] = data - self._store.async_delay_save(self._data_to_save, STORAGE_WRITE_DELAY) - - @callback - def _data_to_save(self) -> dict[str, dict[str, ZWaveMigrationData]]: - """Return data to save.""" - return self._data - - @callback - def add_entity_value( - self, - entity_id: str, - entity_values: ZWaveDeviceEntityValues, - ) -> None: - """Add info for one entity and Z-Wave value.""" - ent_reg = async_get_entity_registry(self._hass) - dev_reg = async_get_device_registry(self._hass) - - node = entity_values.primary.node - entity_entry = ent_reg.async_get(entity_id) - assert entity_entry - device_identifier, _ = node_device_id_and_name( - node, entity_values.primary.instance - ) - device_entry = dev_reg.async_get_device({device_identifier}, set()) - assert device_entry - - # Normalize unit of measurement. - if unit := entity_entry.unit_of_measurement: - unit = unit.lower() - if unit == "": - unit = None - - data: ZWaveMigrationData = { - "node_id": node.node_id, - "node_instance": entity_values.primary.instance, - "command_class": entity_values.primary.command_class, - "command_class_label": entity_values.primary.label, - "value_index": entity_values.primary.index, - "device_id": device_entry.id, - "domain": entity_entry.domain, - "entity_id": entity_id, - "unique_id": entity_entry.unique_id, - "unit_of_measurement": unit, - } - - self.save_data(entity_entry.config_entry_id, entity_id, data) - - async def get_data( - self, config_entry: ConfigEntry - ) -> dict[str, ZWaveMigrationData]: - """Return Z-Wave migration data.""" - await self.load_data() - data = self._data.get(config_entry.entry_id) - return data or {} diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py deleted file mode 100644 index ade68284313..00000000000 --- a/homeassistant/components/zwave/node_entity.py +++ /dev/null @@ -1,381 +0,0 @@ -"""Entity class that represents Z-Wave node.""" -# pylint: disable=import-error -# pylint: disable=import-outside-toplevel -from itertools import count - -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_ENTITY_ID, - ATTR_VIA_DEVICE, - ATTR_WAKEUP, -) -from homeassistant.core import callback -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.entity_registry import async_get_registry - -from .const import ( - ATTR_BASIC_LEVEL, - ATTR_NODE_ID, - ATTR_SCENE_DATA, - ATTR_SCENE_ID, - COMMAND_CLASS_CENTRAL_SCENE, - COMMAND_CLASS_VERSION, - COMMAND_CLASS_WAKE_UP, - DOMAIN, - EVENT_NODE_EVENT, - EVENT_SCENE_ACTIVATED, -) -from .util import is_node_parsed, node_device_id_and_name, node_name - -ATTR_QUERY_STAGE = "query_stage" -ATTR_AWAKE = "is_awake" -ATTR_READY = "is_ready" -ATTR_FAILED = "is_failed" -ATTR_PRODUCT_NAME = "product_name" -ATTR_MANUFACTURER_NAME = "manufacturer_name" -ATTR_NODE_NAME = "node_name" -ATTR_APPLICATION_VERSION = "application_version" - -STAGE_COMPLETE = "Complete" - -_REQUIRED_ATTRIBUTES = [ - ATTR_QUERY_STAGE, - ATTR_AWAKE, - ATTR_READY, - ATTR_FAILED, - "is_info_received", - "max_baud_rate", - "is_zwave_plus", -] -_OPTIONAL_ATTRIBUTES = ["capabilities", "neighbors", "location"] -_COMM_ATTRIBUTES = [ - "sentCnt", - "sentFailed", - "retries", - "receivedCnt", - "receivedDups", - "receivedUnsolicited", - "sentTS", - "receivedTS", - "lastRequestRTT", - "averageRequestRTT", - "lastResponseRTT", - "averageResponseRTT", -] -ATTRIBUTES = _REQUIRED_ATTRIBUTES + _OPTIONAL_ATTRIBUTES - - -class ZWaveBaseEntity(Entity): - """Base class for Z-Wave Node and Value entities.""" - - def __init__(self): - """Initialize the base Z-Wave class.""" - self._update_scheduled = False - - def maybe_schedule_update(self): - """Maybe schedule state update. - - If value changed after device was created but before setup_platform - was called - skip updating state. - """ - if self.hass and not self._update_scheduled: - self.hass.add_job(self._schedule_update) - - @callback - def _schedule_update(self): - """Schedule delayed update.""" - if self._update_scheduled: - return - - @callback - def do_update(): - """Really update.""" - self.async_write_ha_state() - self._update_scheduled = False - - self._update_scheduled = True - self.hass.loop.call_later(0.1, do_update) - - def try_remove_and_add(self): - """Remove this entity and add it back.""" - - async def _async_remove_and_add(): - await self.async_remove(force_remove=True) - self.entity_id = None - await self.platform.async_add_entities([self]) - - if self.hass and self.platform: - self.hass.add_job(_async_remove_and_add) - - async def node_removed(self): - """Call when a node is removed from the Z-Wave network.""" - await self.async_remove(force_remove=True) - - registry = await async_get_registry(self.hass) - if self.entity_id not in registry.entities: - return - - registry.async_remove(self.entity_id) - - -class ZWaveNodeEntity(ZWaveBaseEntity): - """Representation of a Z-Wave node.""" - - def __init__(self, node, network): - """Initialize node.""" - super().__init__() - from openzwave.network import ZWaveNetwork - from pydispatch import dispatcher - - self._network = network - self.node = node - self.node_id = self.node.node_id - self._name = node_name(self.node) - self._product_name = node.product_name - self._manufacturer_name = node.manufacturer_name - self._unique_id = self._compute_unique_id() - self._application_version = None - self._attributes = {} - self.wakeup_interval = None - self.location = None - self.battery_level = None - dispatcher.connect( - self.network_node_value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED - ) - dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) - dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NODE) - dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NOTIFICATION) - dispatcher.connect(self.network_node_event, ZWaveNetwork.SIGNAL_NODE_EVENT) - dispatcher.connect( - self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT - ) - - @property - def unique_id(self): - """Return unique ID of Z-wave node.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - identifier, name = node_device_id_and_name(self.node) - info = DeviceInfo( - identifiers={identifier}, - manufacturer=self.node.manufacturer_name, - model=self.node.product_name, - name=name, - ) - if self.node_id > 1: - info[ATTR_VIA_DEVICE] = (DOMAIN, 1) - return info - - def maybe_update_application_version(self, value): - """Update application version if value is a Command Class Version, Application Value.""" - if ( - value - and value.command_class == COMMAND_CLASS_VERSION - and value.label == "Application Version" - ): - self._application_version = value.data - - def network_node_value_added(self, node=None, value=None, args=None): - """Handle a added value to a none on the network.""" - if node and node.node_id != self.node_id: - return - if args is not None and "nodeId" in args and args["nodeId"] != self.node_id: - return - - self.maybe_update_application_version(value) - - def network_node_changed(self, node=None, value=None, args=None): - """Handle a changed node on the network.""" - if node and node.node_id != self.node_id: - return - if args is not None and "nodeId" in args and args["nodeId"] != self.node_id: - return - - # Process central scene activation - if value is not None and value.command_class == COMMAND_CLASS_CENTRAL_SCENE: - self.central_scene_activated(value.index, value.data) - - self.maybe_update_application_version(value) - - self.node_changed() - - def get_node_statistics(self): - """Retrieve statistics from the node.""" - return self._network.manager.getNodeStatistics( - self._network.home_id, self.node_id - ) - - def node_changed(self): - """Update node properties.""" - attributes = {} - stats = self.get_node_statistics() - for attr in ATTRIBUTES: - value = getattr(self.node, attr) - if attr in _REQUIRED_ATTRIBUTES or value: - attributes[attr] = value - - for attr in _COMM_ATTRIBUTES: - attributes[attr] = stats[attr] - - if self.node.can_wake_up(): - for value in self.node.get_values(COMMAND_CLASS_WAKE_UP).values(): - if value.index != 0: - continue - - self.wakeup_interval = value.data - break - else: - self.wakeup_interval = None - - self.battery_level = self.node.get_battery_level() - self._product_name = self.node.product_name - self._manufacturer_name = self.node.manufacturer_name - self._name = node_name(self.node) - self._attributes = attributes - - if not self._unique_id: - self._unique_id = self._compute_unique_id() - if self._unique_id: - # Node info parsed. Remove and re-add - self.try_remove_and_add() - - self.maybe_schedule_update() - - async def node_renamed(self, update_ids=False): - """Rename the node and update any IDs.""" - identifier, self._name = node_device_id_and_name(self.node) - # Set the name in the devices. If they're customised - # the customisation will not be stored as name and will stick. - dev_reg = await get_dev_reg(self.hass) - device = dev_reg.async_get_device(identifiers={identifier}) - dev_reg.async_update_device(device.id, name=self._name) - # update sub-devices too - for i in count(2): - identifier, new_name = node_device_id_and_name(self.node, i) - device = dev_reg.async_get_device(identifiers={identifier}) - if not device: - break - dev_reg.async_update_device(device.id, name=new_name) - - # Update entity ID. - if update_ids: - ent_reg = await async_get_registry(self.hass) - new_entity_id = ent_reg.async_generate_entity_id( - DOMAIN, self._name, self.platform.entities.keys() - {self.entity_id} - ) - if new_entity_id != self.entity_id: - # Don't change the name attribute, it will be None unless - # customised and if it's been customised, keep the - # customisation. - ent_reg.async_update_entity(self.entity_id, new_entity_id=new_entity_id) - return - # else for the above two ifs, update if not using update_entity - self.async_write_ha_state() - - def network_node_event(self, node, value): - """Handle a node activated event on the network.""" - if node.node_id == self.node.node_id: - self.node_event(value) - - def node_event(self, value): - """Handle a node activated event for this node.""" - if self.hass is None: - return - - self.hass.bus.fire( - EVENT_NODE_EVENT, - { - ATTR_ENTITY_ID: self.entity_id, - ATTR_NODE_ID: self.node.node_id, - ATTR_BASIC_LEVEL: value, - }, - ) - - def network_scene_activated(self, node, scene_id): - """Handle a scene activated event on the network.""" - if node.node_id == self.node.node_id: - self.scene_activated(scene_id) - - def scene_activated(self, scene_id): - """Handle an activated scene for this node.""" - if self.hass is None: - return - - self.hass.bus.fire( - EVENT_SCENE_ACTIVATED, - { - ATTR_ENTITY_ID: self.entity_id, - ATTR_NODE_ID: self.node.node_id, - ATTR_SCENE_ID: scene_id, - }, - ) - - def central_scene_activated(self, scene_id, scene_data): - """Handle an activated central scene for this node.""" - if self.hass is None: - return - - self.hass.bus.fire( - EVENT_SCENE_ACTIVATED, - { - ATTR_ENTITY_ID: self.entity_id, - ATTR_NODE_ID: self.node_id, - ATTR_SCENE_ID: scene_id, - ATTR_SCENE_DATA: scene_data, - }, - ) - - @property - def state(self): - """Return the state.""" - if ATTR_READY not in self._attributes: - return None - - if self._attributes[ATTR_FAILED]: - return "dead" - if self._attributes[ATTR_QUERY_STAGE] != "Complete": - return "initializing" - if not self._attributes[ATTR_AWAKE]: - return "sleeping" - if self._attributes[ATTR_READY]: - return "ready" - - return None - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - attrs = { - ATTR_NODE_ID: self.node_id, - ATTR_NODE_NAME: self._name, - ATTR_MANUFACTURER_NAME: self._manufacturer_name, - ATTR_PRODUCT_NAME: self._product_name, - } - attrs.update(self._attributes) - if self.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = self.battery_level - if self.wakeup_interval is not None: - attrs[ATTR_WAKEUP] = self.wakeup_interval - if self._application_version is not None: - attrs[ATTR_APPLICATION_VERSION] = self._application_version - - return attrs - - def _compute_unique_id(self): - if is_node_parsed(self.node) or self.node.is_ready: - return f"node-{self.node_id}" - return None diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py deleted file mode 100644 index 1f32f8bb681..00000000000 --- a/homeassistant/components/zwave/sensor.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Support for Z-Wave sensors.""" -from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, const - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Sensor from Config Entry.""" - - @callback - def async_add_sensor(sensor): - """Add Z-Wave Sensor.""" - async_add_entities([sensor]) - - async_dispatcher_connect(hass, "zwave_new_sensor", async_add_sensor) - - -def get_device(node, values, **kwargs): - """Create Z-Wave entity device.""" - # Generic Device mappings - if values.primary.command_class == const.COMMAND_CLASS_BATTERY: - return ZWaveBatterySensor(values) - if node.has_command_class(const.COMMAND_CLASS_SENSOR_MULTILEVEL): - return ZWaveMultilevelSensor(values) - if ( - node.has_command_class(const.COMMAND_CLASS_METER) - and values.primary.type == const.TYPE_DECIMAL - ): - return ZWaveMultilevelSensor(values) - if node.has_command_class(const.COMMAND_CLASS_ALARM) or node.has_command_class( - const.COMMAND_CLASS_SENSOR_ALARM - ): - return ZWaveAlarmSensor(values) - return None - - -class ZWaveSensor(ZWaveDeviceEntity, SensorEntity): - """Representation of a Z-Wave sensor.""" - - def __init__(self, values): - """Initialize the sensor.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.update_properties() - - def update_properties(self): - """Handle the data changes for node values.""" - self._state = self.values.primary.data - self._units = self.values.primary.units - - @property - def force_update(self): - """Return force_update.""" - return True - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement the value is expressed in.""" - return self._units - - -class ZWaveMultilevelSensor(ZWaveSensor): - """Representation of a multi level sensor Z-Wave sensor.""" - - @property - def native_value(self): - """Return the state of the sensor.""" - if self._units in ("C", "F"): - return round(self._state, 1) - if isinstance(self._state, float): - return round(self._state, 2) - - return self._state - - @property - def device_class(self): - """Return the class of this device.""" - if self._units in ["C", "F"]: - return SensorDeviceClass.TEMPERATURE - return None - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - if self._units == "C": - return TEMP_CELSIUS - if self._units == "F": - return TEMP_FAHRENHEIT - return self._units - - -class ZWaveAlarmSensor(ZWaveSensor): - """Representation of a Z-Wave sensor that sends Alarm alerts. - - Examples include certain Multisensors that have motion and vibration - capabilities. Z-Wave defines various alarm types such as Smoke, Flood, - Burglar, CarbonMonoxide, etc. - - This wraps these alarms and allows you to use them to trigger things, etc. - - COMMAND_CLASS_ALARM is what we get here. - """ - - -class ZWaveBatterySensor(ZWaveSensor): - """Representation of Z-Wave device battery level.""" - - @property - def device_class(self): - """Return the class of this device.""" - return SensorDeviceClass.BATTERY diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml deleted file mode 100644 index d3063ef5d43..00000000000 --- a/homeassistant/components/zwave/services.yaml +++ /dev/null @@ -1,411 +0,0 @@ -# Describes the format for available Z-Wave services - -change_association: - name: Change association - description: Change an association in the Z-Wave network. - fields: - association: - name: Association - description: Specify add or remove association - required: true - example: add - selector: - text: - node_id: - name: Node ID - description: Node id of the node to set association for. - required: true - selector: - number: - min: 1 - max: 255 - target_node_id: - name: Target node ID - description: Node id of the node to associate to. - required: true - selector: - number: - min: 1 - max: 255 - group: - name: Group - description: Group number to set association for. - required: true - selector: - number: - min: 1 - max: 5 - instance: - name: Instance - description: Instance of multichannel association. - default: 0 - selector: - number: - min: 0 - max: 255 - -add_node: - name: Add node - description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW_Log.txt for progress. - -add_node_secure: - name: Add node secure - description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW_Log.txt for progress. - -cancel_command: - name: Cancel command - description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you weren't going to use it but activated it. - -heal_network: - name: Heal network - description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. - fields: - return_routes: - name: Return routes - description: Whether or not to update the return routes from the nodes to the controller. - default: false - selector: - boolean: - -heal_node: - name: Heal node - description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. - fields: - return_routes: - name: Return routes - description: Whether or not to update the return routes from the node to the controller. - default: false - selector: - boolean: - -remove_node: - name: Remove node - description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. - -remove_failed_node: - name: Remove failed node - description: This command will remove a failed node from the network. The node should be on the controller's failed nodes list, otherwise this command will fail. Refer to OZW_Log.txt for progress. - fields: - node_id: - name: Node ID - description: Node id of the device to remove. - required: true - selector: - number: - min: 1 - max: 255 - -replace_failed_node: - name: Replace failed node - description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW_Log.txt for progress. - fields: - node_id: - name: Node ID - description: Node id of the device to replace. - required: true - selector: - number: - min: 1 - max: 255 - -set_config_parameter: - name: Set config parameter - description: Set a config parameter to a node on the Z-Wave network. - fields: - node_id: - name: Node ID - description: Node id of the device to set config parameter to. - required: true - selector: - number: - min: 1 - max: 255 - parameter: - name: Parameter - description: Parameter number to set. - required: true - selector: - number: - min: 1 - max: 255 - value: - name: Value - description: Value to set for parameter. (String value for list and bool parameters, integer for others). - required: true - selector: - text: - size: - name: Size - description: Set the size of the parameter value. Only needed if no parameters are available. - default: 2 - selector: - number: - min: 1 - max: 255 - -set_node_value: - name: Set node value - description: Set the value for a given value_id on a Z-Wave device. - fields: - node_id: - name: Node ID - description: Node id of the device to set the value on. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: Value id of the value to set (integer or string). - required: true - selector: - text: - value: - name: Value - description: Value to set (integer or string). - required: true - selector: - text: - -refresh_node_value: - name: Refresh node value - description: Refresh the value for a given value_id on a Z-Wave device. - fields: - node_id: - name: Node ID - description: Node id of the device to refresh value from. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: Value id of the value to refresh. - required: true - selector: - text: - -set_poll_intensity: - name: Set poll intensity - description: Set the polling interval to a nodes value - fields: - node_id: - name: Node ID - description: ID of the node to set polling to. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: ID of the value to set polling to. - example: 72037594255792737 - required: true - selector: - text: - poll_intensity: - name: Poll intensity - description: The intensity to poll, 0 = disabled, 1 = Every time through list, 2 = Every second time through list... - required: true - selector: - number: - min: 0 - max: 100 - -print_config_parameter: - name: Print configuration parameter - description: Prints a Z-Wave node config parameter value to log. - fields: - node_id: - name: Node ID - description: Node id of the device to print the parameter from. - required: true - selector: - number: - min: 1 - max: 255 - parameter: - name: Parameter - description: Parameter number to print. - required: true - selector: - number: - min: 1 - max: 255 - -print_node: - name: Print node - description: Print all information about z-wave node. - fields: - node_id: - name: Node ID - description: Node id of the device to print. - required: true - selector: - number: - min: 1 - max: 255 - -refresh_entity: - name: Refresh entity - description: Refresh zwave entity. - fields: - entity_id: - name: Entity - description: Name of the entity to refresh. - required: true - selector: - entity: - integration: zwave - -refresh_node: - name: Refresh node - description: Refresh zwave node. - fields: - node_id: - name: Node ID - description: ID of the node to refresh. - required: true - selector: - number: - min: 1 - max: 255 - -set_wakeup: - name: Set wakeup - description: Sets wake-up interval of a node. - fields: - node_id: - name: Node ID - description: Node id of the device to set the wake-up interval for. - required: true - selector: - number: - min: 1 - max: 255 - value: - name: Value - description: Value of the interval to set. - required: true - selector: - text: - -start_network: - name: Start network - description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. - -stop_network: - name: Stop network - description: Stop the Z-Wave network, all updates into Home Assistant will stop. - -soft_reset: - name: Soft reset - description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. - -test_network: - name: Test network - description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW_Log.txt for progress. - -test_node: - name: Test node - description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes. - fields: - node_id: - name: Node ID - description: ID of the node to send test messages to. - required: true - selector: - number: - min: 1 - max: 255 - messages: - name: Messages - description: Amount of test messages to send. - default: 1 - selector: - number: - min: 1 - max: 100 - -rename_node: - name: Rename node - description: Set the name of a node. This will also affect the IDs of all entities in the node. - fields: - node_id: - name: Node ID - description: ID of the node to rename. - required: true - selector: - number: - min: 1 - max: 255 - update_ids: - name: Update IDs - description: Rename the entity IDs for entities of this node. - default: false - selector: - boolean: - name: - name: Name - description: New Name - required: true - example: "kitchen" - selector: - text: - -rename_value: - name: Rename value - description: Set the name of a node value. This will affect the ID of the value entity. Value IDs can be queried from /api/zwave/values/{node_id} - fields: - node_id: - name: Node ID - description: ID of the node to rename. - required: true - selector: - number: - min: 1 - max: 255 - value_id: - name: Value ID - description: ID of the value to rename. - example: 72037594255792737 - required: true - selector: - text: - update_ids: - name: Update IDs - description: Update the entity ID for this value's entity. - default: false - selector: - boolean: - name: - name: Name - description: New Name - example: "Luminosity" - required: true - selector: - text: - -reset_node_meters: - name: Reset node meters - description: Resets the meter counters of a node. - fields: - node_id: - name: Node ID - description: Node id of the device to reset meters for. - required: true - selector: - number: - min: 1 - max: 255 - instance: - name: Instance - description: Instance of association. - default: 1 - selector: - number: - min: 1 - max: 100 diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json deleted file mode 100644 index 69401b171e2..00000000000 --- a/homeassistant/components/zwave/strings.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", - "data": { - "usb_path": "[%key:common::config_flow::data::usb_path%]", - "network_key": "Network Key (leave blank to auto-generate)" - } - } - }, - "error": { - "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - } - }, - "state": { - "query_stage": { - "initializing": "[%key:component::zwave::state::_::initializing%]", - "dead": "[%key:component::zwave::state::_::dead%]" - }, - "_": { - "initializing": "Initializing", - "dead": "Dead", - "sleeping": "Sleeping", - "ready": "Ready" - } - } -} diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py deleted file mode 100644 index f7d3471e2ab..00000000000 --- a/homeassistant/components/zwave/switch.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Support for Z-Wave switches.""" -import time - -from homeassistant.components.switch import DOMAIN, SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import ZWaveDeviceEntity, workaround - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Z-Wave Switch from Config Entry.""" - - @callback - def async_add_switch(switch): - """Add Z-Wave Switch.""" - async_add_entities([switch]) - - async_dispatcher_connect(hass, "zwave_new_switch", async_add_switch) - - -def get_device(values, **kwargs): - """Create zwave entity device.""" - return ZwaveSwitch(values) - - -class ZwaveSwitch(ZWaveDeviceEntity, SwitchEntity): - """Representation of a Z-Wave switch.""" - - def __init__(self, values): - """Initialize the Z-Wave switch device.""" - ZWaveDeviceEntity.__init__(self, values, DOMAIN) - self.refresh_on_update = ( - workaround.get_device_mapping(values.primary) - == workaround.WORKAROUND_REFRESH_NODE_ON_UPDATE - ) - self.last_update = time.perf_counter() - self._state = self.values.primary.data - - def update_properties(self): - """Handle data changes for node values.""" - self._state = self.values.primary.data - if self.refresh_on_update and time.perf_counter() - self.last_update > 30: - self.last_update = time.perf_counter() - self.node.request_state() - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn the device on.""" - self.node.set_switch(self.values.primary.value_id, True) - - def turn_off(self, **kwargs): - """Turn the device off.""" - self.node.set_switch(self.values.primary.value_id, False) diff --git a/homeassistant/components/zwave/translations/af.json b/homeassistant/components/zwave/translations/af.json deleted file mode 100644 index 155960b3884..00000000000 --- a/homeassistant/components/zwave/translations/af.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Dood", - "initializing": "Inisialiseer", - "ready": "Gereed", - "sleeping": "Aan die slaap" - }, - "query_stage": { - "dead": "Dood ({query_stage})", - "initializing": "Inisialiseer ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ar.json b/homeassistant/components/zwave/translations/ar.json deleted file mode 100644 index 5dc1469d468..00000000000 --- a/homeassistant/components/zwave/translations/ar.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0645\u0641\u0635\u0648\u0644", - "initializing": "\u0642\u064a\u062f \u0627\u0644\u0625\u0646\u0634\u0627\u0621", - "ready": "\u062c\u0627\u0647\u0632", - "sleeping": "\u0646\u0627\u0626\u0645" - }, - "query_stage": { - "dead": "\u0645\u0641\u0635\u0648\u0644 ({query_stage})", - "initializing": "\u0642\u064a\u062f \u0627\u0644\u0625\u0646\u0634\u0627\u0621 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/bg.json b/homeassistant/components/zwave/translations/bg.json deleted file mode 100644 index ae82e98d705..00000000000 --- a/homeassistant/components/zwave/translations/bg.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" - }, - "error": { - "option_error": "\u0412\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Z-Wave \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e. \u041f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043b\u0438 \u0435 \u043f\u044a\u0442\u044f\u0442 \u043a\u044a\u043c USB \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e?" - }, - "step": { - "user": { - "data": { - "network_key": "\u041c\u0440\u0435\u0436\u043e\u0432 \u043a\u043b\u044e\u0447 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435)", - "usb_path": "USB \u043f\u044a\u0442" - }, - "description": "\u0412\u0438\u0436\u0442\u0435 https://www.home-assistant.io/docs/z-wave/installation/ \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442\u043d\u043e\u0441\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0438" - } - } - }, - "state": { - "_": { - "dead": "\u041c\u044a\u0440\u0442\u044a\u0432", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", - "ready": "\u0413\u043e\u0442\u043e\u0432", - "sleeping": "\u0421\u043f\u044f\u0449" - }, - "query_stage": { - "dead": "\u041c\u044a\u0440\u0442\u044a\u0432 ({query_stage})", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/bs.json b/homeassistant/components/zwave/translations/bs.json deleted file mode 100644 index 8d58bad5606..00000000000 --- a/homeassistant/components/zwave/translations/bs.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Mrtav", - "initializing": "Inicijalizacija", - "ready": "Spreman", - "sleeping": "Spava" - }, - "query_stage": { - "dead": "Mrtav ({query_stage})", - "initializing": "Inicijalizacija ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json deleted file mode 100644 index 4b9e8953b8a..00000000000 --- a/homeassistant/components/zwave/translations/ca.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "error": { - "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port USB on hi ha connectat el dispositiu?" - }, - "step": { - "user": { - "data": { - "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", - "usb_path": "Ruta del dispositiu USB" - }, - "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3" - } - } - }, - "state": { - "_": { - "dead": "No disponible", - "initializing": "Inicialitzant", - "ready": "A punt", - "sleeping": "Dormint" - }, - "query_stage": { - "dead": "No disponible", - "initializing": "Inicialitzant" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/cs.json b/homeassistant/components/zwave/translations/cs.json deleted file mode 100644 index 320feafe08a..00000000000 --- a/homeassistant/components/zwave/translations/cs.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "error": { - "option_error": "Z-Wave ov\u011b\u0159en\u00ed se nezda\u0159ilo. Je cesta k USB za\u0159\u00edzen\u00ed spr\u00e1vn\u011b?" - }, - "step": { - "user": { - "data": { - "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d (ponechte pr\u00e1zdn\u00e9 pro automatick\u00e9 generov\u00e1n\u00ed)", - "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" - }, - "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch" - } - } - }, - "state": { - "_": { - "dead": "Nereaguje", - "initializing": "Inicializace", - "ready": "P\u0159ipraveno", - "sleeping": "\u00dasporn\u00fd re\u017eim" - }, - "query_stage": { - "dead": "Nereaguje", - "initializing": "Inicializace" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/cy.json b/homeassistant/components/zwave/translations/cy.json deleted file mode 100644 index 43860e1c1fd..00000000000 --- a/homeassistant/components/zwave/translations/cy.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Marw", - "initializing": "Ymgychwyn", - "ready": "Barod", - "sleeping": "Cysgu" - }, - "query_stage": { - "dead": "Marw ({query_stage})", - "initializing": "Ymgychwyn ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/da.json b/homeassistant/components/zwave/translations/da.json deleted file mode 100644 index 7bad51c4f8e..00000000000 --- a/homeassistant/components/zwave/translations/da.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave er allerede konfigureret" - }, - "error": { - "option_error": "Z-Wave-validering mislykkedes. Er stien til USB-enhed korrekt?" - }, - "step": { - "user": { - "data": { - "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", - "usb_path": "Sti til USB-enhed" - }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler" - } - } - }, - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "Sover" - }, - "query_stage": { - "dead": "D\u00f8d ({query_stage})", - "initializing": "Initialiserer ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json deleted file mode 100644 index 9d432edd1e5..00000000000 --- a/homeassistant/components/zwave/translations/de.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "error": { - "option_error": "Z-Wave-Validierung fehlgeschlagen. Ist der Pfad zum USB-Stick korrekt?" - }, - "step": { - "user": { - "data": { - "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", - "usb_path": "USB-Ger\u00e4te-Pfad" - }, - "description": "Diese Integration wird nicht mehr gepflegt. Verwende bei Neuinstallationen stattdessen Z-Wave JS.\n\nSiehe https://www.home-assistant.io/docs/z-wave/installation/ f\u00fcr Informationen zu den Konfigurationsvariablen" - } - } - }, - "state": { - "_": { - "dead": "Nicht erreichbar", - "initializing": "Initialisierend", - "ready": "Bereit", - "sleeping": "Schlafend" - }, - "query_stage": { - "dead": "Nicht erreichbar ({query_stage})", - "initializing": "Initialisierend" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/el.json b/homeassistant/components/zwave/translations/el.json deleted file mode 100644 index 48b5eba30f6..00000000000 --- a/homeassistant/components/zwave/translations/el.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." - }, - "error": { - "option_error": "\u0397 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 Z-Wave \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5. \u0395\u03af\u03bd\u03b1\u03b9 \u03c3\u03c9\u03c3\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c3\u03c4\u03b9\u03ba\u03ac\u03ba\u03b9 USB;" - }, - "step": { - "user": { - "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1)", - "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" - }, - "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b4\u03b9\u03b1\u03c4\u03b7\u03c1\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u0393\u03b9\u03b1 \u03bd\u03ad\u03b5\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Z-Wave JS. \n\n \u0394\u03b5\u03af\u03c4\u03b5 https://www.home-assistant.io/docs/z-wave/installation/ \u03b3\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ad\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2" - } - } - }, - "state": { - "_": { - "dead": "\u039d\u03b5\u03ba\u03c1\u03cc", - "initializing": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "ready": "\u0388\u03c4\u03bf\u03b9\u03bc\u03bf", - "sleeping": "\u039a\u03bf\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9" - }, - "query_stage": { - "dead": "\u039d\u03b5\u03ba\u03c1\u03cc", - "initializing": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json deleted file mode 100644 index 5c7442fea05..00000000000 --- a/homeassistant/components/zwave/translations/en.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "error": { - "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" - }, - "step": { - "user": { - "data": { - "network_key": "Network Key (leave blank to auto-generate)", - "usb_path": "USB Device Path" - }, - "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables" - } - } - }, - "state": { - "_": { - "dead": "Dead", - "initializing": "Initializing", - "ready": "Ready", - "sleeping": "Sleeping" - }, - "query_stage": { - "dead": "Dead", - "initializing": "Initializing" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/es-419.json b/homeassistant/components/zwave/translations/es-419.json deleted file mode 100644 index abcf85fffa1..00000000000 --- a/homeassistant/components/zwave/translations/es-419.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave ya est\u00e1 configurado" - }, - "error": { - "option_error": "La validaci\u00f3n de Z-Wave fall\u00f3. \u00bfEs correcta la ruta a la memoria USB?" - }, - "step": { - "user": { - "data": { - "network_key": "Clave de red (dejar en blanco para auto-generar)", - "usb_path": "Ruta USB" - }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" - } - } - }, - "state": { - "_": { - "dead": "Desconectado", - "initializing": "Iniciando", - "ready": "Listo", - "sleeping": "Hibernacion" - }, - "query_stage": { - "dead": "Desconectado ({query_stage})", - "initializing": "Iniciando ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/es.json b/homeassistant/components/zwave/translations/es.json deleted file mode 100644 index 0d8b9a3020c..00000000000 --- a/homeassistant/components/zwave/translations/es.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave ya est\u00e1 configurado", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "error": { - "option_error": "Z-Wave error de validaci\u00f3n. \u00bfLa ruta de acceso a la memoria USB escorrecta?" - }, - "step": { - "user": { - "data": { - "network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)", - "usb_path": "Ruta del dispositivo USB" - }, - "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" - } - } - }, - "state": { - "_": { - "dead": "No responde", - "initializing": "Inicializando", - "ready": "Listo", - "sleeping": "Ahorro de energ\u00eda" - }, - "query_stage": { - "dead": "No responde", - "initializing": "Inicializando" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json deleted file mode 100644 index 922126e0d86..00000000000 --- a/homeassistant/components/zwave/translations/et.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "error": { - "option_error": "Z-Wave valideerimine nurjus. Kas USB-m\u00e4lupulga tee on \u00f5ige?" - }, - "step": { - "user": { - "data": { - "network_key": "V\u00f5rguv\u00f5ti (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", - "usb_path": "USB seadme rada" - }, - "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/" - } - } - }, - "state": { - "_": { - "dead": "Surnud", - "initializing": "L\u00e4htestan", - "ready": "Valmis", - "sleeping": "Ootel" - }, - "query_stage": { - "dead": "Surnud", - "initializing": "L\u00e4htestan" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/eu.json b/homeassistant/components/zwave/translations/eu.json deleted file mode 100644 index ceab4ed0d98..00000000000 --- a/homeassistant/components/zwave/translations/eu.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Hilda", - "initializing": "Hasieratzen", - "ready": "Prest", - "sleeping": "Lotan" - }, - "query_stage": { - "dead": "Ez du erantzuten ({query_stage})", - "initializing": "Hasieratzen ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/fa.json b/homeassistant/components/zwave/translations/fa.json deleted file mode 100644 index 21d9a0c0fb7..00000000000 --- a/homeassistant/components/zwave/translations/fa.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0645\u0631\u062f\u0647", - "initializing": "\u062f\u0631 \u062d\u0627\u0644 \u0622\u0645\u0627\u062f\u0647 \u0634\u062f\u0646", - "ready": "\u0622\u0645\u0627\u062f\u0647", - "sleeping": "\u062f\u0631 \u062d\u0627\u0644 \u062e\u0648\u0627\u0628" - }, - "query_stage": { - "dead": "\u0645\u0631\u062f\u0647 ({query_stage})", - "initializing": "\u062f\u0631 \u062d\u0627\u0644 \u0622\u0645\u0627\u062f\u0647 \u0634\u062f\u0646 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/fi.json b/homeassistant/components/zwave/translations/fi.json deleted file mode 100644 index 90fb77b49e1..00000000000 --- a/homeassistant/components/zwave/translations/fi.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "usb_path": "USB-polku" - } - } - } - }, - "state": { - "_": { - "dead": "Kuollut", - "initializing": "Alustaa", - "ready": "Valmis", - "sleeping": "Lepotilassa" - }, - "query_stage": { - "dead": "Kuollut ({query_stage})", - "initializing": "Alustaa ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/fr.json b/homeassistant/components/zwave/translations/fr.json deleted file mode 100644 index 280d86e1537..00000000000 --- a/homeassistant/components/zwave/translations/fr.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "error": { - "option_error": "La validation Z-Wave a \u00e9chou\u00e9. Le chemin d'acc\u00e8s \u00e0 la cl\u00e9 USB est-il correct?" - }, - "step": { - "user": { - "data": { - "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration." - } - } - }, - "state": { - "_": { - "dead": "Morte", - "initializing": "Initialisation", - "ready": "Pr\u00eat", - "sleeping": "En veille" - }, - "query_stage": { - "dead": "Morte", - "initializing": "Initialisation" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/gsw.json b/homeassistant/components/zwave/translations/gsw.json deleted file mode 100644 index fb704e97c7d..00000000000 --- a/homeassistant/components/zwave/translations/gsw.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Tod", - "initializing": "Inizialisi\u00e4r\u00e4", - "ready": "Parat", - "sleeping": "Schlaf\u00e4" - }, - "query_stage": { - "dead": "Tod ({query_stage})", - "initializing": "Inizialisi\u00e4r\u00e4 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/he.json b/homeassistant/components/zwave/translations/he.json deleted file mode 100644 index 9cbf39a6d16..00000000000 --- a/homeassistant/components/zwave/translations/he.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "step": { - "user": { - "data": { - "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" - } - } - } - }, - "state": { - "_": { - "dead": "\u05de\u05ea", - "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc", - "ready": "\u05de\u05d5\u05db\u05df", - "sleeping": "\u05d9\u05e9\u05df" - }, - "query_stage": { - "dead": "\u05de\u05ea", - "initializing": "\u05d0\u05ea\u05d7\u05d5\u05dc" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hi.json b/homeassistant/components/zwave/translations/hi.json deleted file mode 100644 index 99e98c4aa9f..00000000000 --- a/homeassistant/components/zwave/translations/hi.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u092e\u0943\u0924", - "initializing": "\u0906\u0930\u0902\u092d", - "ready": "\u0924\u0948\u092f\u093e\u0930", - "sleeping": "\u0938\u094b\u092f\u093e \u0939\u0941\u0906" - }, - "query_stage": { - "dead": " ( {query_stage} )", - "initializing": "\u0906\u0930\u0902\u092d ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hr.json b/homeassistant/components/zwave/translations/hr.json deleted file mode 100644 index dbff348b761..00000000000 --- a/homeassistant/components/zwave/translations/hr.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Mrtav", - "initializing": "Inicijalizacija", - "ready": "Spreman", - "sleeping": "Spavanje" - }, - "query_stage": { - "dead": "Mrtav ({query_stage})", - "initializing": "Inicijalizacija ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json deleted file mode 100644 index 4d0c6adff59..00000000000 --- a/homeassistant/components/zwave/translations/hu.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "error": { - "option_error": "A Z-Wave \u00e9rv\u00e9nyes\u00edt\u00e9s sikertelen. Az USB-meghajt\u00f3 el\u00e9r\u00e9si \u00fatj\u00e1t helyesen adtad meg?" - }, - "step": { - "user": { - "data": { - "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", - "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" - }, - "description": "Ezt az integr\u00e1ci\u00f3t m\u00e1r nem tartj\u00e1k fenn. \u00daj telep\u00edt\u00e9sek eset\u00e9n haszn\u00e1lja helyette a Z-Wave JS-t.\n\nA konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kkal kapcsolatos inform\u00e1ci\u00f3k\u00e9rt l\u00e1sd https://www.home-assistant.io/docs/z-wave/installation/." - } - } - }, - "state": { - "_": { - "dead": "Nem ad \u00e9letjelet", - "initializing": "Inicializ\u00e1l\u00e1s", - "ready": "K\u00e9sz", - "sleeping": "Alv\u00e1s" - }, - "query_stage": { - "dead": "Nem ad \u00e9letjelet", - "initializing": "Inicializ\u00e1l\u00e1s" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/hy.json b/homeassistant/components/zwave/translations/hy.json deleted file mode 100644 index c4fa19f700a..00000000000 --- a/homeassistant/components/zwave/translations/hy.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0544\u0565\u057c\u0561\u056e", - "initializing": "\u0546\u0561\u056d\u0561\u0571\u0565\u057c\u0576\u0578\u0572", - "ready": "\u054a\u0561\u057f\u0580\u0561\u057d\u057f \u0567", - "sleeping": "\u0554\u0576\u0565\u056c" - }, - "query_stage": { - "dead": "\u0544\u0561\u0570\u0561\u0581\u0561\u056e{query_stage})", - "initializing": "\u0546\u0561\u056d\u0561\u0571\u0565\u057c\u0576\u0578\u0582\u0569\u0575\u0578\u0582\u0576({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/id.json b/homeassistant/components/zwave/translations/id.json deleted file mode 100644 index 91301f6e00e..00000000000 --- a/homeassistant/components/zwave/translations/id.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "error": { - "option_error": "Validasi Z-Wave gagal. Apakah jalur ke stik USB sudah benar?" - }, - "step": { - "user": { - "data": { - "network_key": "Kunci Jaringan (biarkan kosong untuk dibuat secara otomatis)", - "usb_path": "Jalur Perangkat USB" - }, - "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi" - } - } - }, - "state": { - "_": { - "dead": "Mati", - "initializing": "Inisialisasi", - "ready": "Siap", - "sleeping": "Tidur" - }, - "query_stage": { - "dead": "Mati", - "initializing": "Inisialisasi" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/is.json b/homeassistant/components/zwave/translations/is.json deleted file mode 100644 index bb54fd48425..00000000000 --- a/homeassistant/components/zwave/translations/is.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Dau\u00f0ur", - "initializing": "Frumstilli", - "ready": "Tilb\u00fainn", - "sleeping": "\u00cd dvala" - }, - "query_stage": { - "dead": "Dau\u00f0ur ({query_stage})", - "initializing": "Frumstilli ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json deleted file mode 100644 index 17207c23b50..00000000000 --- a/homeassistant/components/zwave/translations/it.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "error": { - "option_error": "Convalida Z-Wave non riuscita. Il percorso della chiavetta USB \u00e8 corretto?" - }, - "step": { - "user": { - "data": { - "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", - "usb_path": "Percorso del dispositivo USB" - }, - "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione" - } - } - }, - "state": { - "_": { - "dead": "Disattivo", - "initializing": "In avvio", - "ready": "Pronto", - "sleeping": "Dormiente" - }, - "query_stage": { - "dead": "Disattivo", - "initializing": "In avvio" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ja.json b/homeassistant/components/zwave/translations/ja.json deleted file mode 100644 index ff0afe58c0e..00000000000 --- a/homeassistant/components/zwave/translations/ja.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" - }, - "error": { - "option_error": "Z-Wave\u306e\u691c\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002USB\u30b9\u30c6\u30a3\u30c3\u30af\u3078\u306e\u30d1\u30b9\u306f\u6b63\u3057\u3044\u3067\u3059\u304b\uff1f" - }, - "step": { - "user": { - "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", - "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306f\u7d42\u4e86\u3057\u307e\u3057\u305f\u3002\u65b0\u898f\u306b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3059\u308b\u5834\u5408\u306f\u3001\u4ee3\u308f\u308a\u306bZ-Wave JS\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u69cb\u6210\u5909\u6570\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/docs/z-wave/installation/ \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" - } - } - }, - "state": { - "_": { - "dead": "\u30c7\u30c3\u30c9", - "initializing": "\u521d\u671f\u5316\u4e2d", - "ready": "\u6e96\u5099\u5b8c\u4e86", - "sleeping": "\u30b9\u30ea\u30fc\u30d7" - }, - "query_stage": { - "dead": "\u30c7\u30c3\u30c9", - "initializing": "\u521d\u671f\u5316\u4e2d" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json deleted file mode 100644 index 613b4108d22..00000000000 --- a/homeassistant/components/zwave/translations/ko.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "error": { - "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" - }, - "step": { - "user": { - "data": { - "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", - "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" - }, - "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694" - } - } - }, - "state": { - "_": { - "dead": "\uc751\ub2f5\uc5c6\uc74c", - "initializing": "\ucd08\uae30\ud654\uc911", - "ready": "\uc900\ube44", - "sleeping": "\uc808\uc804\ubaa8\ub4dc" - }, - "query_stage": { - "dead": "\uc751\ub2f5\uc5c6\uc74c", - "initializing": "\ucd08\uae30\ud654\uc911" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/lb.json b/homeassistant/components/zwave/translations/lb.json deleted file mode 100644 index e41d37b2025..00000000000 --- a/homeassistant/components/zwave/translations/lb.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." - }, - "error": { - "option_error": "Z-Wave Validatioun net g\u00eblteg. Ass de Pad zum USB Stick richteg?" - }, - "step": { - "user": { - "data": { - "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", - "usb_path": "Pad zum USB Apparat" - }, - "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen" - } - } - }, - "state": { - "_": { - "dead": "Doud", - "initializing": "Initialis\u00e9iert", - "ready": "Bereet", - "sleeping": "Schl\u00e9ift" - }, - "query_stage": { - "dead": "Doud", - "initializing": "Initialis\u00e9iert" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/lt.json b/homeassistant/components/zwave/translations/lt.json deleted file mode 100644 index a390b260a03..00000000000 --- a/homeassistant/components/zwave/translations/lt.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "state": { - "query_stage": { - "dead": " ({query_stage})", - "initializing": " ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/lv.json b/homeassistant/components/zwave/translations/lv.json deleted file mode 100644 index d759c7a9213..00000000000 --- a/homeassistant/components/zwave/translations/lv.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "Beigta", - "initializing": "Inicializ\u0113", - "ready": "Gatavs", - "sleeping": "Gu\u013c" - }, - "query_stage": { - "dead": "Beigta ({query_stage})", - "initializing": "Inicializ\u0113 ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nb.json b/homeassistant/components/zwave/translations/nb.json deleted file mode 100644 index 9dcd1e82788..00000000000 --- a/homeassistant/components/zwave/translations/nb.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "Sover" - }, - "query_stage": { - "dead": "D\u00f8d ({query_stage})", - "initializing": "Initialiserer ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json deleted file mode 100644 index d8c58fe784c..00000000000 --- a/homeassistant/components/zwave/translations/nl.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." - }, - "error": { - "option_error": "Z-Wave-validatie mislukt. Is het pad naar de USB-stick correct?" - }, - "step": { - "user": { - "data": { - "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", - "usb_path": "USB-apparaatpad" - }, - "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen" - } - } - }, - "state": { - "_": { - "dead": "Onbereikbaar", - "initializing": "Initialiseren", - "ready": "Gereed", - "sleeping": "Slaapt" - }, - "query_stage": { - "dead": "Onbereikbaar", - "initializing": "Initialiseren" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/nn.json b/homeassistant/components/zwave/translations/nn.json deleted file mode 100644 index 76ff6120d81..00000000000 --- a/homeassistant/components/zwave/translations/nn.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." - } - } - }, - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "S\u00f8v" - }, - "query_stage": { - "dead": "D\u00f8d ({query_stage})", - "initializing": "Initialiserer ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json deleted file mode 100644 index 8582f906b57..00000000000 --- a/homeassistant/components/zwave/translations/no.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Enheten er allerede konfigurert", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "error": { - "option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?" - }, - "step": { - "user": { - "data": { - "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)", - "usb_path": "USB enhetsbane" - }, - "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene" - } - } - }, - "state": { - "_": { - "dead": "D\u00f8d", - "initializing": "Initialiserer", - "ready": "Klar", - "sleeping": "Sover" - }, - "query_stage": { - "dead": "D\u00f8d", - "initializing": "Initialiserer" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json deleted file mode 100644 index 9706fb1721f..00000000000 --- a/homeassistant/components/zwave/translations/pl.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "error": { - "option_error": "Walidacja Z-Wave si\u0119 nie powiod\u0142a. Czy \u015bcie\u017cka do kontrolera Z-Wave USB jest prawid\u0142owa?" - }, - "step": { - "user": { - "data": { - "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych" - } - } - }, - "state": { - "_": { - "dead": "martwy", - "initializing": "inicjalizacja", - "ready": "gotowy", - "sleeping": "u\u015bpiony" - }, - "query_stage": { - "dead": "martwy", - "initializing": "inicjalizacja" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pt-BR.json b/homeassistant/components/zwave/translations/pt-BR.json deleted file mode 100644 index 079f1ab8593..00000000000 --- a/homeassistant/components/zwave/translations/pt-BR.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o USB est\u00e1 correto?" - }, - "step": { - "user": { - "data": { - "network_key": "Chave de rede (deixe em branco para gerar automaticamente)", - "usb_path": "Caminho do Dispositivo USB" - }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" - } - } - }, - "state": { - "_": { - "dead": "Morto", - "initializing": "Iniciando", - "ready": "Pronto", - "sleeping": "Dormindo" - }, - "query_stage": { - "dead": "Morto", - "initializing": "Iniciando" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pt.json b/homeassistant/components/zwave/translations/pt.json deleted file mode 100644 index 27fc303f08b..00000000000 --- a/homeassistant/components/zwave/translations/pt.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o dispositivo USB est\u00e1 correto?" - }, - "step": { - "user": { - "data": { - "network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)", - "usb_path": "Endere\u00e7o USB" - }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" - } - } - }, - "state": { - "_": { - "dead": "Morto", - "initializing": "A inicializar", - "ready": "Pronto", - "sleeping": "Adormecido" - }, - "query_stage": { - "dead": "Morto ({query_stage})", - "initializing": "A inicializar ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ro.json b/homeassistant/components/zwave/translations/ro.json deleted file mode 100644 index de199bea03b..00000000000 --- a/homeassistant/components/zwave/translations/ro.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave este deja configurat" - }, - "error": { - "option_error": "Validarea Z-Wave a e\u0219uat. Este corect\u0103 calea c\u0103tre stick-ul USB?" - }, - "step": { - "user": { - "data": { - "network_key": "Cheie de re\u021bea (l\u0103sa\u021bi necompletat pentru a genera automat)", - "usb_path": "Cale USB" - }, - "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare" - } - } - }, - "state": { - "_": { - "dead": "Inactiv", - "initializing": "Se ini\u021bializeaz\u0103", - "ready": "Disponibil", - "sleeping": "Adormit" - }, - "query_stage": { - "dead": "Inactiv ({query_stage})", - "initializing": "Se ini\u021bializeaz\u0103 ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json deleted file mode 100644 index 7b7f0c6733d..00000000000 --- a/homeassistant/components/zwave/translations/ru.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "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.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." - }, - "error": { - "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." - }, - "step": { - "user": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", - "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." - } - } - }, - "state": { - "_": { - "dead": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", - "ready": "\u0413\u043e\u0442\u043e\u0432", - "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0430" - }, - "query_stage": { - "dead": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e", - "initializing": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sk.json b/homeassistant/components/zwave/translations/sk.json deleted file mode 100644 index 9819295ee1f..00000000000 --- a/homeassistant/components/zwave/translations/sk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" - } - }, - "state": { - "_": { - "dead": "Nereaguje", - "initializing": "Inicializ\u00e1cia", - "ready": "Pripraven\u00e9", - "sleeping": "\u00dasporn\u00fd re\u017eim" - }, - "query_stage": { - "dead": "Nereaguje ({query_stage})", - "initializing": "Inicializ\u00e1cia ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sl.json b/homeassistant/components/zwave/translations/sl.json deleted file mode 100644 index 38e70c59652..00000000000 --- a/homeassistant/components/zwave/translations/sl.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave je \u017ee konfiguriran" - }, - "error": { - "option_error": "Potrjevanje Z-Wave ni uspelo. Ali je pot do USB klju\u010da pravilna?" - }, - "step": { - "user": { - "data": { - "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", - "usb_path": "USB Pot" - }, - "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/" - } - } - }, - "state": { - "_": { - "dead": "Mrtva", - "initializing": "Inicializacija", - "ready": "Pripravljen", - "sleeping": "Spi" - }, - "query_stage": { - "dead": "Mrtva", - "initializing": "Inicializacija" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sr-Latn.json b/homeassistant/components/zwave/translations/sr-Latn.json deleted file mode 100644 index a390b260a03..00000000000 --- a/homeassistant/components/zwave/translations/sr-Latn.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "state": { - "query_stage": { - "dead": " ({query_stage})", - "initializing": " ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sr.json b/homeassistant/components/zwave/translations/sr.json deleted file mode 100644 index 00727fbb694..00000000000 --- a/homeassistant/components/zwave/translations/sr.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "state": { - "_": { - "ready": "Spreman" - }, - "query_stage": { - "dead": " ({query_stage})", - "initializing": " ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/sv.json b/homeassistant/components/zwave/translations/sv.json deleted file mode 100644 index 6d3af30a057..00000000000 --- a/homeassistant/components/zwave/translations/sv.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave \u00e4r redan konfigurerat" - }, - "error": { - "option_error": "Z-Wave-valideringen misslyckades. \u00c4r s\u00f6kv\u00e4gen till USB-minnet korrekt?" - }, - "step": { - "user": { - "data": { - "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", - "usb_path": "USB-s\u00f6kv\u00e4g" - }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler" - } - } - }, - "state": { - "_": { - "dead": "D\u00f6d", - "initializing": "Initierar", - "ready": "Redo", - "sleeping": "Sovande" - }, - "query_stage": { - "dead": "D\u00f6d ({query_stage})", - "initializing": "Initierar ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/ta.json b/homeassistant/components/zwave/translations/ta.json deleted file mode 100644 index 9b4fa65530c..00000000000 --- a/homeassistant/components/zwave/translations/ta.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0b87\u0bb1\u0ba8\u0bcd\u0ba4\u0bc1\u0bb5\u0bbf\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1", - "initializing": "\u0ba4\u0bc1\u0bb5\u0b95\u0bcd\u0b95\u0bc1\u0b95\u0bbf\u0bb1\u0ba4\u0bc1", - "ready": "\u0ba4\u0baf\u0bbe\u0bb0\u0bcd", - "sleeping": "\u0ba4\u0bc2\u0b99\u0bcd\u0b95\u0bc1\u0b95\u0bbf\u0ba9\u0bcd\u0bb1\u0ba4\u0bc1" - }, - "query_stage": { - "dead": "\u0b87\u0bb1\u0ba8\u0bcd\u0ba4\u0bc1\u0bb5\u0bbf\u0b9f\u0bcd\u0b9f\u0ba4\u0bc1 ({query_stage})", - "initializing": "\u0ba4\u0bc1\u0bb5\u0b95\u0bcd\u0b95\u0bc1\u0b95\u0bbf\u0bb1\u0ba4\u0bc1 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/te.json b/homeassistant/components/zwave/translations/te.json deleted file mode 100644 index 88e4eac6961..00000000000 --- a/homeassistant/components/zwave/translations/te.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0c2e\u0c43\u0c24 \u0c2a\u0c30\u0c3f\u0c15\u0c30\u0c02", - "initializing": "\u0c38\u0c3f\u0c26\u0c4d\u0c27\u0c02 \u0c05\u0c35\u0c41\u0c24\u0c4b\u0c02\u0c26\u0c3f", - "ready": "\u0c30\u0c46\u0c21\u0c40", - "sleeping": "\u0c28\u0c3f\u0c26\u0c4d\u0c30\u0c3f\u0c38\u0c4d\u0c24\u0c4b\u0c02\u0c26\u0c3f" - }, - "query_stage": { - "dead": "\u0c2e\u0c43\u0c24 \u0c2a\u0c30\u0c3f\u0c15\u0c30\u0c02 ({query_stage})", - "initializing": "\u0c38\u0c3f\u0c26\u0c4d\u0c27\u0c02 \u0c05\u0c35\u0c41\u0c24\u0c4b\u0c02\u0c26\u0c3f ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/th.json b/homeassistant/components/zwave/translations/th.json deleted file mode 100644 index 51db4f5b2e1..00000000000 --- a/homeassistant/components/zwave/translations/th.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0e44\u0e21\u0e48\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19", - "initializing": "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19", - "ready": "\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19", - "sleeping": "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e2b\u0e25\u0e31\u0e1a" - }, - "query_stage": { - "dead": "\u0e44\u0e21\u0e48\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19 ({query_stage})", - "initializing": "\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19 ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/tr.json b/homeassistant/components/zwave/translations/tr.json deleted file mode 100644 index b6afa368b6d..00000000000 --- a/homeassistant/components/zwave/translations/tr.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "error": { - "option_error": "Z-Wave do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu. USB stickin yolu do\u011fru mu?" - }, - "step": { - "user": { - "data": { - "network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)", - "usb_path": "USB Cihaz Yolu" - }, - "description": "Bu entegrasyon art\u0131k korunmuyor. Yeni kurulumlar i\u00e7in bunun yerine Z-Wave JS kullan\u0131n. \n\n Yap\u0131land\u0131rma de\u011fi\u015fkenleri hakk\u0131nda bilgi i\u00e7in https://www.home-assistant.io/docs/z-wave/installation/ adresine bak\u0131n." - } - } - }, - "state": { - "_": { - "dead": "\u00d6l\u00fc", - "initializing": "Ba\u015flat\u0131l\u0131yor", - "ready": "Haz\u0131r", - "sleeping": "Uyuyor" - }, - "query_stage": { - "dead": "\u00d6l\u00fc", - "initializing": "Ba\u015flat\u0131l\u0131yor" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json deleted file mode 100644 index 3c5b49681c8..00000000000 --- a/homeassistant/components/zwave/translations/uk.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "error": { - "option_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 Z-Wave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0448\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." - }, - "step": { - "user": { - "data": { - "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", - "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" - }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." - } - } - }, - "state": { - "_": { - "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", - "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f", - "ready": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439", - "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0443" - }, - "query_stage": { - "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", - "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/vi.json b/homeassistant/components/zwave/translations/vi.json deleted file mode 100644 index 4055e09a8df..00000000000 --- a/homeassistant/components/zwave/translations/vi.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "state": { - "_": { - "dead": "\u0110\u00e3 t\u1eaft", - "initializing": "Kh\u1edfi t\u1ea1o", - "ready": "S\u1eb5n s\u00e0ng", - "sleeping": "Ng\u1ee7" - }, - "query_stage": { - "dead": "\u0110\u00e3 t\u1eaft ({query_stage})", - "initializing": "Kh\u1edfi t\u1ea1o ( {query_stage} )" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/zh-Hans.json b/homeassistant/components/zwave/translations/zh-Hans.json deleted file mode 100644 index 64af99e21ff..00000000000 --- a/homeassistant/components/zwave/translations/zh-Hans.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Z-Wave \u5df2\u914d\u7f6e\u5b8c\u6210" - }, - "error": { - "option_error": "Z-Wave \u9a8c\u8bc1\u5931\u8d25\u3002 USB \u68d2\u7684\u8def\u5f84\u662f\u5426\u6b63\u786e\uff1f" - }, - "step": { - "user": { - "data": { - "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", - "usb_path": "USB \u8def\u5f84" - }, - "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/" - } - } - }, - "state": { - "_": { - "dead": "\u65ad\u5f00", - "initializing": "\u521d\u59cb\u5316", - "ready": "\u5c31\u7eea", - "sleeping": "\u4f11\u7720" - }, - "query_stage": { - "dead": "\u65ad\u5f00 ({query_stage})", - "initializing": "\u521d\u59cb\u5316 ({query_stage})" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json deleted file mode 100644 index 9dc8810f499..00000000000 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "error": { - "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" - }, - "step": { - "user": { - "data": { - "network_key": "\u7db2\u8def\u91d1\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" - }, - "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a" - } - } - }, - "state": { - "_": { - "dead": "\u5931\u53bb\u9023\u7dda", - "initializing": "\u6b63\u5728\u521d\u59cb\u5316", - "ready": "\u6e96\u5099\u5c31\u7dd2", - "sleeping": "\u4f11\u7720\u4e2d" - }, - "query_stage": { - "dead": "\u5931\u53bb\u9023\u7dda", - "initializing": "\u6b63\u5728\u521d\u59cb\u5316" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py deleted file mode 100644 index 19be3f7a659..00000000000 --- a/homeassistant/components/zwave/util.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Zwave util methods.""" -import asyncio -import logging - -import homeassistant.util.dt as dt_util - -from . import const - -_LOGGER = logging.getLogger(__name__) - - -def check_node_schema(node, schema): - """Check if node matches the passed node schema.""" - if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]: - _LOGGER.debug( - "node.node_id %s not in node_id %s", - node.node_id, - schema[const.DISC_NODE_ID], - ) - return False - if ( - const.DISC_GENERIC_DEVICE_CLASS in schema - and node.generic not in schema[const.DISC_GENERIC_DEVICE_CLASS] - ): - _LOGGER.debug( - "node.generic %s not in generic_device_class %s", - node.generic, - schema[const.DISC_GENERIC_DEVICE_CLASS], - ) - return False - if ( - const.DISC_SPECIFIC_DEVICE_CLASS in schema - and node.specific not in schema[const.DISC_SPECIFIC_DEVICE_CLASS] - ): - _LOGGER.debug( - "node.specific %s not in specific_device_class %s", - node.specific, - schema[const.DISC_SPECIFIC_DEVICE_CLASS], - ) - return False - return True - - -def check_value_schema(value, schema): - """Check if the value matches the passed value schema.""" - if ( - const.DISC_COMMAND_CLASS in schema - and value.command_class not in schema[const.DISC_COMMAND_CLASS] - ): - _LOGGER.debug( - "value.command_class %s not in command_class %s", - value.command_class, - schema[const.DISC_COMMAND_CLASS], - ) - return False - if const.DISC_TYPE in schema and value.type not in schema[const.DISC_TYPE]: - _LOGGER.debug( - "value.type %s not in type %s", value.type, schema[const.DISC_TYPE] - ) - return False - if const.DISC_GENRE in schema and value.genre not in schema[const.DISC_GENRE]: - _LOGGER.debug( - "value.genre %s not in genre %s", value.genre, schema[const.DISC_GENRE] - ) - return False - if const.DISC_INDEX in schema and value.index not in schema[const.DISC_INDEX]: - _LOGGER.debug( - "value.index %s not in index %s", value.index, schema[const.DISC_INDEX] - ) - return False - if ( - const.DISC_INSTANCE in schema - and value.instance not in schema[const.DISC_INSTANCE] - ): - _LOGGER.debug( - "value.instance %s not in instance %s", - value.instance, - schema[const.DISC_INSTANCE], - ) - return False - if const.DISC_SCHEMAS in schema: - found = False - for schema_item in schema[const.DISC_SCHEMAS]: - found = found or check_value_schema(value, schema_item) - if not found: - return False - - return True - - -def compute_value_unique_id(node, value): - """Compute unique_id a value would get if it were to get one.""" - return f"{node.node_id}-{value.object_id}" - - -def node_name(node): - """Return the name of the node.""" - if is_node_parsed(node): - return node.name or f"{node.manufacturer_name} {node.product_name}" - return f"Unknown Node {node.node_id}" - - -def node_device_id_and_name(node, instance=1): - """Return the name and device ID for the value with the given index.""" - name = node_name(node) - if instance == 1: - return ((const.DOMAIN, node.node_id), name) - name = f"{name} ({instance})" - return ((const.DOMAIN, node.node_id, instance), name) - - -async def check_has_unique_id(entity, ready_callback, timeout_callback): - """Wait for entity to have unique_id.""" - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow() - start_time).total_seconds()) - if entity.unique_id: - ready_callback(waited) - return - if waited >= const.NODE_READY_WAIT_SECS: - # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. - timeout_callback(waited) - return - await asyncio.sleep(1) - - -def is_node_parsed(node): - """Check whether the node has been parsed or still waiting to be parsed.""" - return bool((node.manufacturer_name and node.product_name) or node.name) diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py deleted file mode 100644 index b86e46bee98..00000000000 --- a/homeassistant/components/zwave/websocket_api.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Web socket API for Z-Wave.""" -import voluptuous as vol - -from homeassistant.components import websocket_api -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.core import callback - -from .const import ( - CONF_AUTOHEAL, - CONF_DEBUG, - CONF_NETWORK_KEY, - CONF_POLLING_INTERVAL, - CONF_USB_STICK_PATH, - DATA_NETWORK, - DATA_ZWAVE_CONFIG, -) - -TYPE = "type" -ID = "id" - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required(TYPE): "zwave/network_status"}) -def websocket_network_status(hass, connection, msg): - """Get Z-Wave network status.""" - network = hass.data[DATA_NETWORK] - connection.send_result(msg[ID], {"state": network.state}) - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_config"}) -def websocket_get_config(hass, connection, msg): - """Get Z-Wave configuration.""" - config = hass.data[DATA_ZWAVE_CONFIG] - connection.send_result( - msg[ID], - { - CONF_AUTOHEAL: config[CONF_AUTOHEAL], - CONF_DEBUG: config[CONF_DEBUG], - CONF_POLLING_INTERVAL: config[CONF_POLLING_INTERVAL], - CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_migration_config"}) -def websocket_get_migration_config(hass, connection, msg): - """Get Z-Wave configuration for migration.""" - config = hass.data[DATA_ZWAVE_CONFIG] - connection.send_result( - msg[ID], - { - CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], - CONF_NETWORK_KEY: config[CONF_NETWORK_KEY], - }, - ) - - -@websocket_api.require_admin -@websocket_api.websocket_command( - {vol.Required(TYPE): "zwave/start_zwave_js_config_flow"} -) -@websocket_api.async_response -async def websocket_start_zwave_js_config_flow(hass, connection, msg): - """Start the Z-Wave JS integration config flow (for migration wizard). - - Return data with the flow id of the started Z-Wave JS config flow. - """ - config = hass.data[DATA_ZWAVE_CONFIG] - data = { - "usb_path": config[CONF_USB_STICK_PATH], - "network_key": config[CONF_NETWORK_KEY], - } - result = await hass.config_entries.flow.async_init( - "zwave_js", context={"source": SOURCE_IMPORT}, data=data - ) - connection.send_result( - msg[ID], - {"flow_id": result["flow_id"]}, - ) - - -@callback -def async_load_websocket_api(hass): - """Set up the web socket API.""" - websocket_api.async_register_command(hass, websocket_network_status) - websocket_api.async_register_command(hass, websocket_get_config) - websocket_api.async_register_command(hass, websocket_get_migration_config) - websocket_api.async_register_command(hass, websocket_start_zwave_js_config_flow) diff --git a/homeassistant/components/zwave/workaround.py b/homeassistant/components/zwave/workaround.py deleted file mode 100644 index 9ef7cde446d..00000000000 --- a/homeassistant/components/zwave/workaround.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Z-Wave workarounds.""" -from . import const - -# Manufacturers -FIBARO = 0x010F -GE = 0x0063 -PHILIO = 0x013C -SOMFY = 0x0047 -WENZHOU = 0x0118 -LEVITON = 0x001D - -# Product IDs -GE_FAN_CONTROLLER_12730 = 0x3034 -GE_FAN_CONTROLLER_14287 = 0x3131 -JASCO_FAN_CONTROLLER_14314 = 0x3138 -PHILIO_SLIM_SENSOR = 0x0002 -PHILIO_3_IN_1_SENSOR_GEN_4 = 0x000D -PHILIO_PAN07 = 0x0005 -VIZIA_FAN_CONTROLLER_VRF01 = 0x0334 -LEVITON_DECORA_FAN_CONTROLLER_ZW4SF = 0x0002 - -# Product Types -FGFS101_FLOOD_SENSOR_TYPE = 0x0B00 -FGRM222_SHUTTER2 = 0x0301 -FGR222_SHUTTER2 = 0x0302 -GE_DIMMER = 0x4944 -PHILIO_SWITCH = 0x0001 -PHILIO_SENSOR = 0x0002 -SOMFY_ZRTSI = 0x5A52 -VIZIA_DIMMER = 0x1001 -LEVITON_DECORA_FAN_CONTROLLER = 0x0038 - -# Mapping devices -PHILIO_SLIM_SENSOR_MOTION_MTII = (PHILIO, PHILIO_SENSOR, PHILIO_SLIM_SENSOR, 0) -PHILIO_3_IN_1_SENSOR_GEN_4_MOTION_MTII = ( - PHILIO, - PHILIO_SENSOR, - PHILIO_3_IN_1_SENSOR_GEN_4, - 0, -) -PHILIO_PAN07_MTI_INSTANCE = (PHILIO, PHILIO_SWITCH, PHILIO_PAN07, 1) -WENZHOU_SLIM_SENSOR_MOTION_MTII = (WENZHOU, PHILIO_SENSOR, PHILIO_SLIM_SENSOR, 0) - -# Workarounds -WORKAROUND_NO_OFF_EVENT = "trigger_no_off_event" -WORKAROUND_NO_POSITION = "workaround_no_position" -WORKAROUND_REFRESH_NODE_ON_UPDATE = "refresh_node_on_update" -WORKAROUND_IGNORE = "workaround_ignore" - -# List of workarounds by (manufacturer_id, product_type, product_id, index) -DEVICE_MAPPINGS_MTII = { - PHILIO_SLIM_SENSOR_MOTION_MTII: WORKAROUND_NO_OFF_EVENT, - PHILIO_3_IN_1_SENSOR_GEN_4_MOTION_MTII: WORKAROUND_NO_OFF_EVENT, - WENZHOU_SLIM_SENSOR_MOTION_MTII: WORKAROUND_NO_OFF_EVENT, -} - -# List of workarounds by (manufacturer_id, product_type, product_id, instance) -DEVICE_MAPPINGS_MTI_INSTANCE = { - PHILIO_PAN07_MTI_INSTANCE: WORKAROUND_REFRESH_NODE_ON_UPDATE -} - -SOMFY_ZRTSI_CONTROLLER_MT = (SOMFY, SOMFY_ZRTSI) - -# List of workarounds by (manufacturer_id, product_type) -DEVICE_MAPPINGS_MT = {SOMFY_ZRTSI_CONTROLLER_MT: WORKAROUND_NO_POSITION} - -# Component mapping devices -FIBARO_FGFS101_SENSOR_ALARM = ( - FIBARO, - FGFS101_FLOOD_SENSOR_TYPE, - const.COMMAND_CLASS_SENSOR_ALARM, -) -FIBARO_FGRM222_BINARY = (FIBARO, FGRM222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY) -FIBARO_FGR222_BINARY = (FIBARO, FGR222_SHUTTER2, const.COMMAND_CLASS_SWITCH_BINARY) -GE_FAN_CONTROLLER_12730_MULTILEVEL = ( - GE, - GE_DIMMER, - GE_FAN_CONTROLLER_12730, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -GE_FAN_CONTROLLER_14287_MULTILEVEL = ( - GE, - GE_DIMMER, - GE_FAN_CONTROLLER_14287, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -JASCO_FAN_CONTROLLER_14314_MULTILEVEL = ( - GE, - GE_DIMMER, - JASCO_FAN_CONTROLLER_14314, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -VIZIA_FAN_CONTROLLER_VRF01_MULTILEVEL = ( - LEVITON, - VIZIA_DIMMER, - VIZIA_FAN_CONTROLLER_VRF01, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) -LEVITON_FAN_CONTROLLER_ZW4SF_MULTILEVEL = ( - LEVITON, - LEVITON_DECORA_FAN_CONTROLLER, - LEVITON_DECORA_FAN_CONTROLLER_ZW4SF, - const.COMMAND_CLASS_SWITCH_MULTILEVEL, -) - -# List of component workarounds by -# (manufacturer_id, product_type, command_class) -DEVICE_COMPONENT_MAPPING = { - FIBARO_FGFS101_SENSOR_ALARM: "binary_sensor", - FIBARO_FGRM222_BINARY: WORKAROUND_IGNORE, - FIBARO_FGR222_BINARY: WORKAROUND_IGNORE, -} - -# List of component workarounds by -# (manufacturer_id, product_type, product_id, command_class) -DEVICE_COMPONENT_MAPPING_MTI = { - GE_FAN_CONTROLLER_12730_MULTILEVEL: "fan", - GE_FAN_CONTROLLER_14287_MULTILEVEL: "fan", - JASCO_FAN_CONTROLLER_14314_MULTILEVEL: "fan", - VIZIA_FAN_CONTROLLER_VRF01_MULTILEVEL: "fan", - LEVITON_FAN_CONTROLLER_ZW4SF_MULTILEVEL: "fan", -} - - -def get_device_component_mapping(value): - """Get mapping of value to another component.""" - if value.node.manufacturer_id.strip() and value.node.product_type.strip(): - manufacturer_id = int(value.node.manufacturer_id, 16) - product_type = int(value.node.product_type, 16) - product_id = int(value.node.product_id, 16) - result = DEVICE_COMPONENT_MAPPING.get( - (manufacturer_id, product_type, value.command_class) - ) - if result: - return result - - result = DEVICE_COMPONENT_MAPPING_MTI.get( - (manufacturer_id, product_type, product_id, value.command_class) - ) - if result: - return result - - return None - - -def get_device_mapping(value): - """Get mapping of value to a workaround.""" - if ( - value.node.manufacturer_id.strip() - and value.node.product_id.strip() - and value.node.product_type.strip() - ): - manufacturer_id = int(value.node.manufacturer_id, 16) - product_type = int(value.node.product_type, 16) - product_id = int(value.node.product_id, 16) - result = DEVICE_MAPPINGS_MTII.get( - (manufacturer_id, product_type, product_id, value.index) - ) - if result: - return result - - result = DEVICE_MAPPINGS_MTI_INSTANCE.get( - (manufacturer_id, product_type, product_id, value.instance) - ) - if result: - return result - - return DEVICE_MAPPINGS_MT.get((manufacturer_id, product_type)) - - return None diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 89cd4495326..a96acda5824 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -394,7 +394,6 @@ FLOWS = [ "youless", "zerproc", "zha", - "zwave", "zwave_js", "zwave_me" ] diff --git a/mypy.ini b/mypy.ini index 16bf8394eee..0cde5eda9a3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2917,12 +2917,3 @@ ignore_errors = true [mypy-homeassistant.components.zha.switch] ignore_errors = true - -[mypy-homeassistant.components.zwave] -ignore_errors = true - -[mypy-homeassistant.components.zwave.migration] -ignore_errors = true - -[mypy-homeassistant.components.zwave.node_entity] -ignore_errors = true diff --git a/requirements_all.txt b/requirements_all.txt index ba4ec4536e5..52ad3feb66a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -811,9 +811,6 @@ holidays==0.13 # homeassistant.components.frontend home-assistant-frontend==20220317.0 -# homeassistant.components.zwave -# homeassistant-pyozw==0.1.10 - # homeassistant.components.home_connect homeconnect==0.7.0 @@ -1424,9 +1421,6 @@ pydelijn==1.0.0 # homeassistant.components.dexcom pydexcom==0.2.3 -# homeassistant.components.zwave -pydispatcher==2.0.5 - # homeassistant.components.doods pydoods==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da1d72eb5e2..2c68b9fcbc4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,9 +561,6 @@ holidays==0.13 # homeassistant.components.frontend home-assistant-frontend==20220317.0 -# homeassistant.components.zwave -# homeassistant-pyozw==0.1.10 - # homeassistant.components.home_connect homeconnect==0.7.0 @@ -931,9 +928,6 @@ pydeconz==87 # homeassistant.components.dexcom pydexcom==0.2.3 -# homeassistant.components.zwave -pydispatcher==2.0.5 - # homeassistant.components.econet pyeconet==0.1.15 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9d5127a626b..896c87a2d80 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -23,7 +23,6 @@ COMMENT_REQUIREMENTS = ( "decora_wifi", "evdev", "face_recognition", - "homeassistant-pyozw", "opencv-python-headless", "pybluez", "pycups", diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index fcc03a35524..279e78fb44f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -214,9 +214,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.sensor", "homeassistant.components.zha.siren", "homeassistant.components.zha.switch", - "homeassistant.components.zwave", - "homeassistant.components.zwave.migration", - "homeassistant.components.zwave.node_entity", ] # Component modules which should set no_implicit_reexport = true. diff --git a/script/translations/migrate.py b/script/translations/migrate.py index c4c47600698..d3efdc28d13 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -237,7 +237,7 @@ STATE_REWRITE = { "[%key:state::lock::unlocked%]": "[%key:common::state::unlocked%]", } SKIP_DOMAIN = {"default", "scene"} -STATES_WITH_DEV_CLASS = {"binary_sensor", "zwave"} +STATES_WITH_DEV_CLASS = {"binary_sensor"} GROUP_DELETE = {"opening", "closing", "stopped"} # They don't exist diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index dd3e294bac3..2d5610cadfb 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,49 +1,8 @@ """Test config init.""" - -from unittest.mock import patch - -from homeassistant.components import config -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.setup import ATTR_COMPONENT, async_setup_component - -from tests.common import mock_component +from homeassistant.setup import async_setup_component async def test_config_setup(hass, loop): """Test it sets up hassbian.""" await async_setup_component(hass, "config", {}) assert "config" in hass.config.components - - -async def test_load_on_demand_already_loaded(hass, aiohttp_client): - """Test getting suites.""" - mock_component(hass, "zwave") - - with patch.object(config, "SECTIONS", []), patch.object( - config, "ON_DEMAND", ["zwave"] - ), patch( - "homeassistant.components.config.zwave.async_setup", return_value=True - ) as stp: - - await async_setup_component(hass, "config", {}) - - await hass.async_block_till_done() - assert stp.called - - -async def test_load_on_demand_on_load(hass, aiohttp_client): - """Test getting suites.""" - with patch.object(config, "SECTIONS", []), patch.object( - config, "ON_DEMAND", ["zwave"] - ): - await async_setup_component(hass, "config", {}) - - assert "config.zwave" not in hass.config.components - - with patch( - "homeassistant.components.config.zwave.async_setup", return_value=True - ) as stp: - hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "zwave"}) - await hass.async_block_till_done() - - assert stp.called diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py deleted file mode 100644 index bc7f22c104f..00000000000 --- a/tests/components/config/test_zwave.py +++ /dev/null @@ -1,542 +0,0 @@ -"""Test Z-Wave config panel.""" -from http import HTTPStatus -import json -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components import config -from homeassistant.components.zwave import DATA_NETWORK, const - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue - -VIEW_NAME = "api:config:zwave:device_config" - - -@pytest.fixture -def client(loop, hass, hass_client): - """Client to communicate with Z-Wave config views.""" - with patch.object(config, "SECTIONS", ["zwave"]): - loop.run_until_complete(async_setup_component(hass, "config", {})) - - return loop.run_until_complete(hass_client()) - - -async def test_get_device_config(client): - """Test getting device config.""" - - def mock_read(path): - """Mock reading data.""" - return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} - - with patch("homeassistant.components.config._read", mock_read): - resp = await client.get("/api/config/zwave/device_config/hello.beer") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {"free": "beer"} - - -async def test_update_device_config(client): - """Test updating device config.""" - orig_data = { - "hello.beer": {"ignored": True}, - "other.entity": {"polling_intensity": 2}, - } - - def mock_read(path): - """Mock reading data.""" - return orig_data - - written = [] - - def mock_write(path, data): - """Mock writing data.""" - written.append(data) - - with patch("homeassistant.components.config._read", mock_read), patch( - "homeassistant.components.config._write", mock_write - ): - resp = await client.post( - "/api/config/zwave/device_config/hello.beer", - data=json.dumps({"polling_intensity": 2}), - ) - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert result == {"result": "ok"} - - orig_data["hello.beer"]["polling_intensity"] = 2 - - assert written[0] == orig_data - - -async def test_update_device_config_invalid_key(client): - """Test updating device config.""" - resp = await client.post( - "/api/config/zwave/device_config/invalid_entity", - data=json.dumps({"polling_intensity": 2}), - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_update_device_config_invalid_data(client): - """Test updating device config.""" - resp = await client.post( - "/api/config/zwave/device_config/hello.beer", - data=json.dumps({"invalid_option": 2}), - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_update_device_config_invalid_json(client): - """Test updating device config.""" - resp = await client.post( - "/api/config/zwave/device_config/hello.beer", data="not json" - ) - - assert resp.status == HTTPStatus.BAD_REQUEST - - -async def test_get_values(hass, client): - """Test getting values on node.""" - node = MockNode(node_id=1) - value = MockValue( - value_id=123456, - node=node, - label="Test Label", - instance=1, - index=2, - poll_intensity=4, - ) - values = MockEntityValues(primary=value) - node2 = MockNode(node_id=2) - value2 = MockValue(value_id=234567, node=node2, label="Test Label 2") - values2 = MockEntityValues(primary=value2) - hass.data[const.DATA_ENTITY_VALUES] = [values, values2] - - resp = await client.get("/api/zwave/values/1") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == { - "123456": { - "label": "Test Label", - "instance": 1, - "index": 2, - "poll_intensity": 4, - } - } - - -async def test_get_groups(hass, client): - """Test getting groupdata on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - node.groups.associations = "assoc" - node.groups.associations_instances = "inst" - node.groups.label = "the label" - node.groups.max_associations = "max" - node.groups = {1: node.groups} - network.nodes = {2: node} - - resp = await client.get("/api/zwave/groups/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == { - "1": { - "association_instances": "inst", - "associations": "assoc", - "label": "the label", - "max_associations": "max", - } - } - - -async def test_get_groups_nogroups(hass, client): - """Test getting groupdata on node with no groups.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - - network.nodes = {2: node} - - resp = await client.get("/api/zwave/groups/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_get_groups_nonode(hass, client): - """Test getting groupdata on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - network.nodes = {1: 1, 5: 5} - - resp = await client.get("/api/zwave/groups/2") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - - assert result == {"message": "Node not found"} - - -async def test_get_config(hass, client): - """Test getting config on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - value = MockValue(index=12, command_class=const.COMMAND_CLASS_CONFIGURATION) - value.label = "label" - value.help = "help" - value.type = "type" - value.data = "data" - value.data_items = ["item1", "item2"] - value.max = "max" - value.min = "min" - node.values = {12: value} - network.nodes = {2: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/config/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == { - "12": { - "data": "data", - "data_items": ["item1", "item2"], - "help": "help", - "label": "label", - "max": "max", - "min": "min", - "type": "type", - } - } - - -async def test_get_config_noconfig_node(hass, client): - """Test getting config on node without config.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=2) - - network.nodes = {2: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/config/2") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_get_config_nonode(hass, client): - """Test getting config on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - network.nodes = {1: 1, 5: 5} - - resp = await client.get("/api/zwave/config/2") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - - assert result == {"message": "Node not found"} - - -async def test_get_usercodes_nonode(hass, client): - """Test getting usercodes on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - network.nodes = {1: 1, 5: 5} - - resp = await client.get("/api/zwave/usercodes/2") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - - assert result == {"message": "Node not found"} - - -async def test_get_usercodes(hass, client): - """Test getting usercodes on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) - value = MockValue(index=0, command_class=const.COMMAND_CLASS_USER_CODE) - value.genre = const.GENRE_USER - value.label = "label" - value.data = "1234" - node.values = {0: value} - network.nodes = {18: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/usercodes/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {"0": {"code": "1234", "label": "label", "length": 4}} - - -async def test_get_usercode_nousercode_node(hass, client): - """Test getting usercodes on node without usercodes.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18) - - network.nodes = {18: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/usercodes/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_get_usercodes_no_genreuser(hass, client): - """Test getting usercodes on node missing genre user.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) - value = MockValue(index=0, command_class=const.COMMAND_CLASS_USER_CODE) - value.genre = const.GENRE_SYSTEM - value.label = "label" - value.data = "1234" - node.values = {0: value} - network.nodes = {18: node} - node.get_values.return_value = node.values - - resp = await client.get("/api/zwave/usercodes/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - - assert result == {} - - -async def test_save_config_no_network(hass, client): - """Test saving configuration without network data.""" - resp = await client.post("/api/zwave/saveconfig") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert result == {"message": "No Z-Wave network data found"} - - -async def test_save_config(hass, client): - """Test saving configuration.""" - network = hass.data[DATA_NETWORK] = MagicMock() - - resp = await client.post("/api/zwave/saveconfig") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert network.write_config.called - assert result == {"message": "Z-Wave configuration saved to file"} - - -async def test_get_protection_values(hass, client): - """Test getting protection values on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {18: node} - node.value = value - - node.get_protection_item.return_value = "Unprotected" - node.get_protection_items.return_value = value.data_items - node.get_protections.return_value = {value.value_id: "Object"} - - resp = await client.get("/api/zwave/protection/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert node.get_protections.called - assert node.get_protection_item.called - assert node.get_protection_items.called - assert result == { - "value_id": "123456", - "selected": "Unprotected", - "options": ["Unprotected", "Protection by Sequence", "No Operation Possible"], - } - - -async def test_get_protection_values_nonexisting_node(hass, client): - """Test getting protection values on node with wrong nodeid.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {17: node} - node.value = value - - resp = await client.get("/api/zwave/protection/18") - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert not node.get_protections.called - assert not node.get_protection_item.called - assert not node.get_protection_items.called - assert result == {"message": "Node not found"} - - -async def test_get_protection_values_without_protectionclass(hass, client): - """Test getting protection values on node without protectionclass.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18) - value = MockValue(value_id=123456, index=0, instance=1) - network.nodes = {18: node} - node.value = value - - resp = await client.get("/api/zwave/protection/18") - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert not node.get_protections.called - assert not node.get_protection_item.called - assert not node.get_protection_items.called - assert result == {} - - -async def test_set_protection_value(hass, client): - """Test setting protection value on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {18: node} - node.value = value - - resp = await client.post( - "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protection by Sequence"}), - ) - - assert resp.status == HTTPStatus.OK - result = await resp.json() - assert node.set_protection.called - assert result == {"message": "Protection setting successfully set"} - - -async def test_set_protection_value_failed(hass, client): - """Test setting protection value failed on node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {18: node} - node.value = value - node.set_protection.return_value = False - - resp = await client.post( - "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), - ) - - assert resp.status == HTTPStatus.ACCEPTED - result = await resp.json() - assert node.set_protection.called - assert result == {"message": "Protection setting did not complete"} - - -async def test_set_protection_value_nonexisting_node(hass, client): - """Test setting protection value on nonexisting node.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=17, command_classes=[const.COMMAND_CLASS_PROTECTION]) - value = MockValue( - value_id=123456, - index=0, - instance=1, - command_class=const.COMMAND_CLASS_PROTECTION, - ) - value.label = "Protection Test" - value.data_items = [ - "Unprotected", - "Protection by Sequence", - "No Operation Possible", - ] - value.data = "Unprotected" - network.nodes = {17: node} - node.value = value - node.set_protection.return_value = False - - resp = await client.post( - "/api/zwave/protection/18", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), - ) - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert not node.set_protection.called - assert result == {"message": "Node not found"} - - -async def test_set_protection_value_missing_class(hass, client): - """Test setting protection value on node without protectionclass.""" - network = hass.data[DATA_NETWORK] = MagicMock() - node = MockNode(node_id=17) - value = MockValue(value_id=123456, index=0, instance=1) - network.nodes = {17: node} - node.value = value - node.set_protection.return_value = False - - resp = await client.post( - "/api/zwave/protection/17", - data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), - ) - - assert resp.status == HTTPStatus.NOT_FOUND - result = await resp.json() - assert not node.set_protection.called - assert result == {"message": "No protection commandclass on this node"} diff --git a/tests/components/zwave/__init__.py b/tests/components/zwave/__init__.py deleted file mode 100644 index 996bbf22b69..00000000000 --- a/tests/components/zwave/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Z-Wave component.""" diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py deleted file mode 100644 index 027d3a82ea2..00000000000 --- a/tests/components/zwave/conftest.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Fixtures for Z-Wave tests.""" -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -from homeassistant.components.zwave import const - -from tests.components.light.conftest import mock_light_profiles # noqa: F401 -from tests.mock.zwave import MockNetwork, MockNode, MockOption, MockValue - - -@pytest.fixture -def mock_openzwave(): - """Mock out Open Z-Wave.""" - base_mock = MagicMock() - libopenzwave = base_mock.libopenzwave - libopenzwave.__file__ = "test" - base_mock.network.ZWaveNetwork = MockNetwork - base_mock.option.ZWaveOption = MockOption - - with patch.dict( - "sys.modules", - { - "libopenzwave": libopenzwave, - "openzwave.option": base_mock.option, - "openzwave.network": base_mock.network, - "openzwave.group": base_mock.group, - }, - ): - yield base_mock - - -@pytest.fixture -def mock_discovery(): - """Mock discovery.""" - discovery = MagicMock() - discovery.async_load_platform = AsyncMock(return_value=None) - yield discovery - - -@pytest.fixture -def mock_import_module(): - """Mock import module.""" - platform = MagicMock() - mock_device = MagicMock() - mock_device.name = "test_device" - platform.get_device.return_value = mock_device - - import_module = MagicMock() - import_module.return_value = platform - yield import_module - - -@pytest.fixture -def mock_values(): - """Mock values.""" - node = MockNode() - mock_schema = { - const.DISC_COMPONENT: "mock_component", - const.DISC_VALUES: { - const.DISC_PRIMARY: {const.DISC_COMMAND_CLASS: ["mock_primary_class"]}, - "secondary": {const.DISC_COMMAND_CLASS: ["mock_secondary_class"]}, - "optional": { - const.DISC_COMMAND_CLASS: ["mock_optional_class"], - const.DISC_OPTIONAL: True, - }, - }, - } - value_class = MagicMock() - value_class.primary = MockValue( - command_class="mock_primary_class", node=node, value_id=1000 - ) - value_class.secondary = MockValue(command_class="mock_secondary_class", node=node) - value_class.duplicate_secondary = MockValue( - command_class="mock_secondary_class", node=node - ) - value_class.optional = MockValue(command_class="mock_optional_class", node=node) - value_class.no_match_value = MockValue(command_class="mock_bad_class", node=node) - - yield (node, value_class, mock_schema) diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py deleted file mode 100644 index 265ec6f2d1e..00000000000 --- a/tests/components/zwave/test_binary_sensor.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Test Z-Wave binary sensors.""" -import datetime -from unittest.mock import patch - -import pytest - -from homeassistant.components.zwave import binary_sensor, const - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_detects_none(mock_openzwave): - """Test device is not returned.""" - node = MockNode() - value = MockValue(data=False, node=node) - values = MockEntityValues(primary=value) - - device = binary_sensor.get_device(node=node, values=values, node_config={}) - assert device is None - - -def test_get_device_detects_trigger_sensor(mock_openzwave): - """Test device is a trigger sensor.""" - node = MockNode(manufacturer_id="013c", product_type="0002", product_id="0002") - value = MockValue(data=False, node=node) - values = MockEntityValues(primary=value) - - device = binary_sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, binary_sensor.ZWaveTriggerSensor) - assert device.device_class == "motion" - - -def test_get_device_detects_workaround_sensor(mock_openzwave): - """Test that workaround returns a binary sensor.""" - node = MockNode(manufacturer_id="010f", product_type="0b00") - value = MockValue( - data=False, node=node, command_class=const.COMMAND_CLASS_SENSOR_ALARM - ) - values = MockEntityValues(primary=value) - - device = binary_sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, binary_sensor.ZWaveBinarySensor) - - -def test_get_device_detects_sensor(mock_openzwave): - """Test that device returns a binary sensor.""" - node = MockNode() - value = MockValue( - data=False, node=node, command_class=const.COMMAND_CLASS_SENSOR_BINARY - ) - values = MockEntityValues(primary=value) - - device = binary_sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, binary_sensor.ZWaveBinarySensor) - - -def test_binary_sensor_value_changed(mock_openzwave): - """Test value changed for binary sensor.""" - node = MockNode() - value = MockValue( - data=False, node=node, command_class=const.COMMAND_CLASS_SENSOR_BINARY - ) - values = MockEntityValues(primary=value) - device = binary_sensor.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - value.data = True - value_changed(value) - - assert device.is_on - - -async def test_trigger_sensor_value_changed(hass, mock_openzwave): - """Test value changed for trigger sensor.""" - node = MockNode(manufacturer_id="013c", product_type="0002", product_id="0002") - value = MockValue(data=False, node=node) - value_off_delay = MockValue(data=15, node=node) - values = MockEntityValues(primary=value, off_delay=value_off_delay) - device = binary_sensor.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - value.data = True - await hass.async_add_executor_job(value_changed, value) - assert device.invalidate_after is None - - device.hass = hass - - value.data = True - await hass.async_add_executor_job(value_changed, value) - assert device.is_on - - test_time = device.invalidate_after - datetime.timedelta(seconds=1) - with patch("homeassistant.util.dt.utcnow", return_value=test_time): - assert device.is_on - - test_time = device.invalidate_after - with patch("homeassistant.util.dt.utcnow", return_value=test_time): - assert not device.is_on diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py deleted file mode 100644 index a9ad182c4b1..00000000000 --- a/tests/components/zwave/test_climate.py +++ /dev/null @@ -1,893 +0,0 @@ -"""Test Z-Wave climate devices.""" -import pytest - -from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - HVAC_MODES, - PRESET_AWAY, - PRESET_BOOST, - PRESET_ECO, - PRESET_NONE, - SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, -) -from homeassistant.components.zwave import climate, const -from homeassistant.components.zwave.climate import ( - AUX_HEAT_ZWAVE_MODE, - DEFAULT_HVAC_MODES, -) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -@pytest.fixture -def device(hass, mock_openzwave): - """Fixture to provide a precreated climate device.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT, - data_items=[ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - ], - node=node, - ), - setpoint_heating=MockValue(data=1, node=node), - setpoint_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_zxt_120(hass, mock_openzwave): - """Fixture to provide a precreated climate device.""" - node = MockNode(manufacturer_id="5254", product_id="8377") - - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT, - data_items=[ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - ], - node=node, - ), - setpoint_heating=MockValue(data=1, node=node), - setpoint_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_action=MockValue(data=7, node=node), - zxt_120_swing_mode=MockValue(data="test3", data_items=[6, 7, 8], node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_mapping(hass, mock_openzwave): - """Fixture to provide a precreated climate device. Test state mapping.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data="Heat", - data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], - node=node, - ), - setpoint_heating=MockValue(data=1, node=node), - setpoint_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="heating", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_unknown(hass, mock_openzwave): - """Fixture to provide a precreated climate device. Test state unknown.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data="Heat", - data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], - node=node, - ), - setpoint_heating=MockValue(data=1, node=node), - setpoint_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="test4", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_heat_cool(hass, mock_openzwave): - """Fixture to provide a precreated climate device. Test state heat only.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT, - data_items=[ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - "Heat Eco", - "Cool Eco", - ], - node=node, - ), - setpoint_heating=MockValue(data=1, node=node), - setpoint_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="test4", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_heat_cool_range(hass, mock_openzwave): - """Fixture to provide a precreated climate device. Target range mode.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT_COOL, - data_items=[ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - ], - node=node, - ), - setpoint_heating=MockValue(data=1, node=node), - setpoint_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="test4", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_heat_cool_away(hass, mock_openzwave): - """Fixture to provide a precreated climate device. Target range mode.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT_COOL, - data_items=[ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - PRESET_AWAY, - ], - node=node, - ), - setpoint_heating=MockValue(data=2, node=node), - setpoint_cooling=MockValue(data=9, node=node), - setpoint_away_heating=MockValue(data=1, node=node), - setpoint_away_cooling=MockValue(data=10, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="test4", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_heat_eco(hass, mock_openzwave): - """Fixture to provide a precreated climate device. heat/heat eco.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT, - data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], - node=node, - ), - setpoint_heating=MockValue(data=2, node=node), - setpoint_eco_heating=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="test4", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_aux_heat(hass, mock_openzwave): - """Fixture to provide a precreated climate device. aux heat.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT, - data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "Aux Heat"], - node=node, - ), - setpoint_heating=MockValue(data=2, node=node), - setpoint_eco_heating=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data="test4", node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_single_setpoint(hass, mock_openzwave): - """Fixture to provide a precreated climate device. - - SETPOINT_THERMOSTAT device class. - """ - - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node - ), - mode=None, - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -@pytest.fixture -def device_single_setpoint_with_mode(hass, mock_openzwave): - """Fixture to provide a precreated climate device. - - SETPOINT_THERMOSTAT device class with COMMAND_CLASS_THERMOSTAT_MODE command class - """ - - node = MockNode() - values = MockEntityValues( - primary=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node - ), - mode=MockValue( - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - data=HVAC_MODE_HEAT, - data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT], - node=node, - ), - temperature=MockValue(data=5, node=node, units=None), - fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), - operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_action=MockValue(data=7, node=node), - ) - device = climate.get_device(hass, node=node, values=values, node_config={}) - - yield device - - -def test_get_device_detects_none(hass, mock_openzwave): - """Test get_device returns None.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - - device = climate.get_device(hass, node=node, values=values, node_config={}) - assert device is None - - -def test_get_device_detects_multiple_setpoint_device(device): - """Test get_device returns a Z-Wave multiple setpoint device.""" - assert isinstance(device, climate.ZWaveClimateMultipleSetpoint) - - -def test_get_device_detects_single_setpoint_device(device_single_setpoint): - """Test get_device returns a Z-Wave single setpoint device.""" - assert isinstance(device_single_setpoint, climate.ZWaveClimateSingleSetpoint) - - -def test_default_hvac_modes(): - """Test whether all hvac modes are included in default_hvac_modes.""" - for hvac_mode in HVAC_MODES: - assert hvac_mode in DEFAULT_HVAC_MODES - - -def test_supported_features(device): - """Test supported features flags.""" - assert ( - device.supported_features - == SUPPORT_FAN_MODE - + SUPPORT_TARGET_TEMPERATURE - + SUPPORT_TARGET_TEMPERATURE_RANGE - ) - - -def test_supported_features_temp_range(device_heat_cool_range): - """Test supported features flags with target temp range.""" - device = device_heat_cool_range - assert ( - device.supported_features - == SUPPORT_FAN_MODE - + SUPPORT_TARGET_TEMPERATURE - + SUPPORT_TARGET_TEMPERATURE_RANGE - ) - - -def test_supported_features_preset_mode(device_mapping): - """Test supported features flags with swing mode.""" - device = device_mapping - assert ( - device.supported_features - == SUPPORT_FAN_MODE - + SUPPORT_TARGET_TEMPERATURE - + SUPPORT_TARGET_TEMPERATURE_RANGE - + SUPPORT_PRESET_MODE - ) - - -def test_supported_features_preset_mode_away(device_heat_cool_away): - """Test supported features flags with swing mode.""" - device = device_heat_cool_away - assert ( - device.supported_features - == SUPPORT_FAN_MODE - + SUPPORT_TARGET_TEMPERATURE - + SUPPORT_TARGET_TEMPERATURE_RANGE - + SUPPORT_PRESET_MODE - ) - - -def test_supported_features_swing_mode(device_zxt_120): - """Test supported features flags with swing mode.""" - device = device_zxt_120 - assert ( - device.supported_features - == SUPPORT_FAN_MODE - + SUPPORT_TARGET_TEMPERATURE - + SUPPORT_TARGET_TEMPERATURE_RANGE - + SUPPORT_SWING_MODE - ) - - -def test_supported_features_aux_heat(device_aux_heat): - """Test supported features flags with aux heat.""" - device = device_aux_heat - assert ( - device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_AUX_HEAT - ) - - -def test_supported_features_single_setpoint(device_single_setpoint): - """Test supported features flags for SETPOINT_THERMOSTAT.""" - device = device_single_setpoint - assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE - - -def test_supported_features_single_setpoint_with_mode(device_single_setpoint_with_mode): - """Test supported features flags for SETPOINT_THERMOSTAT.""" - device = device_single_setpoint_with_mode - assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE - - -def test_zxt_120_swing_mode(device_zxt_120): - """Test operation of the zxt 120 swing mode.""" - device = device_zxt_120 - - assert device.swing_modes == [6, 7, 8] - assert device._zxt_120 == 1 - - # Test set mode - assert device.values.zxt_120_swing_mode.data == "test3" - device.set_swing_mode("test_swing_set") - assert device.values.zxt_120_swing_mode.data == "test_swing_set" - - # Test mode changed - value_changed(device.values.zxt_120_swing_mode) - assert device.swing_mode == "test_swing_set" - device.values.zxt_120_swing_mode.data = "test_swing_updated" - value_changed(device.values.zxt_120_swing_mode) - assert device.swing_mode == "test_swing_updated" - - -def test_temperature_unit(device): - """Test temperature unit.""" - assert device.temperature_unit == TEMP_CELSIUS - device.values.temperature.units = "F" - value_changed(device.values.temperature) - assert device.temperature_unit == TEMP_FAHRENHEIT - device.values.temperature.units = "C" - value_changed(device.values.temperature) - assert device.temperature_unit == TEMP_CELSIUS - - -def test_data_lists(device): - """Test data lists from zwave value items.""" - assert device.fan_modes == [3, 4, 5] - assert device.hvac_modes == [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - ] - assert device.preset_modes == [] - device.values.primary = None - assert device.preset_modes == [] - - -def test_data_lists_single_setpoint(device_single_setpoint): - """Test data lists from zwave value items.""" - device = device_single_setpoint - assert device.fan_modes == [3, 4, 5] - assert device.hvac_modes == [] - assert device.preset_modes == [] - - -def test_data_lists_single_setpoint_with_mode(device_single_setpoint_with_mode): - """Test data lists from zwave value items.""" - device = device_single_setpoint_with_mode - assert device.fan_modes == [3, 4, 5] - assert device.hvac_modes == [HVAC_MODE_OFF, HVAC_MODE_HEAT] - assert device.preset_modes == [] - - -def test_data_lists_mapping(device_mapping): - """Test data lists from zwave value items.""" - device = device_mapping - assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] - assert device.preset_modes == ["boost", "none"] - device.values.primary = None - assert device.preset_modes == [] - - -def test_target_value_set(device): - """Test values changed for climate device.""" - assert device.values.setpoint_heating.data == 1 - assert device.values.setpoint_cooling.data == 10 - device.set_temperature() - assert device.values.setpoint_heating.data == 1 - assert device.values.setpoint_cooling.data == 10 - device.set_temperature(**{ATTR_TEMPERATURE: 2}) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 10 - device.set_hvac_mode(HVAC_MODE_COOL) - value_changed(device.values.primary) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 10 - device.set_temperature(**{ATTR_TEMPERATURE: 9}) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 9 - - -def test_target_value_set_range(device_heat_cool_range): - """Test values changed for climate device.""" - device = device_heat_cool_range - assert device.values.setpoint_heating.data == 1 - assert device.values.setpoint_cooling.data == 10 - device.set_temperature() - assert device.values.setpoint_heating.data == 1 - assert device.values.setpoint_cooling.data == 10 - device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 2}) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 10 - device.set_temperature(**{ATTR_TARGET_TEMP_HIGH: 9}) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 9 - device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 3, ATTR_TARGET_TEMP_HIGH: 8}) - assert device.values.setpoint_heating.data == 3 - assert device.values.setpoint_cooling.data == 8 - - -def test_target_value_set_range_away(device_heat_cool_away): - """Test values changed for climate device.""" - device = device_heat_cool_away - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 9 - assert device.values.setpoint_away_heating.data == 1 - assert device.values.setpoint_away_cooling.data == 10 - device.set_preset_mode(PRESET_AWAY) - device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 0, ATTR_TARGET_TEMP_HIGH: 11}) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_cooling.data == 9 - assert device.values.setpoint_away_heating.data == 0 - assert device.values.setpoint_away_cooling.data == 11 - - -def test_target_value_set_eco(device_heat_eco): - """Test values changed for climate device.""" - device = device_heat_eco - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_eco_heating.data == 1 - device.set_preset_mode("heat econ") - device.set_temperature(**{ATTR_TEMPERATURE: 0}) - assert device.values.setpoint_heating.data == 2 - assert device.values.setpoint_eco_heating.data == 0 - - -def test_target_value_set_single_setpoint(device_single_setpoint): - """Test values changed for climate device.""" - device = device_single_setpoint - assert device.values.primary.data == 1 - device.set_temperature(**{ATTR_TEMPERATURE: 2}) - assert device.values.primary.data == 2 - - -def test_operation_value_set(device): - """Test values changed for climate device.""" - assert device.values.primary.data == HVAC_MODE_HEAT - device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.primary.data == HVAC_MODE_COOL - device.set_preset_mode(PRESET_ECO) - assert device.values.primary.data == PRESET_ECO - device.set_preset_mode(PRESET_NONE) - assert device.values.primary.data == HVAC_MODE_HEAT_COOL - device.values.primary = None - device.set_hvac_mode("test_set_failes") - assert device.values.primary is None - device.set_preset_mode("test_set_failes") - assert device.values.primary is None - - -def test_operation_value_set_mapping(device_mapping): - """Test values changed for climate device. Mapping.""" - device = device_mapping - assert device.values.primary.data == "Heat" - device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.primary.data == "Cool" - device.set_hvac_mode(HVAC_MODE_OFF) - assert device.values.primary.data == "Off" - device.set_preset_mode(PRESET_BOOST) - assert device.values.primary.data == "Full Power" - device.set_preset_mode(PRESET_ECO) - assert device.values.primary.data == "eco" - - -def test_operation_value_set_unknown(device_unknown): - """Test values changed for climate device. Unknown.""" - device = device_unknown - assert device.values.primary.data == "Heat" - device.set_preset_mode("Abcdefg") - assert device.values.primary.data == "Abcdefg" - device.set_preset_mode(PRESET_NONE) - assert device.values.primary.data == HVAC_MODE_HEAT_COOL - - -def test_operation_value_set_heat_cool(device_heat_cool): - """Test values changed for climate device. Heat/Cool only.""" - device = device_heat_cool - assert device.values.primary.data == HVAC_MODE_HEAT - device.set_preset_mode("Heat Eco") - assert device.values.primary.data == "Heat Eco" - device.set_preset_mode(PRESET_NONE) - assert device.values.primary.data == HVAC_MODE_HEAT - device.set_preset_mode("Cool Eco") - assert device.values.primary.data == "Cool Eco" - device.set_preset_mode(PRESET_NONE) - assert device.values.primary.data == HVAC_MODE_COOL - - -def test_fan_mode_value_set(device): - """Test values changed for climate device.""" - assert device.values.fan_mode.data == "test2" - device.set_fan_mode("test_fan_set") - assert device.values.fan_mode.data == "test_fan_set" - device.values.fan_mode = None - device.set_fan_mode("test_fan_set_failes") - assert device.values.fan_mode is None - - -def test_target_value_changed(device): - """Test values changed for climate device.""" - assert device.target_temperature == 1 - device.values.setpoint_heating.data = 2 - value_changed(device.values.setpoint_heating) - assert device.target_temperature == 2 - device.values.primary.data = HVAC_MODE_COOL - value_changed(device.values.primary) - assert device.target_temperature == 10 - device.values.setpoint_cooling.data = 9 - value_changed(device.values.setpoint_cooling) - assert device.target_temperature == 9 - - -def test_target_range_changed(device_heat_cool_range): - """Test values changed for climate device.""" - device = device_heat_cool_range - assert device.target_temperature_low == 1 - assert device.target_temperature_high == 10 - device.values.setpoint_heating.data = 2 - value_changed(device.values.setpoint_heating) - assert device.target_temperature_low == 2 - assert device.target_temperature_high == 10 - device.values.setpoint_cooling.data = 9 - value_changed(device.values.setpoint_cooling) - assert device.target_temperature_low == 2 - assert device.target_temperature_high == 9 - - -def test_target_changed_preset_range(device_heat_cool_away): - """Test values changed for climate device.""" - device = device_heat_cool_away - assert device.target_temperature_low == 2 - assert device.target_temperature_high == 9 - device.values.primary.data = PRESET_AWAY - value_changed(device.values.primary) - assert device.target_temperature_low == 1 - assert device.target_temperature_high == 10 - device.values.setpoint_away_heating.data = 0 - value_changed(device.values.setpoint_away_heating) - device.values.setpoint_away_cooling.data = 11 - value_changed(device.values.setpoint_away_cooling) - assert device.target_temperature_low == 0 - assert device.target_temperature_high == 11 - device.values.primary.data = HVAC_MODE_HEAT_COOL - value_changed(device.values.primary) - assert device.target_temperature_low == 2 - assert device.target_temperature_high == 9 - - -def test_target_changed_eco(device_heat_eco): - """Test values changed for climate device.""" - device = device_heat_eco - assert device.target_temperature == 2 - device.values.primary.data = "heat econ" - value_changed(device.values.primary) - assert device.target_temperature == 1 - device.values.setpoint_eco_heating.data = 0 - value_changed(device.values.setpoint_eco_heating) - assert device.target_temperature == 0 - device.values.primary.data = HVAC_MODE_HEAT - value_changed(device.values.primary) - assert device.target_temperature == 2 - - -def test_target_changed_with_mode(device): - """Test values changed for climate device.""" - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.target_temperature == 1 - device.values.primary.data = HVAC_MODE_COOL - value_changed(device.values.primary) - assert device.target_temperature == 10 - device.values.primary.data = HVAC_MODE_HEAT_COOL - value_changed(device.values.primary) - assert device.target_temperature_low == 1 - assert device.target_temperature_high == 10 - - -def test_target_value_changed_single_setpoint(device_single_setpoint): - """Test values changed for climate device.""" - device = device_single_setpoint - assert device.target_temperature == 1 - device.values.primary.data = 2 - value_changed(device.values.primary) - assert device.target_temperature == 2 - - -def test_temperature_value_changed(device): - """Test values changed for climate device.""" - assert device.current_temperature == 5 - device.values.temperature.data = 3 - value_changed(device.values.temperature) - assert device.current_temperature == 3 - - -def test_operation_value_changed(device): - """Test values changed for climate device.""" - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == PRESET_NONE - device.values.primary.data = HVAC_MODE_COOL - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_COOL - assert device.preset_mode == PRESET_NONE - device.values.primary.data = HVAC_MODE_OFF - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_OFF - assert device.preset_mode == PRESET_NONE - device.values.primary = None - assert device.hvac_mode == HVAC_MODE_HEAT_COOL - assert device.preset_mode == PRESET_NONE - - -def test_operation_value_changed_preset(device_mapping): - """Test preset changed for climate device.""" - device = device_mapping - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == PRESET_NONE - device.values.primary.data = PRESET_ECO - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_HEAT_COOL - assert device.preset_mode == PRESET_ECO - - -def test_operation_value_changed_mapping(device_mapping): - """Test values changed for climate device. Mapping.""" - device = device_mapping - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == PRESET_NONE - device.values.primary.data = "Off" - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_OFF - assert device.preset_mode == PRESET_NONE - device.values.primary.data = "Cool" - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_COOL - assert device.preset_mode == PRESET_NONE - - -def test_operation_value_changed_mapping_preset(device_mapping): - """Test values changed for climate device. Mapping with presets.""" - device = device_mapping - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == PRESET_NONE - device.values.primary.data = "Full Power" - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_HEAT_COOL - assert device.preset_mode == PRESET_BOOST - device.values.primary = None - assert device.hvac_mode == HVAC_MODE_HEAT_COOL - assert device.preset_mode == PRESET_NONE - - -def test_operation_value_changed_unknown(device_unknown): - """Test preset changed for climate device. Unknown.""" - device = device_unknown - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == PRESET_NONE - device.values.primary.data = "Abcdefg" - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_HEAT_COOL - assert device.preset_mode == "Abcdefg" - - -def test_operation_value_changed_heat_cool(device_heat_cool): - """Test preset changed for climate device. Heat/Cool only.""" - device = device_heat_cool - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == PRESET_NONE - device.values.primary.data = "Cool Eco" - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_COOL - assert device.preset_mode == "Cool Eco" - device.values.primary.data = "Heat Eco" - value_changed(device.values.primary) - assert device.hvac_mode == HVAC_MODE_HEAT - assert device.preset_mode == "Heat Eco" - - -def test_fan_mode_value_changed(device): - """Test values changed for climate device.""" - assert device.fan_mode == "test2" - device.values.fan_mode.data = "test_updated_fan" - value_changed(device.values.fan_mode) - assert device.fan_mode == "test_updated_fan" - - -def test_hvac_action_value_changed(device): - """Test values changed for climate device.""" - assert device.hvac_action == CURRENT_HVAC_HEAT - device.values.operating_state.data = CURRENT_HVAC_COOL - value_changed(device.values.operating_state) - assert device.hvac_action == CURRENT_HVAC_COOL - - -def test_hvac_action_value_changed_mapping(device_mapping): - """Test values changed for climate device.""" - device = device_mapping - assert device.hvac_action == CURRENT_HVAC_HEAT - device.values.operating_state.data = "cooling" - value_changed(device.values.operating_state) - assert device.hvac_action == CURRENT_HVAC_COOL - - -def test_hvac_action_value_changed_unknown(device_unknown): - """Test values changed for climate device.""" - device = device_unknown - assert device.hvac_action == "test4" - device.values.operating_state.data = "another_hvac_action" - value_changed(device.values.operating_state) - assert device.hvac_action == "another_hvac_action" - - -def test_fan_action_value_changed(device): - """Test values changed for climate device.""" - assert device.extra_state_attributes[climate.ATTR_FAN_ACTION] == 7 - device.values.fan_action.data = 9 - value_changed(device.values.fan_action) - assert device.extra_state_attributes[climate.ATTR_FAN_ACTION] == 9 - - -def test_aux_heat_unsupported_set(device): - """Test aux heat for climate device.""" - assert device.values.primary.data == HVAC_MODE_HEAT - device.turn_aux_heat_on() - assert device.values.primary.data == HVAC_MODE_HEAT - device.turn_aux_heat_off() - assert device.values.primary.data == HVAC_MODE_HEAT - - -def test_aux_heat_unsupported_value_changed(device): - """Test aux heat for climate device.""" - assert device.is_aux_heat is None - device.values.primary.data = HVAC_MODE_HEAT - value_changed(device.values.primary) - assert device.is_aux_heat is None - - -def test_aux_heat_set(device_aux_heat): - """Test aux heat for climate device.""" - device = device_aux_heat - assert device.values.primary.data == HVAC_MODE_HEAT - device.turn_aux_heat_on() - assert device.values.primary.data == AUX_HEAT_ZWAVE_MODE - device.turn_aux_heat_off() - assert device.values.primary.data == HVAC_MODE_HEAT - - -def test_aux_heat_value_changed(device_aux_heat): - """Test aux heat for climate device.""" - device = device_aux_heat - assert device.is_aux_heat is False - device.values.primary.data = AUX_HEAT_ZWAVE_MODE - value_changed(device.values.primary) - assert device.is_aux_heat is True - device.values.primary.data = HVAC_MODE_HEAT - value_changed(device.values.primary) - assert device.is_aux_heat is False diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py deleted file mode 100644 index e7283de25b4..00000000000 --- a/tests/components/zwave/test_cover.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Test Z-Wave cover devices.""" -from unittest.mock import MagicMock - -import pytest - -from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN -from homeassistant.components.zwave import ( - CONF_INVERT_OPENCLOSE_BUTTONS, - CONF_INVERT_PERCENT, - const, - cover, -) - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_detects_none(hass, mock_openzwave): - """Test device returns none.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value, node=node) - - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert device is None - - -def test_get_device_detects_rollershutter(hass, mock_openzwave): - """Test device returns rollershutter.""" - hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode() - value = MockValue( - data=0, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - values = MockEntityValues(primary=value, open=None, close=None, node=node) - - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert isinstance(device, cover.ZwaveRollershutter) - - -def test_get_device_detects_garagedoor_switch(hass, mock_openzwave): - """Test device returns garage door.""" - node = MockNode() - value = MockValue( - data=False, node=node, command_class=const.COMMAND_CLASS_SWITCH_BINARY - ) - values = MockEntityValues(primary=value, node=node) - - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert isinstance(device, cover.ZwaveGarageDoorSwitch) - assert device.device_class == "garage" - assert device.supported_features == SUPPORT_OPEN | SUPPORT_CLOSE - - -def test_get_device_detects_garagedoor_barrier(hass, mock_openzwave): - """Test device returns garage door.""" - node = MockNode() - value = MockValue( - data="Closed", node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR - ) - values = MockEntityValues(primary=value, node=node) - - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - assert isinstance(device, cover.ZwaveGarageDoorBarrier) - assert device.device_class == "garage" - assert device.supported_features == SUPPORT_OPEN | SUPPORT_CLOSE - - -def test_roller_no_position_workaround(hass, mock_openzwave): - """Test position changed.""" - hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode(manufacturer_id="0047", product_type="5a52") - value = MockValue( - data=45, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - assert device.current_cover_position is None - - -def test_roller_value_changed(hass, mock_openzwave): - """Test position changed.""" - hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode() - value = MockValue( - data=None, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - values = MockEntityValues(primary=value, open=None, close=None, node=node) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - assert device.current_cover_position is None - assert device.is_closed is None - - value.data = 2 - value_changed(value) - - assert device.current_cover_position == 0 - assert device.is_closed - - value.data = 35 - value_changed(value) - - assert device.current_cover_position == 35 - assert not device.is_closed - - value.data = 97 - value_changed(value) - - assert device.current_cover_position == 100 - assert not device.is_closed - - -def test_roller_commands(hass, mock_openzwave): - """Test position changed.""" - mock_network = hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode() - value = MockValue( - data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - open_value = MockValue(data=False, node=node) - close_value = MockValue(data=False, node=node) - values = MockEntityValues( - primary=value, open=open_value, close=close_value, node=node - ) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - device.set_cover_position(position=25) - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - assert value_id == value.value_id - assert brightness == 25 - - device.open_cover() - assert mock_network.manager.pressButton.called - (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] - assert value_id == open_value.value_id - - device.close_cover() - assert mock_network.manager.pressButton.called - (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] - assert value_id == close_value.value_id - - device.stop_cover() - assert mock_network.manager.releaseButton.called - (value_id,) = mock_network.manager.releaseButton.mock_calls.pop(0)[1] - assert value_id == open_value.value_id - - -def test_roller_invert_percent(hass, mock_openzwave): - """Test position changed.""" - mock_network = hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode() - value = MockValue( - data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - open_value = MockValue(data=False, node=node) - close_value = MockValue(data=False, node=node) - values = MockEntityValues( - primary=value, open=open_value, close=close_value, node=node - ) - device = cover.get_device( - hass=hass, node=node, values=values, node_config={CONF_INVERT_PERCENT: True} - ) - - device.set_cover_position(position=25) - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - assert value_id == value.value_id - assert brightness == 75 - - device.open_cover() - assert mock_network.manager.pressButton.called - (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] - assert value_id == open_value.value_id - - -def test_roller_reverse_open_close(hass, mock_openzwave): - """Test position changed.""" - mock_network = hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode() - value = MockValue( - data=50, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - open_value = MockValue(data=False, node=node) - close_value = MockValue(data=False, node=node) - values = MockEntityValues( - primary=value, open=open_value, close=close_value, node=node - ) - device = cover.get_device( - hass=hass, - node=node, - values=values, - node_config={CONF_INVERT_OPENCLOSE_BUTTONS: True}, - ) - - device.open_cover() - assert mock_network.manager.pressButton.called - (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] - assert value_id == close_value.value_id - - device.close_cover() - assert mock_network.manager.pressButton.called - (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] - assert value_id == open_value.value_id - - device.stop_cover() - assert mock_network.manager.releaseButton.called - (value_id,) = mock_network.manager.releaseButton.mock_calls.pop(0)[1] - assert value_id == close_value.value_id - - -def test_switch_garage_value_changed(hass, mock_openzwave): - """Test position changed.""" - node = MockNode() - value = MockValue( - data=False, node=node, command_class=const.COMMAND_CLASS_SWITCH_BINARY - ) - values = MockEntityValues(primary=value, node=node) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - assert device.is_closed - - value.data = True - value_changed(value) - assert not device.is_closed - - -def test_switch_garage_commands(hass, mock_openzwave): - """Test position changed.""" - node = MockNode() - value = MockValue( - data=False, node=node, command_class=const.COMMAND_CLASS_SWITCH_BINARY - ) - values = MockEntityValues(primary=value, node=node) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - assert value.data is False - device.open_cover() - assert value.data is True - device.close_cover() - assert value.data is False - - -def test_barrier_garage_value_changed(hass, mock_openzwave): - """Test position changed.""" - node = MockNode() - value = MockValue( - data="Closed", node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR - ) - values = MockEntityValues(primary=value, node=node) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - assert device.is_closed - assert not device.is_opening - assert not device.is_closing - - value.data = "Opening" - value_changed(value) - assert not device.is_closed - assert device.is_opening - assert not device.is_closing - - value.data = "Opened" - value_changed(value) - assert not device.is_closed - assert not device.is_opening - assert not device.is_closing - - value.data = "Closing" - value_changed(value) - assert not device.is_closed - assert not device.is_opening - assert device.is_closing - - -def test_barrier_garage_commands(hass, mock_openzwave): - """Test position changed.""" - node = MockNode() - value = MockValue( - data="Closed", node=node, command_class=const.COMMAND_CLASS_BARRIER_OPERATOR - ) - values = MockEntityValues(primary=value, node=node) - device = cover.get_device(hass=hass, node=node, values=values, node_config={}) - - assert value.data == "Closed" - device.open_cover() - assert value.data == "Opened" - device.close_cover() - assert value.data == "Closed" diff --git a/tests/components/zwave/test_fan.py b/tests/components/zwave/test_fan.py deleted file mode 100644 index 0104353f851..00000000000 --- a/tests/components/zwave/test_fan.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Test Z-Wave fans.""" -import pytest - -from homeassistant.components.fan import SUPPORT_SET_SPEED -from homeassistant.components.zwave import fan - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_detects_fan(mock_openzwave): - """Test get_device returns a zwave fan.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - - device = fan.get_device(node=node, values=values, node_config={}) - assert isinstance(device, fan.ZwaveFan) - assert device.supported_features == SUPPORT_SET_SPEED - - -def test_fan_turn_on(mock_openzwave): - """Test turning on a zwave fan.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - device = fan.get_device(node=node, values=values, node_config={}) - - device.turn_on() - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - assert value_id == value.value_id - assert brightness == 255 - - node.reset_mock() - - device.turn_on(percentage=0) - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - - assert value_id == value.value_id - assert brightness == 0 - - node.reset_mock() - - device.turn_on(percentage=1) - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - - assert value_id == value.value_id - assert brightness == 1 - - node.reset_mock() - - device.turn_on(percentage=50) - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - - assert value_id == value.value_id - assert brightness == 50 - - node.reset_mock() - - device.turn_on(percentage=100) - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - - assert value_id == value.value_id - assert brightness == 99 - - -def test_fan_turn_off(mock_openzwave): - """Test turning off a dimmable zwave fan.""" - node = MockNode() - value = MockValue(data=46, node=node) - values = MockEntityValues(primary=value) - device = fan.get_device(node=node, values=values, node_config={}) - - device.turn_off() - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - assert value_id == value.value_id - assert brightness == 0 diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py deleted file mode 100644 index 745d6d8ce57..00000000000 --- a/tests/components/zwave/test_init.py +++ /dev/null @@ -1,1977 +0,0 @@ -"""Tests for the Z-Wave init.""" -import asyncio -from collections import OrderedDict -from datetime import datetime -from unittest.mock import MagicMock, patch - -import pytest -import voluptuous as vol - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components import zwave -from homeassistant.components.zwave import ( - CONF_DEVICE_CONFIG_GLOB, - CONFIG_SCHEMA, - DATA_NETWORK, - const, -) -from homeassistant.const import ATTR_NAME -from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util import dt as dt_util - -from tests.common import async_fire_time_changed, mock_registry -from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -@pytest.fixture(autouse=True) -def mock_storage(hass_storage): - """Autouse hass_storage for the TestCase tests.""" - - -@pytest.fixture -async def zwave_setup(hass): - """Zwave setup.""" - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - -@pytest.fixture -async def zwave_setup_ready(hass, zwave_setup): - """Zwave setup and set network to ready.""" - zwave_network = hass.data[DATA_NETWORK] - zwave_network.state = MockNetwork.STATE_READY - - await hass.async_start() - - -async def test_valid_device_config(hass, mock_openzwave): - """Test valid device config.""" - device_config = {"light.kitchen": {"ignored": "true"}} - result = await async_setup_component( - hass, "zwave", {"zwave": {"device_config": device_config}} - ) - await hass.async_block_till_done() - - assert result - - -async def test_invalid_device_config(hass, mock_openzwave): - """Test invalid device config.""" - device_config = {"light.kitchen": {"some_ignored": "true"}} - result = await async_setup_component( - hass, "zwave", {"zwave": {"device_config": device_config}} - ) - await hass.async_block_till_done() - - assert not result - - -def test_config_access_error(): - """Test threading error accessing config values.""" - node = MagicMock() - - def side_effect(): - raise RuntimeError - - node.values.values.side_effect = side_effect - result = zwave.get_config_value(node, 1) - assert result is None - - -async def test_network_options(hass, mock_openzwave): - """Test network options.""" - result = await async_setup_component( - hass, - "zwave", - {"zwave": {"usb_path": "mock_usb_path", "config_path": "mock_config_path"}}, - ) - await hass.async_block_till_done() - - assert result - - network = hass.data[zwave.DATA_NETWORK] - assert network.options.device == "mock_usb_path" - assert network.options.config_path == "mock_config_path" - - -async def test_network_key_validation(hass, mock_openzwave): - """Test network key validation.""" - test_values = [ - ( - "0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, " - "0x0C, 0x0D, 0x0E, 0x0F, 0x10" - ), - ( - "0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D," - "0x0E,0x0F,0x10" - ), - ] - for value in test_values: - result = zwave.CONFIG_SCHEMA({"zwave": {"network_key": value}}) - assert result["zwave"]["network_key"] == value - - -async def test_erronous_network_key_fails_validation(hass, mock_openzwave): - """Test failing erroneous network key validation.""" - test_values = [ - ( - "0x 01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, " - "0x0C, 0x0D, 0x0E, 0x0F, 0x10" - ), - ( - "0X01,0X02,0X03,0X04,0X05,0X06,0X07,0X08,0X09,0X0A,0X0B,0X0C,0X0D," - "0X0E,0X0F,0X10" - ), - "invalid", - "1234567", - 1234567, - ] - for value in test_values: - with pytest.raises(vol.Invalid): - zwave.CONFIG_SCHEMA({"zwave": {"network_key": value}}) - - -async def test_auto_heal_midnight(hass, mock_openzwave, legacy_patchable_time): - """Test network auto-heal at midnight.""" - await async_setup_component(hass, "zwave", {"zwave": {"autoheal": True}}) - await hass.async_block_till_done() - - network = hass.data[zwave.DATA_NETWORK] - assert not network.heal.called - - time = datetime(2017, 5, 6, 0, 0, 0, tzinfo=dt_util.UTC) - async_fire_time_changed(hass, time) - await hass.async_block_till_done() - await hass.async_block_till_done() - assert network.heal.called - assert len(network.heal.mock_calls) == 1 - - -async def test_auto_heal_disabled(hass, mock_openzwave): - """Test network auto-heal disabled.""" - await async_setup_component(hass, "zwave", {"zwave": {"autoheal": False}}) - await hass.async_block_till_done() - - network = hass.data[zwave.DATA_NETWORK] - assert not network.heal.called - - time = datetime(2017, 5, 6, 0, 0, 0, tzinfo=dt_util.UTC) - async_fire_time_changed(hass, time) - await hass.async_block_till_done() - assert not network.heal.called - - -async def test_setup_platform(hass, mock_openzwave): - """Test invalid device config.""" - mock_device = MagicMock() - hass.data[DATA_NETWORK] = MagicMock() - hass.data[zwave.DATA_DEVICES] = {456: mock_device} - async_add_entities = MagicMock() - - result = await zwave.async_setup_platform(hass, None, async_add_entities, None) - assert not result - assert not async_add_entities.called - - result = await zwave.async_setup_platform( - hass, None, async_add_entities, {const.DISCOVERY_DEVICE: 123} - ) - assert not result - assert not async_add_entities.called - - result = await zwave.async_setup_platform( - hass, None, async_add_entities, {const.DISCOVERY_DEVICE: 456} - ) - assert result - assert async_add_entities.called - assert len(async_add_entities.mock_calls) == 1 - assert async_add_entities.mock_calls[0][1][0] == [mock_device] - - -async def test_zwave_ready_wait(hass, mock_openzwave, zwave_setup): - """Test that zwave continues after waiting for network ready.""" - sleeps = [] - - def utcnow(): - return datetime.fromtimestamp(len(sleeps)) - - asyncio_sleep = asyncio.sleep - - async def sleep(duration, loop=None): - if duration > 0: - sleeps.append(duration) - await asyncio_sleep(0) - - with patch("homeassistant.components.zwave.dt_util.utcnow", new=utcnow), patch( - "asyncio.sleep", new=sleep - ), patch.object(zwave, "_LOGGER") as mock_logger: - hass.data[DATA_NETWORK].state = MockNetwork.STATE_STARTED - - await hass.async_start() - - assert len(sleeps) == const.NETWORK_READY_WAIT_SECS - assert mock_logger.warning.called - assert len(mock_logger.warning.mock_calls) == 1 - assert mock_logger.warning.mock_calls[0][1][1] == const.NETWORK_READY_WAIT_SECS - - -async def test_device_entity(hass, mock_openzwave): - """Test device entity base class.""" - node = MockNode(node_id="10", name="Mock Node") - value = MockValue( - data=False, - node=node, - instance=2, - object_id="11", - label="Sensor", - command_class=const.COMMAND_CLASS_SENSOR_BINARY, - ) - power_value = MockValue( - data=50.123456, node=node, precision=3, command_class=const.COMMAND_CLASS_METER - ) - values = MockEntityValues(primary=value, power=power_value) - device = zwave.ZWaveDeviceEntity(values, "zwave") - device.hass = hass - device.value_added() - device.update_properties() - await hass.async_block_till_done() - - assert not device.should_poll - assert device.unique_id == "10-11" - assert device.name == "Mock Node Sensor" - assert device.extra_state_attributes[zwave.ATTR_POWER] == 50.123 - - -async def test_node_removed(hass, mock_openzwave): - """Test node removed in base class.""" - # Create a mock node & node entity - node = MockNode(node_id="10", name="Mock Node") - value = MockValue( - data=False, - node=node, - instance=2, - object_id="11", - label="Sensor", - command_class=const.COMMAND_CLASS_SENSOR_BINARY, - ) - power_value = MockValue( - data=50.123456, node=node, precision=3, command_class=const.COMMAND_CLASS_METER - ) - values = MockEntityValues(primary=value, power=power_value) - device = zwave.ZWaveDeviceEntity(values, "zwave") - device.hass = hass - device.entity_id = "zwave.mock_node" - device.value_added() - device.update_properties() - await hass.async_block_till_done() - - # Save it to the entity registry - registry = mock_registry(hass) - registry.async_get_or_create("zwave", "zwave", device.unique_id) - device.entity_id = registry.async_get_entity_id("zwave", "zwave", device.unique_id) - - # Create dummy entity registry entries for other integrations - hue_entity = registry.async_get_or_create("light", "hue", 1234) - zha_entity = registry.async_get_or_create("sensor", "zha", 5678) - - # Verify our Z-Wave entity is registered - assert registry.async_is_registered(device.entity_id) - - # Remove it - entity_id = device.entity_id - await device.node_removed() - - # Verify registry entry for our Z-Wave node is gone - assert not registry.async_is_registered(entity_id) - - # Verify registry entries for our other entities remain - assert registry.async_is_registered(hue_entity.entity_id) - assert registry.async_is_registered(zha_entity.entity_id) - - -async def test_node_discovery(hass, mock_openzwave): - """Test discovery of a node.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_NODE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode(node_id=14) - await hass.async_add_executor_job(mock_receivers[0], node) - await hass.async_block_till_done() - - assert hass.states.get("zwave.mock_node").state == "unknown" - - -async def test_unparsed_node_discovery(hass, mock_openzwave): - """Test discovery of a node.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_NODE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode(node_id=14, manufacturer_name=None, name=None, is_ready=False) - - sleeps = [] - - def utcnow(): - return datetime.fromtimestamp(len(sleeps)) - - asyncio_sleep = asyncio.sleep - - async def sleep(duration, loop=None): - if duration > 0: - sleeps.append(duration) - await asyncio_sleep(0) - - with patch("homeassistant.components.zwave.dt_util.utcnow", new=utcnow), patch( - "asyncio.sleep", new=sleep - ), patch.object(zwave, "_LOGGER") as mock_logger: - await hass.async_add_executor_job(mock_receivers[0], node) - await hass.async_block_till_done() - - assert len(sleeps) == const.NODE_READY_WAIT_SECS - assert mock_logger.warning.called - assert len(mock_logger.warning.mock_calls) == 1 - assert mock_logger.warning.mock_calls[0][1][1:] == ( - 14, - const.NODE_READY_WAIT_SECS, - ) - assert hass.states.get("zwave.unknown_node_14").state == "unknown" - - -async def test_node_ignored(hass, mock_openzwave): - """Test discovery of a node.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_NODE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component( - hass, - "zwave", - {"zwave": {"device_config": {"zwave.mock_node": {"ignored": True}}}}, - ) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode(node_id=14) - await hass.async_add_executor_job(mock_receivers[0], node) - await hass.async_block_till_done() - - assert hass.states.get("zwave.mock_node") is None - - -async def test_value_discovery(hass, mock_openzwave): - """Test discovery of a node.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_VALUE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_SENSOR_BINARY) - value = MockValue( - data=False, - node=node, - index=12, - instance=13, - command_class=const.COMMAND_CLASS_SENSOR_BINARY, - type=const.TYPE_BOOL, - genre=const.GENRE_USER, - ) - await hass.async_add_executor_job(mock_receivers[0], node, value) - await hass.async_block_till_done() - - assert hass.states.get("binary_sensor.mock_node_mock_value").state == "off" - - -async def test_value_entities(hass, mock_openzwave): - """Test discovery of a node.""" - mock_receivers = {} - - def mock_connect(receiver, signal, *args, **kwargs): - mock_receivers[signal] = receiver - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - zwave_network = hass.data[DATA_NETWORK] - zwave_network.state = MockNetwork.STATE_READY - - await hass.async_start() - - assert mock_receivers - - await hass.async_add_executor_job( - mock_receivers[MockNetwork.SIGNAL_ALL_NODES_QUERIED] - ) - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_SENSOR_BINARY) - zwave_network.nodes = {node.node_id: node} - value = MockValue( - data=False, - node=node, - index=12, - instance=1, - command_class=const.COMMAND_CLASS_SENSOR_BINARY, - type=const.TYPE_BOOL, - genre=const.GENRE_USER, - ) - node.values = {"primary": value, value.value_id: value} - value2 = MockValue( - data=False, - node=node, - index=12, - instance=2, - label="Mock Value B", - command_class=const.COMMAND_CLASS_SENSOR_BINARY, - type=const.TYPE_BOOL, - genre=const.GENRE_USER, - ) - node.values[value2.value_id] = value2 - - await hass.async_add_executor_job( - mock_receivers[MockNetwork.SIGNAL_NODE_ADDED], node - ) - await hass.async_add_executor_job( - mock_receivers[MockNetwork.SIGNAL_VALUE_ADDED], node, value - ) - await hass.async_add_executor_job( - mock_receivers[MockNetwork.SIGNAL_VALUE_ADDED], node, value2 - ) - await hass.async_block_till_done() - - assert hass.states.get("binary_sensor.mock_node_mock_value").state == "off" - assert hass.states.get("binary_sensor.mock_node_mock_value_b").state == "off" - - ent_reg = er.async_get(hass) - dev_reg = dr.async_get(hass) - - entry = ent_reg.async_get("zwave.mock_node") - assert entry is not None - assert entry.unique_id == f"node-{node.node_id}" - node_dev_id = entry.device_id - - entry = ent_reg.async_get("binary_sensor.mock_node_mock_value") - assert entry is not None - assert entry.unique_id == f"{node.node_id}-{value.object_id}" - assert entry.name is None - assert entry.device_id == node_dev_id - - entry = ent_reg.async_get("binary_sensor.mock_node_mock_value_b") - assert entry is not None - assert entry.unique_id == f"{node.node_id}-{value2.object_id}" - assert entry.name is None - assert entry.device_id != node_dev_id - device_id_b = entry.device_id - - device = dev_reg.async_get(node_dev_id) - assert device is not None - assert device.name == node.name - old_device = device - - device = dev_reg.async_get(device_id_b) - assert device is not None - assert device.name == f"{node.name} ({value2.instance})" - - # test renaming without updating - await hass.services.async_call( - "zwave", - "rename_node", - {const.ATTR_NODE_ID: node.node_id, ATTR_NAME: "Demo Node"}, - ) - await hass.async_block_till_done() - - assert node.name == "Demo Node" - - entry = ent_reg.async_get("zwave.mock_node") - assert entry is not None - - entry = ent_reg.async_get("binary_sensor.mock_node_mock_value") - assert entry is not None - - entry = ent_reg.async_get("binary_sensor.mock_node_mock_value_b") - assert entry is not None - - device = dev_reg.async_get(node_dev_id) - assert device is not None - assert device.id == old_device.id - assert device.name == node.name - - device = dev_reg.async_get(device_id_b) - assert device is not None - assert device.name == f"{node.name} ({value2.instance})" - - # test renaming - await hass.services.async_call( - "zwave", - "rename_node", - { - const.ATTR_NODE_ID: node.node_id, - const.ATTR_UPDATE_IDS: True, - ATTR_NAME: "New Node", - }, - ) - await hass.async_block_till_done() - - assert node.name == "New Node" - - entry = ent_reg.async_get("zwave.new_node") - assert entry is not None - assert entry.unique_id == f"node-{node.node_id}" - - entry = ent_reg.async_get("binary_sensor.new_node_mock_value") - assert entry is not None - assert entry.unique_id == f"{node.node_id}-{value.object_id}" - - device = dev_reg.async_get(node_dev_id) - assert device is not None - assert device.id == old_device.id - assert device.name == node.name - - device = dev_reg.async_get(device_id_b) - assert device is not None - assert device.name == f"{node.name} ({value2.instance})" - - await hass.services.async_call( - "zwave", - "rename_value", - { - const.ATTR_NODE_ID: node.node_id, - const.ATTR_VALUE_ID: value.object_id, - const.ATTR_UPDATE_IDS: True, - ATTR_NAME: "New Label", - }, - ) - await hass.async_block_till_done() - - entry = ent_reg.async_get("binary_sensor.new_node_new_label") - assert entry is not None - assert entry.unique_id == f"{node.node_id}-{value.object_id}" - - -async def test_value_discovery_existing_entity(hass, mock_openzwave): - """Test discovery of a node.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_VALUE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode( - node_id=11, - generic=const.GENERIC_TYPE_THERMOSTAT, - specific=const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, - ) - thermostat_mode = MockValue( - data="Heat", - data_items=["Off", "Heat"], - node=node, - command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, - genre=const.GENRE_USER, - ) - setpoint_heating = MockValue( - data=22.0, - node=node, - command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, - index=1, - genre=const.GENRE_USER, - ) - - await hass.async_add_executor_job(mock_receivers[0], node, thermostat_mode) - await hass.async_block_till_done() - - def mock_update(self): - self.hass.add_job(self.async_update_ha_state) - - with patch.object( - zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update - ): - await hass.async_add_executor_job(mock_receivers[0], node, setpoint_heating) - await hass.async_block_till_done() - - assert ( - hass.states.get("climate.mock_node_mock_value").attributes["temperature"] - == 22.0 - ) - assert ( - hass.states.get("climate.mock_node_mock_value").attributes[ - "current_temperature" - ] - is None - ) - - with patch.object( - zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update - ): - temperature = MockValue( - data=23.5, - node=node, - index=1, - command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, - genre=const.GENRE_USER, - units="C", - ) - await hass.async_add_executor_job(mock_receivers[0], node, temperature) - await hass.async_block_till_done() - - assert ( - hass.states.get("climate.mock_node_mock_value").attributes["temperature"] - == 22.0 - ) - assert ( - hass.states.get("climate.mock_node_mock_value").attributes[ - "current_temperature" - ] - == 23.5 - ) - - -async def test_value_discovery_legacy_thermostat(hass, mock_openzwave): - """Test discovery of a node. Special case for legacy thermostats.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_VALUE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode( - node_id=11, - generic=const.GENERIC_TYPE_THERMOSTAT, - specific=const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, - ) - setpoint_heating = MockValue( - data=22.0, - node=node, - command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, - index=1, - genre=const.GENRE_USER, - ) - - await hass.async_add_executor_job(mock_receivers[0], node, setpoint_heating) - await hass.async_block_till_done() - - assert ( - hass.states.get("climate.mock_node_mock_value").attributes["temperature"] - == 22.0 - ) - - -async def test_power_schemes(hass, mock_openzwave): - """Test power attribute.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_VALUE_ADDED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_SWITCH_BINARY) - switch = MockValue( - data=True, - node=node, - index=12, - instance=13, - command_class=const.COMMAND_CLASS_SWITCH_BINARY, - genre=const.GENRE_USER, - type=const.TYPE_BOOL, - ) - await hass.async_add_executor_job(mock_receivers[0], node, switch) - - await hass.async_block_till_done() - - assert hass.states.get("switch.mock_node_mock_value").state == "on" - assert ( - "power_consumption" - not in hass.states.get("switch.mock_node_mock_value").attributes - ) - - def mock_update(self): - self.hass.add_job(self.async_update_ha_state) - - with patch.object( - zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update - ): - power = MockValue( - data=23.5, - node=node, - index=const.INDEX_SENSOR_MULTILEVEL_POWER, - instance=13, - command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, - genre=const.GENRE_USER, # to avoid exception - ) - await hass.async_add_executor_job(mock_receivers[0], node, power) - await hass.async_block_till_done() - - assert ( - hass.states.get("switch.mock_node_mock_value").attributes["power_consumption"] - == 23.5 - ) - - -async def test_network_ready(hass, mock_openzwave): - """Test Node network ready event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_ALL_NODES_QUERIED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_NETWORK_COMPLETE, listener) - - await hass.async_add_executor_job(mock_receivers[0]) - await hass.async_block_till_done() - - assert len(events) == 1 - - -async def test_network_complete(hass, mock_openzwave): - """Test Node network complete event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_AWAKE_NODES_QUERIED: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_NETWORK_READY, listener) - - await hass.async_add_executor_job(mock_receivers[0]) - await hass.async_block_till_done() - - assert len(events) == 1 - - -async def test_network_complete_some_dead(hass, mock_openzwave): - """Test Node network complete some dead event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == MockNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD: - mock_receivers.append(receiver) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - await async_setup_component(hass, "zwave", {"zwave": {}}) - await hass.async_block_till_done() - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_NETWORK_COMPLETE_SOME_DEAD, listener) - - await hass.async_add_executor_job(mock_receivers[0]) - await hass.async_block_till_done() - - assert len(events) == 1 - - -async def test_entity_discovery( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test the creation of a new entity.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - assert not mock_discovery.async_load_platform.called - - assert values.primary is value_class.primary - assert len(list(values)) == 3 - assert sorted(values, key=lambda a: id(a)) == sorted( - [value_class.primary, None, None], key=lambda a: id(a) - ) - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values.check_value(value_class.secondary) - await hass.async_block_till_done() - - assert mock_discovery.async_load_platform.called - assert len(mock_discovery.async_load_platform.mock_calls) == 1 - - args = mock_discovery.async_load_platform.mock_calls[0][1] - assert args[0] == hass - assert args[1] == "mock_component" - assert args[2] == "zwave" - assert args[3] == { - const.DISCOVERY_DEVICE: mock_import_module().get_device().unique_id - } - assert args[4] == zwave_config - - assert values.secondary is value_class.secondary - assert len(list(values)) == 3 - assert sorted(values, key=lambda a: id(a)) == sorted( - [value_class.primary, value_class.secondary, None], key=lambda a: id(a) - ) - - mock_discovery.async_load_platform.reset_mock() - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values.check_value(value_class.optional) - values.check_value(value_class.duplicate_secondary) - values.check_value(value_class.no_match_value) - await hass.async_block_till_done() - - assert not mock_discovery.async_load_platform.called - - assert values.optional is value_class.optional - assert len(list(values)) == 3 - assert sorted(values, key=lambda a: id(a)) == sorted( - [value_class.primary, value_class.secondary, value_class.optional], - key=lambda a: id(a), - ) - - assert values._entity.value_added.called - assert len(values._entity.value_added.mock_calls) == 1 - assert values._entity.value_changed.called - assert len(values._entity.value_changed.mock_calls) == 1 - - -async def test_entity_existing_values( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test the loading of already discovered values.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.values = { - value_class.primary.value_id: value_class.primary, - value_class.secondary.value_id: value_class.secondary, - value_class.optional.value_id: value_class.optional, - value_class.no_match_value.value_id: value_class.no_match_value, - } - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - await hass.async_block_till_done() - - assert mock_discovery.async_load_platform.called - assert len(mock_discovery.async_load_platform.mock_calls) == 1 - args = mock_discovery.async_load_platform.mock_calls[0][1] - assert args[0] == hass - assert args[1] == "mock_component" - assert args[2] == "zwave" - assert args[3] == { - const.DISCOVERY_DEVICE: mock_import_module().get_device().unique_id - } - assert args[4] == zwave_config - assert not value_class.primary.enable_poll.called - - assert values.primary is value_class.primary - assert values.secondary is value_class.secondary - assert values.optional is value_class.optional - assert len(list(values)) == 3 - assert sorted(values, key=lambda a: id(a)) == sorted( - [value_class.primary, value_class.secondary, value_class.optional], - key=lambda a: id(a), - ) - - -async def test_node_schema_mismatch( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test node schema mismatch.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.generic = "no_match" - node.values = { - value_class.primary.value_id: value_class.primary, - value_class.secondary.value_id: value_class.secondary, - } - mock_schema[const.DISC_GENERIC_DEVICE_CLASS] = ["generic_match"] - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - values._check_entity_ready() - await hass.async_block_till_done() - - assert not mock_discovery.async_load_platform.called - - -async def test_entity_workaround_component( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test component workaround.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - node.manufacturer_id = "010f" - node.product_type = "0b00" - value_class.primary.command_class = const.COMMAND_CLASS_SENSOR_ALARM - - entity_id = "binary_sensor.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - mock_schema = { - const.DISC_COMPONENT: "mock_component", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY] - } - }, - } - - with patch.object( - zwave, "async_dispatcher_send" - ) as mock_dispatch_send, patch.object( - zwave, "discovery", mock_discovery - ), patch.object( - zwave, "import_module", mock_import_module - ): - - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - values._check_entity_ready() - await hass.async_block_till_done() - - assert mock_dispatch_send.called - assert len(mock_dispatch_send.mock_calls) == 1 - args = mock_dispatch_send.mock_calls[0][1] - assert args[1] == "zwave_new_binary_sensor" - - -async def test_entity_workaround_ignore( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test ignore workaround.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.manufacturer_id = "010f" - node.product_type = "0301" - value_class.primary.command_class = const.COMMAND_CLASS_SWITCH_BINARY - - mock_schema = { - const.DISC_COMPONENT: "mock_component", - const.DISC_VALUES: { - const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SWITCH_BINARY] - } - }, - } - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - values._check_entity_ready() - await hass.async_block_till_done() - - assert not mock_discovery.async_load_platform.called - - -async def test_entity_config_ignore( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test ignore config.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.values = { - value_class.primary.value_id: value_class.primary, - value_class.secondary.value_id: value_class.secondary, - } - device_config = {entity_id: {zwave.CONF_IGNORED: True}} - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - values._check_entity_ready() - await hass.async_block_till_done() - - assert not mock_discovery.async_load_platform.called - - -async def test_entity_config_ignore_with_registry( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test ignore config. - - The case when the device is in entity registry. - """ - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.values = { - value_class.primary.value_id: value_class.primary, - value_class.secondary.value_id: value_class.secondary, - } - device_config = {"mock_component.registry_id": {zwave.CONF_IGNORED: True}} - with patch.object(registry, "async_schedule_save"): - registry.async_get_or_create( - "mock_component", - zwave.DOMAIN, - "567-1000", - suggested_object_id="registry_id", - ) - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - await hass.async_block_till_done() - - assert not mock_discovery.async_load_platform.called - - -async def test_entity_platform_ignore( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test platform ignore device.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.values = { - value_class.primary.value_id: value_class.primary, - value_class.secondary.value_id: value_class.secondary, - } - - import_module = MagicMock() - platform = MagicMock() - import_module.return_value = platform - platform.get_device.return_value = None - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", import_module - ): - zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - await hass.async_block_till_done() - - assert not mock_discovery.async_load_platform.called - - -async def test_config_polling_intensity( - hass, mock_discovery, mock_import_module, mock_values, mock_openzwave, zwave_setup -): - """Test polling intensity.""" - (node, value_class, mock_schema) = mock_values - - registry = mock_registry(hass) - - entity_id = "mock_component.mock_node_mock_value" - zwave_config = {"zwave": {}} - device_config = {entity_id: {}} - - node.values = { - value_class.primary.value_id: value_class.primary, - value_class.secondary.value_id: value_class.secondary, - } - device_config = {entity_id: {zwave.CONF_POLLING_INTENSITY: 123}} - - with patch.object(zwave, "discovery", mock_discovery), patch.object( - zwave, "import_module", mock_import_module - ): - values = zwave.ZWaveDeviceEntityValues( - hass=hass, - schema=mock_schema, - primary_value=value_class.primary, - zwave_config=zwave_config, - device_config=device_config, - registry=registry, - ) - values._check_entity_ready() - await hass.async_block_till_done() - - assert mock_discovery.async_load_platform.called - - assert value_class.primary.enable_poll.called - assert len(value_class.primary.enable_poll.mock_calls) == 1 - assert value_class.primary.enable_poll.mock_calls[0][1][0] == 123 - - -async def test_device_config_glob_is_ordered(): - """Test that device_config_glob preserves order.""" - conf = CONFIG_SCHEMA({"zwave": {CONF_DEVICE_CONFIG_GLOB: OrderedDict()}}) - assert isinstance(conf["zwave"][CONF_DEVICE_CONFIG_GLOB], OrderedDict) - - -async def test_add_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave add_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "add_node", {}) - await hass.async_block_till_done() - - assert zwave_network.controller.add_node.called - assert len(zwave_network.controller.add_node.mock_calls) == 1 - assert len(zwave_network.controller.add_node.mock_calls[0][1]) == 0 - - -async def test_add_node_secure(hass, mock_openzwave, zwave_setup_ready): - """Test zwave add_node_secure service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "add_node_secure", {}) - await hass.async_block_till_done() - - assert zwave_network.controller.add_node.called - assert len(zwave_network.controller.add_node.mock_calls) == 1 - assert zwave_network.controller.add_node.mock_calls[0][1][0] is True - - -async def test_remove_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave remove_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "remove_node", {}) - await hass.async_block_till_done() - - assert zwave_network.controller.remove_node.called - assert len(zwave_network.controller.remove_node.mock_calls) == 1 - - -async def test_cancel_command(hass, mock_openzwave, zwave_setup_ready): - """Test zwave cancel_command service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "cancel_command", {}) - await hass.async_block_till_done() - - assert zwave_network.controller.cancel_command.called - assert len(zwave_network.controller.cancel_command.mock_calls) == 1 - - -async def test_heal_network(hass, mock_openzwave, zwave_setup_ready): - """Test zwave heal_network service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "heal_network", {}) - await hass.async_block_till_done() - - assert zwave_network.heal.called - assert len(zwave_network.heal.mock_calls) == 1 - - -async def test_soft_reset(hass, mock_openzwave, zwave_setup_ready): - """Test zwave soft_reset service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "soft_reset", {}) - await hass.async_block_till_done() - - assert zwave_network.controller.soft_reset.called - assert len(zwave_network.controller.soft_reset.mock_calls) == 1 - - -async def test_test_network(hass, mock_openzwave, zwave_setup_ready): - """Test zwave test_network service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call("zwave", "test_network", {}) - await hass.async_block_till_done() - - assert zwave_network.test.called - assert len(zwave_network.test.mock_calls) == 1 - - -async def test_stop_network(hass, mock_openzwave, zwave_setup_ready): - """Test zwave stop_network service.""" - zwave_network = hass.data[DATA_NETWORK] - - with patch.object(hass.bus, "fire") as mock_fire: - await hass.services.async_call("zwave", "stop_network", {}) - await hass.async_block_till_done() - - assert zwave_network.stop.called - assert len(zwave_network.stop.mock_calls) == 1 - assert mock_fire.called - assert len(mock_fire.mock_calls) == 1 - assert mock_fire.mock_calls[0][1][0] == const.EVENT_NETWORK_STOP - - -async def test_rename_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave rename_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - zwave_network.nodes = {11: MagicMock()} - await hass.services.async_call( - "zwave", - "rename_node", - {const.ATTR_NODE_ID: 11, ATTR_NAME: "test_name"}, - ) - await hass.async_block_till_done() - - assert zwave_network.nodes[11].name == "test_name" - - -async def test_rename_value(hass, mock_openzwave, zwave_setup_ready): - """Test zwave rename_value service.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - value = MockValue(index=12, value_id=123456, label="Old Label") - node.values = {123456: value} - zwave_network.nodes = {11: node} - - assert value.label == "Old Label" - await hass.services.async_call( - "zwave", - "rename_value", - { - const.ATTR_NODE_ID: 11, - const.ATTR_VALUE_ID: 123456, - ATTR_NAME: "New Label", - }, - ) - await hass.async_block_till_done() - - assert value.label == "New Label" - - -async def test_set_poll_intensity_enable(hass, mock_openzwave, zwave_setup_ready): - """Test zwave set_poll_intensity service, successful set.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - value = MockValue(index=12, value_id=123456, poll_intensity=0) - node.values = {123456: value} - zwave_network.nodes = {11: node} - - assert value.poll_intensity == 0 - await hass.services.async_call( - "zwave", - "set_poll_intensity", - { - const.ATTR_NODE_ID: 11, - const.ATTR_VALUE_ID: 123456, - const.ATTR_POLL_INTENSITY: 4, - }, - ) - await hass.async_block_till_done() - - enable_poll = value.enable_poll - assert value.enable_poll.called - assert len(enable_poll.mock_calls) == 2 - assert enable_poll.mock_calls[0][1][0] == 4 - - -async def test_set_poll_intensity_enable_failed( - hass, mock_openzwave, zwave_setup_ready -): - """Test zwave set_poll_intensity service, failed set.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - value = MockValue(index=12, value_id=123456, poll_intensity=0) - value.enable_poll.return_value = False - node.values = {123456: value} - zwave_network.nodes = {11: node} - - assert value.poll_intensity == 0 - await hass.services.async_call( - "zwave", - "set_poll_intensity", - { - const.ATTR_NODE_ID: 11, - const.ATTR_VALUE_ID: 123456, - const.ATTR_POLL_INTENSITY: 4, - }, - ) - await hass.async_block_till_done() - - enable_poll = value.enable_poll - assert value.enable_poll.called - assert len(enable_poll.mock_calls) == 1 - - -async def test_set_poll_intensity_disable(hass, mock_openzwave, zwave_setup_ready): - """Test zwave set_poll_intensity service, successful disable.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - value = MockValue(index=12, value_id=123456, poll_intensity=4) - node.values = {123456: value} - zwave_network.nodes = {11: node} - - assert value.poll_intensity == 4 - await hass.services.async_call( - "zwave", - "set_poll_intensity", - { - const.ATTR_NODE_ID: 11, - const.ATTR_VALUE_ID: 123456, - const.ATTR_POLL_INTENSITY: 0, - }, - ) - await hass.async_block_till_done() - - disable_poll = value.disable_poll - assert value.disable_poll.called - assert len(disable_poll.mock_calls) == 2 - - -async def test_set_poll_intensity_disable_failed( - hass, mock_openzwave, zwave_setup_ready -): - """Test zwave set_poll_intensity service, failed disable.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - value = MockValue(index=12, value_id=123456, poll_intensity=4) - value.disable_poll.return_value = False - node.values = {123456: value} - zwave_network.nodes = {11: node} - - assert value.poll_intensity == 4 - await hass.services.async_call( - "zwave", - "set_poll_intensity", - { - const.ATTR_NODE_ID: 11, - const.ATTR_VALUE_ID: 123456, - const.ATTR_POLL_INTENSITY: 0, - }, - ) - await hass.async_block_till_done() - - disable_poll = value.disable_poll - assert value.disable_poll.called - assert len(disable_poll.mock_calls) == 1 - - -async def test_remove_failed_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave remove_failed_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call( - "zwave", "remove_failed_node", {const.ATTR_NODE_ID: 12} - ) - await hass.async_block_till_done() - - remove_failed_node = zwave_network.controller.remove_failed_node - assert remove_failed_node.called - assert len(remove_failed_node.mock_calls) == 1 - assert remove_failed_node.mock_calls[0][1][0] == 12 - - -async def test_replace_failed_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave replace_failed_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - await hass.services.async_call( - "zwave", "replace_failed_node", {const.ATTR_NODE_ID: 13} - ) - await hass.async_block_till_done() - - replace_failed_node = zwave_network.controller.replace_failed_node - assert replace_failed_node.called - assert len(replace_failed_node.mock_calls) == 1 - assert replace_failed_node.mock_calls[0][1][0] == 13 - - -async def test_set_config_parameter(hass, mock_openzwave, zwave_setup_ready): - """Test zwave set_config_parameter service.""" - zwave_network = hass.data[DATA_NETWORK] - - value_byte = MockValue( - index=12, - command_class=const.COMMAND_CLASS_CONFIGURATION, - type=const.TYPE_BYTE, - ) - value_list = MockValue( - index=13, - command_class=const.COMMAND_CLASS_CONFIGURATION, - type=const.TYPE_LIST, - data_items=["item1", "item2", "item3"], - ) - value_button = MockValue( - index=14, - command_class=const.COMMAND_CLASS_CONFIGURATION, - type=const.TYPE_BUTTON, - ) - value_list_int = MockValue( - index=15, - command_class=const.COMMAND_CLASS_CONFIGURATION, - type=const.TYPE_LIST, - data_items=["1", "2", "3"], - ) - value_bool = MockValue( - index=16, - command_class=const.COMMAND_CLASS_CONFIGURATION, - type=const.TYPE_BOOL, - ) - node = MockNode(node_id=14) - node.get_values.return_value = { - 12: value_byte, - 13: value_list, - 14: value_button, - 15: value_list_int, - 16: value_bool, - } - zwave_network.nodes = {14: node} - - # Byte - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 12, - const.ATTR_CONFIG_VALUE: 7, - }, - ) - await hass.async_block_till_done() - - assert value_byte.data == 7 - - # List - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 13, - const.ATTR_CONFIG_VALUE: "item3", - }, - ) - await hass.async_block_till_done() - - assert value_list.data == "item3" - - # Button - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 14, - const.ATTR_CONFIG_VALUE: True, - }, - ) - await hass.async_block_till_done() - - assert zwave_network.manager.pressButton.called - assert zwave_network.manager.releaseButton.called - - # List of Ints - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 15, - const.ATTR_CONFIG_VALUE: 3, - }, - ) - await hass.async_block_till_done() - - assert value_list_int.data == "3" - - # Boolean Truthy - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 16, - const.ATTR_CONFIG_VALUE: "True", - }, - ) - await hass.async_block_till_done() - - assert value_bool.data == 1 - - # Boolean Falsy - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 16, - const.ATTR_CONFIG_VALUE: "False", - }, - ) - await hass.async_block_till_done() - - assert value_bool.data == 0 - - # Different Parameter Size - await hass.services.async_call( - "zwave", - "set_config_parameter", - { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 19, - const.ATTR_CONFIG_VALUE: 0x01020304, - const.ATTR_CONFIG_SIZE: 4, - }, - ) - await hass.async_block_till_done() - - assert node.set_config_param.called - assert len(node.set_config_param.mock_calls) == 1 - assert node.set_config_param.mock_calls[0][1][0] == 19 - assert node.set_config_param.mock_calls[0][1][1] == 0x01020304 - assert node.set_config_param.mock_calls[0][1][2] == 4 - node.set_config_param.reset_mock() - - -async def test_print_config_parameter(hass, mock_openzwave, zwave_setup_ready, caplog): - """Test zwave print_config_parameter service.""" - zwave_network = hass.data[DATA_NETWORK] - - value1 = MockValue( - index=12, command_class=const.COMMAND_CLASS_CONFIGURATION, data=1234 - ) - value2 = MockValue( - index=13, command_class=const.COMMAND_CLASS_CONFIGURATION, data=2345 - ) - node = MockNode(node_id=14) - node.values = {12: value1, 13: value2} - zwave_network.nodes = {14: node} - - caplog.clear() - - await hass.services.async_call( - "zwave", - "print_config_parameter", - {const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 13}, - ) - await hass.async_block_till_done() - - assert "Config parameter 13 on Node 14: 2345" in caplog.text - - -async def test_print_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave print_node_parameter service.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - - zwave_network.nodes = {14: node} - - with patch.object(zwave, "_LOGGER") as mock_logger: - await hass.services.async_call("zwave", "print_node", {const.ATTR_NODE_ID: 14}) - await hass.async_block_till_done() - - assert "FOUND NODE " in mock_logger.info.mock_calls[0][1][0] - - -async def test_set_wakeup(hass, mock_openzwave, zwave_setup_ready): - """Test zwave set_wakeup service.""" - zwave_network = hass.data[DATA_NETWORK] - - value = MockValue(index=12, command_class=const.COMMAND_CLASS_WAKE_UP) - node = MockNode(node_id=14) - node.values = {12: value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", "set_wakeup", {const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_VALUE: 15} - ) - await hass.async_block_till_done() - - assert value.data == 15 - - node.can_wake_up_value = False - await hass.services.async_call( - "zwave", "set_wakeup", {const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_VALUE: 20} - ) - await hass.async_block_till_done() - - assert value.data == 15 - - -async def test_reset_node_meters(hass, mock_openzwave, zwave_setup_ready): - """Test zwave reset_node_meters service.""" - zwave_network = hass.data[DATA_NETWORK] - - value = MockValue( - instance=1, index=8, data=99.5, command_class=const.COMMAND_CLASS_METER - ) - reset_value = MockValue( - instance=1, index=33, command_class=const.COMMAND_CLASS_METER - ) - node = MockNode(node_id=14) - node.values = {8: value, 33: reset_value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", - "reset_node_meters", - {const.ATTR_NODE_ID: 14, const.ATTR_INSTANCE: 2}, - ) - await hass.async_block_till_done() - - assert not zwave_network.manager.pressButton.called - assert not zwave_network.manager.releaseButton.called - - await hass.services.async_call( - "zwave", "reset_node_meters", {const.ATTR_NODE_ID: 14} - ) - await hass.async_block_till_done() - - assert zwave_network.manager.pressButton.called - (value_id,) = zwave_network.manager.pressButton.mock_calls.pop(0)[1] - assert value_id == reset_value.value_id - assert zwave_network.manager.releaseButton.called - (value_id,) = zwave_network.manager.releaseButton.mock_calls.pop(0)[1] - assert value_id == reset_value.value_id - - -async def test_add_association(hass, mock_openzwave, zwave_setup_ready): - """Test zwave change_association service.""" - zwave_network = hass.data[DATA_NETWORK] - - ZWaveGroup = mock_openzwave.group.ZWaveGroup - group = MagicMock() - ZWaveGroup.return_value = group - - value = MockValue(index=12, command_class=const.COMMAND_CLASS_WAKE_UP) - node = MockNode(node_id=14) - node.values = {12: value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", - "change_association", - { - const.ATTR_ASSOCIATION: "add", - const.ATTR_NODE_ID: 14, - const.ATTR_TARGET_NODE_ID: 24, - const.ATTR_GROUP: 3, - const.ATTR_INSTANCE: 5, - }, - ) - await hass.async_block_till_done() - - assert ZWaveGroup.called - assert len(ZWaveGroup.mock_calls) == 2 - assert ZWaveGroup.mock_calls[0][1][0] == 3 - assert ZWaveGroup.mock_calls[0][1][2] == 14 - assert group.add_association.called - assert len(group.add_association.mock_calls) == 1 - assert group.add_association.mock_calls[0][1][0] == 24 - assert group.add_association.mock_calls[0][1][1] == 5 - - -async def test_remove_association(hass, mock_openzwave, zwave_setup_ready): - """Test zwave change_association service.""" - zwave_network = hass.data[DATA_NETWORK] - - ZWaveGroup = mock_openzwave.group.ZWaveGroup - group = MagicMock() - ZWaveGroup.return_value = group - - value = MockValue(index=12, command_class=const.COMMAND_CLASS_WAKE_UP) - node = MockNode(node_id=14) - node.values = {12: value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", - "change_association", - { - const.ATTR_ASSOCIATION: "remove", - const.ATTR_NODE_ID: 14, - const.ATTR_TARGET_NODE_ID: 24, - const.ATTR_GROUP: 3, - const.ATTR_INSTANCE: 5, - }, - ) - await hass.async_block_till_done() - - assert ZWaveGroup.called - assert len(ZWaveGroup.mock_calls) == 2 - assert ZWaveGroup.mock_calls[0][1][0] == 3 - assert ZWaveGroup.mock_calls[0][1][2] == 14 - assert group.remove_association.called - assert len(group.remove_association.mock_calls) == 1 - assert group.remove_association.mock_calls[0][1][0] == 24 - assert group.remove_association.mock_calls[0][1][1] == 5 - - -async def test_refresh_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave refresh_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=14) - zwave_network.nodes = {14: node} - await hass.services.async_call("zwave", "refresh_node", {const.ATTR_NODE_ID: 14}) - await hass.async_block_till_done() - - assert node.refresh_info.called - assert len(node.refresh_info.mock_calls) == 1 - - -async def test_set_node_value(hass, mock_openzwave, zwave_setup_ready): - """Test zwave set_node_value service.""" - zwave_network = hass.data[DATA_NETWORK] - - value = MockValue(index=12, command_class=const.COMMAND_CLASS_INDICATOR, data=4) - node = MockNode(node_id=14, command_classes=[const.COMMAND_CLASS_INDICATOR]) - node.values = {12: value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", - "set_node_value", - { - const.ATTR_NODE_ID: 14, - const.ATTR_VALUE_ID: 12, - const.ATTR_CONFIG_VALUE: 2, - }, - ) - await hass.async_block_till_done() - - assert zwave_network.nodes[14].values[12].data == 2 - - -async def test_set_node_value_with_long_id_and_text_value( - hass, mock_openzwave, zwave_setup_ready -): - """Test zwave set_node_value service.""" - zwave_network = hass.data[DATA_NETWORK] - - value = MockValue( - index=87512398541236578, - command_class=const.COMMAND_CLASS_SWITCH_COLOR, - data="#ff0000", - ) - node = MockNode(node_id=14, command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - node.values = {87512398541236578: value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", - "set_node_value", - { - const.ATTR_NODE_ID: 14, - const.ATTR_VALUE_ID: "87512398541236578", - const.ATTR_CONFIG_VALUE: "#00ff00", - }, - ) - await hass.async_block_till_done() - - assert zwave_network.nodes[14].values[87512398541236578].data == "#00ff00" - - -async def test_refresh_node_value(hass, mock_openzwave, zwave_setup_ready): - """Test zwave refresh_node_value service.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode( - node_id=14, - command_classes=[const.COMMAND_CLASS_INDICATOR], - network=zwave_network, - ) - value = MockValue( - node=node, index=12, command_class=const.COMMAND_CLASS_INDICATOR, data=2 - ) - value.refresh = MagicMock() - - node.values = {12: value} - node.get_values.return_value = node.values - zwave_network.nodes = {14: node} - - await hass.services.async_call( - "zwave", - "refresh_node_value", - {const.ATTR_NODE_ID: 14, const.ATTR_VALUE_ID: 12}, - ) - await hass.async_block_till_done() - - assert value.refresh.called - - -async def test_heal_node(hass, mock_openzwave, zwave_setup_ready): - """Test zwave heal_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=19) - zwave_network.nodes = {19: node} - await hass.services.async_call("zwave", "heal_node", {const.ATTR_NODE_ID: 19}) - await hass.async_block_till_done() - - assert node.heal.called - assert len(node.heal.mock_calls) == 1 - - -async def test_test_node(hass, mock_openzwave, zwave_setup_ready): - """Test the zwave test_node service.""" - zwave_network = hass.data[DATA_NETWORK] - - node = MockNode(node_id=19) - zwave_network.nodes = {19: node} - await hass.services.async_call("zwave", "test_node", {const.ATTR_NODE_ID: 19}) - await hass.async_block_till_done() - - assert node.test.called - assert len(node.test.mock_calls) == 1 diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py deleted file mode 100644 index 87bfb1ec726..00000000000 --- a/tests/components/zwave/test_light.py +++ /dev/null @@ -1,481 +0,0 @@ -"""Test Z-Wave lights.""" -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant.components import zwave -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, - ATTR_RGBW_COLOR, - ATTR_TRANSITION, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_RGB, - COLOR_MODE_RGBW, - SUPPORT_TRANSITION, -) -from homeassistant.components.zwave import const, light - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -class MockLightValues(MockEntityValues): - """Mock Z-Wave light values.""" - - def __init__(self, **kwargs): - """Initialize the mock zwave values.""" - self.dimming_duration = None - self.color = None - self.color_channels = None - super().__init__(**kwargs) - - -def test_get_device_detects_dimmer(mock_openzwave): - """Test get_device returns a normal dimmer.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - - device = light.get_device(node=node, values=values, node_config={}) - assert isinstance(device, light.ZwaveDimmer) - assert device.color_mode == COLOR_MODE_BRIGHTNESS - assert device.supported_features == 0 - assert device.supported_color_modes == {COLOR_MODE_BRIGHTNESS} - - -def test_get_device_detects_colorlight(mock_openzwave): - """Test get_device returns a color light.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - - device = light.get_device(node=node, values=values, node_config={}) - assert isinstance(device, light.ZwaveColorLight) - assert device.color_mode == COLOR_MODE_RGB - assert device.supported_features == 0 - assert device.supported_color_modes == {COLOR_MODE_RGB} - - -def test_get_device_detects_zw098(mock_openzwave): - """Test get_device returns a zw098 color light.""" - node = MockNode( - manufacturer_id="0086", - product_id="0062", - command_classes=[const.COMMAND_CLASS_SWITCH_COLOR], - ) - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - device = light.get_device(node=node, values=values, node_config={}) - assert isinstance(device, light.ZwaveColorLight) - assert device.color_mode == COLOR_MODE_RGB - assert device.supported_features == 0 - assert device.supported_color_modes == {COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB} - - -def test_get_device_detects_rgbw_light(mock_openzwave): - """Test get_device returns a color light.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - color_channels = MockValue(data=0x1D, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - - device = light.get_device(node=node, values=values, node_config={}) - device.value_added() - assert isinstance(device, light.ZwaveColorLight) - assert device.color_mode == COLOR_MODE_RGBW - assert device.supported_features == 0 - assert device.supported_color_modes == {COLOR_MODE_RGBW} - - -def test_dimmer_turn_on(mock_openzwave): - """Test turning on a dimmable Z-Wave light.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - device = light.get_device(node=node, values=values, node_config={}) - - device.turn_on() - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - assert value_id == value.value_id - assert brightness == 255 - - node.reset_mock() - - device.turn_on(**{ATTR_BRIGHTNESS: 224}) - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - - assert value_id == value.value_id - assert brightness == 87 # round(224 / 255 * 99) - - node.reset_mock() - - device.turn_on(**{ATTR_BRIGHTNESS: 120}) - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - - assert value_id == value.value_id - assert brightness == 47 # round(120 / 255 * 99) - - with patch.object(light, "_LOGGER", MagicMock()) as mock_logger: - device.turn_on(**{ATTR_TRANSITION: 35}) - assert mock_logger.debug.called - assert node.set_dimmer.called - msg, entity_id = mock_logger.debug.mock_calls[0][1] - assert entity_id == device.entity_id - - -def test_dimmer_min_brightness(mock_openzwave): - """Test turning on a dimmable Z-Wave light to its minimum brightness.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - device = light.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - device.turn_on(**{ATTR_BRIGHTNESS: 1}) - - assert device.is_on - assert device.brightness == 1 - - device.turn_on(**{ATTR_BRIGHTNESS: 0}) - - assert device.is_on - assert device.brightness == 0 - - -def test_dimmer_transitions(mock_openzwave): - """Test dimming transition on a dimmable Z-Wave light.""" - node = MockNode() - value = MockValue(data=0, node=node) - duration = MockValue(data=0, node=node) - values = MockLightValues(primary=value, dimming_duration=duration) - device = light.get_device(node=node, values=values, node_config={}) - assert device.color_mode == COLOR_MODE_BRIGHTNESS - assert device.supported_features == SUPPORT_TRANSITION - assert device.supported_color_modes == {COLOR_MODE_BRIGHTNESS} - - # Test turn_on - # Factory Default - device.turn_on() - assert duration.data == 0xFF - - # Seconds transition - device.turn_on(**{ATTR_TRANSITION: 45}) - assert duration.data == 45 - - # Minutes transition - device.turn_on(**{ATTR_TRANSITION: 245}) - assert duration.data == 0x83 - - # Clipped transition - device.turn_on(**{ATTR_TRANSITION: 10000}) - assert duration.data == 0xFE - - # Test turn_off - # Factory Default - device.turn_off() - assert duration.data == 0xFF - - # Seconds transition - device.turn_off(**{ATTR_TRANSITION: 45}) - assert duration.data == 45 - - # Minutes transition - device.turn_off(**{ATTR_TRANSITION: 245}) - assert duration.data == 0x83 - - # Clipped transition - device.turn_off(**{ATTR_TRANSITION: 10000}) - assert duration.data == 0xFE - - -def test_dimmer_turn_off(mock_openzwave): - """Test turning off a dimmable Z-Wave light.""" - node = MockNode() - value = MockValue(data=46, node=node) - values = MockLightValues(primary=value) - device = light.get_device(node=node, values=values, node_config={}) - - device.turn_off() - - assert node.set_dimmer.called - value_id, brightness = node.set_dimmer.mock_calls[0][1] - assert value_id == value.value_id - assert brightness == 0 - - -def test_dimmer_value_changed(mock_openzwave): - """Test value changed for dimmer lights.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - device = light.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - value.data = 46 - value_changed(value) - - assert device.is_on - assert device.brightness == 118 - - -def test_dimmer_refresh_value(mock_openzwave): - """Test value changed for dimmer lights.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - device = light.get_device( - node=node, - values=values, - node_config={zwave.CONF_REFRESH_VALUE: True, zwave.CONF_REFRESH_DELAY: 5}, - ) - - assert not device.is_on - - with patch.object(light, "Timer") as mock_timer: - value.data = 46 - value_changed(value) - - assert not device.is_on - assert mock_timer.called - assert len(mock_timer.mock_calls) == 2 - timeout, callback = mock_timer.mock_calls[0][1][:2] - assert timeout == 5 - assert mock_timer().start.called - assert len(mock_timer().start.mock_calls) == 1 - - with patch.object(light, "Timer") as mock_timer_2: - value_changed(value) - assert not device.is_on - assert mock_timer().cancel.called - assert len(mock_timer_2.mock_calls) == 2 - timeout, callback = mock_timer_2.mock_calls[0][1][:2] - assert timeout == 5 - assert mock_timer_2().start.called - assert len(mock_timer_2().start.mock_calls) == 1 - - callback() - assert device.is_on - assert device.brightness == 118 - - -def test_set_rgb_color(mock_openzwave): - """Test setting zwave light color.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB only - color_channels = MockValue(data=0x1C, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert color.data == "#0000000000" - - device.turn_on(**{ATTR_RGB_COLOR: (0xFF, 0xBF, 0x7F)}) - - assert color.data == "#ffbf7f0000" - - -def test_set_white_value(mock_openzwave): - """Test setting zwave light color.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGBW - color_channels = MockValue(data=0x1D, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert color.data == "#0000000000" - - device.turn_on(**{ATTR_RGBW_COLOR: (0xFF, 0xFF, 0xFF, 0xC8)}) - - assert color.data == "#ffffffc800" - - -def test_disable_white_if_set_color(mock_openzwave): - """ - Test that _white is set to 0 if turn_on with ATTR_RGB_COLOR. - - See Issue #13930 - many RGBW ZWave bulbs will only activate the RGB LED to - produce color if _white is set to zero. - """ - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB only - color_channels = MockValue(data=0x1C, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - device._white = 234 - - assert color.data == "#0000000000" - assert device.rgbw_color == (0, 0, 0, 234) - - device.turn_on(**{ATTR_RGB_COLOR: (0xFF, 0xBF, 0x7F)}) - - assert color.data == "#ffbf7f0000" - assert device.rgbw_color == (0xFF, 0xBF, 0x7F, 0x00) - - -def test_zw098_set_color_temp(mock_openzwave): - """Test setting zwave light color.""" - node = MockNode( - manufacturer_id="0086", - product_id="0062", - command_classes=[const.COMMAND_CLASS_SWITCH_COLOR], - ) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB, warm white, cold white - color_channels = MockValue(data=0x1F, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert color.data == "#0000000000" - - device.turn_on(**{ATTR_COLOR_TEMP: 200}) - - assert color.data == "#00000000ff" - - device.turn_on(**{ATTR_COLOR_TEMP: 400}) - - assert color.data == "#000000ff00" - - -def test_rgb_not_supported(mock_openzwave): - """Test value changed for rgb lights.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports color temperature only - color_channels = MockValue(data=0x01, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.rgb_color is None - assert device.rgbw_color is None - - -def test_no_color_value(mock_openzwave): - """Test value changed for rgb lights.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - values = MockLightValues(primary=value) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.rgb_color is None - assert device.rgbw_color is None - - -def test_no_color_channels_value(mock_openzwave): - """Test value changed for rgb lights.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - values = MockLightValues(primary=value, color=color) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.rgb_color is None - assert device.rgbw_color is None - - -def test_rgb_value_changed(mock_openzwave): - """Test value changed for rgb lights.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB only - color_channels = MockValue(data=0x1C, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.rgb_color == (0, 0, 0) - - color.data = "#ffbf800000" - value_changed(color) - - assert device.rgb_color == (0xFF, 0xBF, 0x80) - - -def test_rgbww_value_changed(mock_openzwave): - """Test value changed for rgb lights.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB, Warm White - color_channels = MockValue(data=0x1D, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.rgbw_color == (0, 0, 0, 0) - - color.data = "#c86400c800" - value_changed(color) - - assert device.rgbw_color == (0xC8, 0x64, 0x00, 0xC8) - - -def test_rgbcw_value_changed(mock_openzwave): - """Test value changed for rgb lights.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB, Cold White - color_channels = MockValue(data=0x1E, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.rgbw_color == (0, 0, 0, 0) - - color.data = "#c86400c800" - value_changed(color) - - assert device.rgbw_color == (0xC8, 0x64, 0x00, 0xC8) - - -def test_ct_value_changed(mock_openzwave): - """Test value changed for zw098 lights.""" - node = MockNode( - manufacturer_id="0086", - product_id="0062", - command_classes=[const.COMMAND_CLASS_SWITCH_COLOR], - ) - value = MockValue(data=0, node=node) - color = MockValue(data="#0000000000", node=node) - # Supports RGB, Cold White - color_channels = MockValue(data=0x1F, node=node) - values = MockLightValues(primary=value, color=color, color_channels=color_channels) - device = light.get_device(node=node, values=values, node_config={}) - - assert device.color_mode == COLOR_MODE_RGB - assert device.color_temp is None - - color.data = "#000000ff00" - value_changed(color) - - assert device.color_mode == COLOR_MODE_COLOR_TEMP - assert device.color_temp == light.TEMP_WARM_HASS - - color.data = "#00000000ff" - value_changed(color) - - assert device.color_mode == COLOR_MODE_COLOR_TEMP - assert device.color_temp == light.TEMP_COLD_HASS - - color.data = "#ff00000000" - value_changed(color) - assert device.color_mode == COLOR_MODE_RGB diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py deleted file mode 100644 index 575df9491ad..00000000000 --- a/tests/components/zwave/test_lock.py +++ /dev/null @@ -1,389 +0,0 @@ -"""Test Z-Wave locks.""" -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant import config_entries -from homeassistant.components.zwave import const, lock - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_detects_lock(mock_openzwave): - """Test get_device returns a Z-Wave lock.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue(data=None, node=node), - access_control=None, - alarm_type=None, - alarm_level=None, - ) - - device = lock.get_device(node=node, values=values, node_config={}) - assert isinstance(device, lock.ZwaveLock) - - -def test_lock_turn_on_and_off(mock_openzwave): - """Test turning on a Z-Wave lock.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue(data=None, node=node), - access_control=None, - alarm_type=None, - alarm_level=None, - ) - device = lock.get_device(node=node, values=values, node_config={}) - - assert not values.primary.data - - device.lock() - assert values.primary.data - - device.unlock() - assert not values.primary.data - - -def test_lock_value_changed(mock_openzwave): - """Test value changed for Z-Wave lock.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue(data=None, node=node), - access_control=None, - alarm_type=None, - alarm_level=None, - ) - device = lock.get_device(node=node, values=values, node_config={}) - - assert not device.is_locked - - values.primary.data = True - value_changed(values.primary) - - assert device.is_locked - - -def test_lock_state_workaround(mock_openzwave): - """Test value changed for Z-Wave lock using notification state.""" - node = MockNode(manufacturer_id="0090", product_id="0440") - values = MockEntityValues( - primary=MockValue(data=True, node=node), - access_control=MockValue(data=1, node=node), - alarm_type=None, - alarm_level=None, - ) - device = lock.get_device(node=node, values=values) - assert device.is_locked - values.access_control.data = 2 - value_changed(values.access_control) - assert not device.is_locked - - -def test_track_message_workaround(mock_openzwave): - """Test value changed for Z-Wave lock by alarm-clearing workaround.""" - node = MockNode( - manufacturer_id="003B", - product_id="5044", - stats={"lastReceivedMessage": [0] * 6}, - ) - values = MockEntityValues( - primary=MockValue(data=True, node=node), - access_control=None, - alarm_type=None, - alarm_level=None, - ) - - # Here we simulate an RF lock. The first lock.get_device will call - # update properties, simulating the first DoorLock report. We then trigger - # a change, simulating the openzwave automatic refreshing behavior (which - # is enabled for at least the lock that needs this workaround) - node.stats["lastReceivedMessage"][5] = const.COMMAND_CLASS_DOOR_LOCK - device = lock.get_device(node=node, values=values) - value_changed(values.primary) - assert device.is_locked - assert device.extra_state_attributes[lock.ATTR_NOTIFICATION] == "RF Lock" - - # Simulate a keypad unlock. We trigger a value_changed() which simulates - # the Alarm notification received from the lock. Then, we trigger - # value_changed() to simulate the automatic refreshing behavior. - values.access_control = MockValue(data=6, node=node) - values.alarm_type = MockValue(data=19, node=node) - values.alarm_level = MockValue(data=3, node=node) - node.stats["lastReceivedMessage"][5] = const.COMMAND_CLASS_ALARM - value_changed(values.access_control) - node.stats["lastReceivedMessage"][5] = const.COMMAND_CLASS_DOOR_LOCK - values.primary.data = False - value_changed(values.primary) - assert not device.is_locked - assert ( - device.extra_state_attributes[lock.ATTR_LOCK_STATUS] - == "Unlocked with Keypad by user 3" - ) - - # Again, simulate an RF lock. - device.lock() - node.stats["lastReceivedMessage"][5] = const.COMMAND_CLASS_DOOR_LOCK - value_changed(values.primary) - assert device.is_locked - assert device.extra_state_attributes[lock.ATTR_NOTIFICATION] == "RF Lock" - - -def test_v2btze_value_changed(mock_openzwave): - """Test value changed for v2btze Z-Wave lock.""" - node = MockNode(manufacturer_id="010e", product_id="0002") - values = MockEntityValues( - primary=MockValue(data=None, node=node), - v2btze_advanced=MockValue(data="Advanced", node=node), - access_control=MockValue(data=19, node=node), - alarm_type=None, - alarm_level=None, - ) - device = lock.get_device(node=node, values=values, node_config={}) - assert device._v2btze - - assert not device.is_locked - - values.access_control.data = 24 - value_changed(values.primary) - - assert device.is_locked - - -def test_alarm_type_workaround(mock_openzwave): - """Test value changed for Z-Wave lock using alarm type.""" - node = MockNode(manufacturer_id="0109", product_id="0000") - values = MockEntityValues( - primary=MockValue(data=True, node=node), - access_control=None, - alarm_type=MockValue(data=16, node=node), - alarm_level=None, - ) - device = lock.get_device(node=node, values=values) - assert not device.is_locked - - values.alarm_type.data = 18 - value_changed(values.alarm_type) - assert device.is_locked - - values.alarm_type.data = 19 - value_changed(values.alarm_type) - assert not device.is_locked - - values.alarm_type.data = 21 - value_changed(values.alarm_type) - assert device.is_locked - - values.alarm_type.data = 22 - value_changed(values.alarm_type) - assert not device.is_locked - - values.alarm_type.data = 24 - value_changed(values.alarm_type) - assert device.is_locked - - values.alarm_type.data = 25 - value_changed(values.alarm_type) - assert not device.is_locked - - values.alarm_type.data = 27 - value_changed(values.alarm_type) - assert device.is_locked - - -def test_lock_access_control(mock_openzwave): - """Test access control for Z-Wave lock.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue(data=None, node=node), - access_control=MockValue(data=11, node=node), - alarm_type=None, - alarm_level=None, - ) - device = lock.get_device(node=node, values=values, node_config={}) - - assert device.extra_state_attributes[lock.ATTR_NOTIFICATION] == "Lock Jammed" - - -def test_lock_alarm_type(mock_openzwave): - """Test alarm type for Z-Wave lock.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue(data=None, node=node), - access_control=None, - alarm_type=MockValue(data=None, node=node), - alarm_level=None, - ) - device = lock.get_device(node=node, values=values, node_config={}) - - assert lock.ATTR_LOCK_STATUS not in device.extra_state_attributes - - values.alarm_type.data = 21 - value_changed(values.alarm_type) - assert ( - device.extra_state_attributes[lock.ATTR_LOCK_STATUS] == "Manually Locked None" - ) - - values.alarm_type.data = 18 - value_changed(values.alarm_type) - assert ( - device.extra_state_attributes[lock.ATTR_LOCK_STATUS] - == "Locked with Keypad by user None" - ) - - values.alarm_type.data = 161 - value_changed(values.alarm_type) - assert device.extra_state_attributes[lock.ATTR_LOCK_STATUS] == "Tamper Alarm: None" - - values.alarm_type.data = 9 - value_changed(values.alarm_type) - assert device.extra_state_attributes[lock.ATTR_LOCK_STATUS] == "Deadbolt Jammed" - - -def test_lock_alarm_level(mock_openzwave): - """Test alarm level for Z-Wave lock.""" - node = MockNode() - values = MockEntityValues( - primary=MockValue(data=None, node=node), - access_control=None, - alarm_type=MockValue(data=None, node=node), - alarm_level=MockValue(data=None, node=node), - ) - device = lock.get_device(node=node, values=values, node_config={}) - - assert lock.ATTR_LOCK_STATUS not in device.extra_state_attributes - - values.alarm_type.data = 21 - values.alarm_level.data = 1 - value_changed(values.alarm_type) - value_changed(values.alarm_level) - assert ( - device.extra_state_attributes[lock.ATTR_LOCK_STATUS] - == "Manually Locked by Key Cylinder or Inside thumb turn" - ) - - values.alarm_type.data = 18 - values.alarm_level.data = "alice" - value_changed(values.alarm_type) - value_changed(values.alarm_level) - assert ( - device.extra_state_attributes[lock.ATTR_LOCK_STATUS] - == "Locked with Keypad by user alice" - ) - - values.alarm_type.data = 161 - values.alarm_level.data = 1 - value_changed(values.alarm_type) - value_changed(values.alarm_level) - assert ( - device.extra_state_attributes[lock.ATTR_LOCK_STATUS] - == "Tamper Alarm: Too many keypresses" - ) - - -async def setup_ozw(hass, mock_openzwave): - """Set up the mock ZWave config entry.""" - hass.config.components.add("zwave") - config_entry = config_entries.ConfigEntry( - 1, - "zwave", - "Mock Title", - {"usb_path": "mock-path", "network_key": "mock-key"}, - "test", - ) - await hass.config_entries.async_forward_entry_setup(config_entry, "lock") - await hass.async_block_till_done() - - -async def test_lock_set_usercode_service(hass, mock_openzwave): - """Test the zwave lock set_usercode service.""" - mock_network = hass.data[const.DATA_NETWORK] = MagicMock() - - node = MockNode(node_id=12) - value0 = MockValue(data=" ", node=node, index=0) - value1 = MockValue(data=" ", node=node, index=1) - - node.get_values.return_value = {value0.value_id: value0, value1.value_id: value1} - - mock_network.nodes = {node.node_id: node} - - await setup_ozw(hass, mock_openzwave) - await hass.async_block_till_done() - - await hass.services.async_call( - lock.DOMAIN, - lock.SERVICE_SET_USERCODE, - { - const.ATTR_NODE_ID: node.node_id, - lock.ATTR_USERCODE: "1234", - lock.ATTR_CODE_SLOT: 1, - }, - ) - await hass.async_block_till_done() - - assert value1.data == "1234" - - mock_network.nodes = {node.node_id: node} - await hass.services.async_call( - lock.DOMAIN, - lock.SERVICE_SET_USERCODE, - { - const.ATTR_NODE_ID: node.node_id, - lock.ATTR_USERCODE: "123", - lock.ATTR_CODE_SLOT: 1, - }, - ) - await hass.async_block_till_done() - - assert value1.data == "1234" - - -async def test_lock_get_usercode_service(hass, mock_openzwave): - """Test the zwave lock get_usercode service.""" - mock_network = hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode(node_id=12) - value0 = MockValue(data=None, node=node, index=0) - value1 = MockValue(data="1234", node=node, index=1) - - node.get_values.return_value = {value0.value_id: value0, value1.value_id: value1} - - await setup_ozw(hass, mock_openzwave) - await hass.async_block_till_done() - - with patch.object(lock, "_LOGGER") as mock_logger: - mock_network.nodes = {node.node_id: node} - await hass.services.async_call( - lock.DOMAIN, - lock.SERVICE_GET_USERCODE, - {const.ATTR_NODE_ID: node.node_id, lock.ATTR_CODE_SLOT: 1}, - ) - await hass.async_block_till_done() - # This service only seems to write to the log - assert mock_logger.info.called - assert len(mock_logger.info.mock_calls) == 1 - assert mock_logger.info.mock_calls[0][1][2] == "1234" - - -async def test_lock_clear_usercode_service(hass, mock_openzwave): - """Test the zwave lock clear_usercode service.""" - mock_network = hass.data[const.DATA_NETWORK] = MagicMock() - node = MockNode(node_id=12) - value0 = MockValue(data=None, node=node, index=0) - value1 = MockValue(data="123", node=node, index=1) - - node.get_values.return_value = {value0.value_id: value0, value1.value_id: value1} - - mock_network.nodes = {node.node_id: node} - - await setup_ozw(hass, mock_openzwave) - await hass.async_block_till_done() - - await hass.services.async_call( - lock.DOMAIN, - lock.SERVICE_CLEAR_USERCODE, - {const.ATTR_NODE_ID: node.node_id, lock.ATTR_CODE_SLOT: 1}, - ) - await hass.async_block_till_done() - - assert value1.data == "\0\0\0" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py deleted file mode 100644 index 56ae0d61d41..00000000000 --- a/tests/components/zwave/test_node_entity.py +++ /dev/null @@ -1,723 +0,0 @@ -"""Test Z-Wave node entity.""" -from unittest.mock import MagicMock, patch - -import pytest - -from homeassistant.components.zwave import const, node_entity -from homeassistant.const import ATTR_ENTITY_ID - -import tests.mock.zwave as mock_zwave - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -async def test_maybe_schedule_update(hass, mock_openzwave): - """Test maybe schedule update.""" - base_entity = node_entity.ZWaveBaseEntity() - base_entity.entity_id = "zwave.bla" - base_entity.hass = hass - - with patch.object(hass.loop, "call_later") as mock_call_later: - base_entity._schedule_update() - assert mock_call_later.called - - base_entity._schedule_update() - assert len(mock_call_later.mock_calls) == 1 - assert base_entity._update_scheduled is True - - do_update = mock_call_later.mock_calls[0][1][1] - - do_update() - assert base_entity._update_scheduled is False - - base_entity._schedule_update() - assert len(mock_call_later.mock_calls) == 2 - - -async def test_node_event_activated(hass, mock_openzwave): - """Test Node event activated event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == mock_zwave.MockNetwork.SIGNAL_NODE_EVENT: - mock_receivers.append(receiver) - - node = mock_zwave.MockNode(node_id=11) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_NODE_EVENT, listener) - - # Test event before entity added to hass - value = 234 - hass.async_add_job(mock_receivers[0], node, value) - await hass.async_block_till_done() - assert len(events) == 0 - - # Add entity to hass - entity.hass = hass - entity.entity_id = "zwave.mock_node" - - value = 234 - hass.async_add_job(mock_receivers[0], node, value) - await hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].data[ATTR_ENTITY_ID] == "zwave.mock_node" - assert events[0].data[const.ATTR_NODE_ID] == 11 - assert events[0].data[const.ATTR_BASIC_LEVEL] == value - - -async def test_scene_activated(hass, mock_openzwave): - """Test scene activated event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == mock_zwave.MockNetwork.SIGNAL_SCENE_EVENT: - mock_receivers.append(receiver) - - node = mock_zwave.MockNode(node_id=11) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_SCENE_ACTIVATED, listener) - - # Test event before entity added to hass - scene_id = 123 - hass.async_add_job(mock_receivers[0], node, scene_id) - await hass.async_block_till_done() - assert len(events) == 0 - - # Add entity to hass - entity.hass = hass - entity.entity_id = "zwave.mock_node" - - scene_id = 123 - hass.async_add_job(mock_receivers[0], node, scene_id) - await hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].data[ATTR_ENTITY_ID] == "zwave.mock_node" - assert events[0].data[const.ATTR_NODE_ID] == 11 - assert events[0].data[const.ATTR_SCENE_ID] == scene_id - - -async def test_central_scene_activated(hass, mock_openzwave): - """Test central scene activated event.""" - mock_receivers = [] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal == mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED: - mock_receivers.append(receiver) - - node = mock_zwave.MockNode(node_id=11) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) - - assert len(mock_receivers) == 1 - - events = [] - - def listener(event): - events.append(event) - - hass.bus.async_listen(const.EVENT_SCENE_ACTIVATED, listener) - - # Test event before entity added to hass - scene_id = 1 - scene_data = 3 - value = mock_zwave.MockValue( - command_class=const.COMMAND_CLASS_CENTRAL_SCENE, index=scene_id, data=scene_data - ) - hass.async_add_job(mock_receivers[0], node, value) - await hass.async_block_till_done() - assert len(events) == 0 - - # Add entity to hass - entity.hass = hass - entity.entity_id = "zwave.mock_node" - - scene_id = 1 - scene_data = 3 - value = mock_zwave.MockValue( - command_class=const.COMMAND_CLASS_CENTRAL_SCENE, index=scene_id, data=scene_data - ) - hass.async_add_job(mock_receivers[0], node, value) - await hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].data[ATTR_ENTITY_ID] == "zwave.mock_node" - assert events[0].data[const.ATTR_NODE_ID] == 11 - assert events[0].data[const.ATTR_SCENE_ID] == scene_id - assert events[0].data[const.ATTR_SCENE_DATA] == scene_data - - -async def test_application_version(hass, mock_openzwave): - """Test application version.""" - mock_receivers = {} - - signal_mocks = [ - mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED, - mock_zwave.MockNetwork.SIGNAL_VALUE_ADDED, - ] - - def mock_connect(receiver, signal, *args, **kwargs): - if signal in signal_mocks: - mock_receivers[signal] = receiver - - node = mock_zwave.MockNode(node_id=11) - - with patch("pydispatch.dispatcher.connect", new=mock_connect): - entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) - - for signal_mock in signal_mocks: - assert signal_mock in mock_receivers.keys() - - events = [] - - def listener(event): - events.append(event) - - # Make sure application version isn't set before - assert ( - node_entity.ATTR_APPLICATION_VERSION not in entity.extra_state_attributes.keys() - ) - - # Add entity to hass - entity.hass = hass - entity.entity_id = "zwave.mock_node" - - # Fire off an added value - value = mock_zwave.MockValue( - command_class=const.COMMAND_CLASS_VERSION, - label="Application Version", - data="5.10", - ) - hass.async_add_job( - mock_receivers[mock_zwave.MockNetwork.SIGNAL_VALUE_ADDED], node, value - ) - await hass.async_block_till_done() - - assert entity.extra_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "5.10" - - # Fire off a changed - value = mock_zwave.MockValue( - command_class=const.COMMAND_CLASS_VERSION, - label="Application Version", - data="4.14", - ) - hass.async_add_job( - mock_receivers[mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED], node, value - ) - await hass.async_block_till_done() - - assert entity.extra_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "4.14" - - -async def test_network_node_changed_from_value(hass, mock_openzwave): - """Test for network_node_changed.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - value = mock_zwave.MockValue(node=node) - with patch.object(entity, "maybe_schedule_update") as mock: - mock_zwave.value_changed(value) - mock.assert_called_once_with() - - -async def test_network_node_changed_from_node(hass, mock_openzwave): - """Test for network_node_changed.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - with patch.object(entity, "maybe_schedule_update") as mock: - mock_zwave.node_changed(node) - mock.assert_called_once_with() - - -async def test_network_node_changed_from_another_node(hass, mock_openzwave): - """Test for network_node_changed.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - with patch.object(entity, "maybe_schedule_update") as mock: - another_node = mock_zwave.MockNode(node_id=1024) - mock_zwave.node_changed(another_node) - assert not mock.called - - -async def test_network_node_changed_from_notification(hass, mock_openzwave): - """Test for network_node_changed.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - with patch.object(entity, "maybe_schedule_update") as mock: - mock_zwave.notification(node_id=node.node_id) - mock.assert_called_once_with() - - -async def test_network_node_changed_from_another_notification(hass, mock_openzwave): - """Test for network_node_changed.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - with patch.object(entity, "maybe_schedule_update") as mock: - mock_zwave.notification(node_id=1024) - assert not mock.called - - -async def test_node_changed(hass, mock_openzwave): - """Test node_changed function.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode( - query_stage="Dynamic", - is_awake=True, - is_ready=False, - is_failed=False, - is_info_received=True, - max_baud_rate=40000, - is_zwave_plus=False, - capabilities=[], - neighbors=[], - location=None, - ) - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - - assert { - "node_id": node.node_id, - "node_name": "Mock Node", - "manufacturer_name": "Test Manufacturer", - "product_name": "Test Product", - } == entity.extra_state_attributes - - node.get_values.return_value = {1: mock_zwave.MockValue(data=1800)} - zwave_network.manager.getNodeStatistics.return_value = { - "receivedCnt": 4, - "ccData": [ - {"receivedCnt": 0, "commandClassId": 134, "sentCnt": 0}, - {"receivedCnt": 1, "commandClassId": 133, "sentCnt": 1}, - {"receivedCnt": 1, "commandClassId": 115, "sentCnt": 1}, - {"receivedCnt": 0, "commandClassId": 114, "sentCnt": 0}, - {"receivedCnt": 0, "commandClassId": 112, "sentCnt": 0}, - {"receivedCnt": 1, "commandClassId": 32, "sentCnt": 1}, - {"receivedCnt": 0, "commandClassId": 0, "sentCnt": 0}, - ], - "receivedUnsolicited": 0, - "sentTS": "2017-03-27 15:38:15:620 ", - "averageRequestRTT": 2462, - "lastResponseRTT": 3679, - "retries": 0, - "sentFailed": 1, - "sentCnt": 7, - "quality": 0, - "lastRequestRTT": 1591, - "lastReceivedMessage": [ - 0, - 4, - 0, - 15, - 3, - 32, - 3, - 0, - 221, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ], - "receivedDups": 1, - "averageResponseRTT": 2443, - "receivedTS": "2017-03-27 15:38:19:298 ", - } - entity.node_changed() - assert { - "node_id": node.node_id, - "node_name": "Mock Node", - "manufacturer_name": "Test Manufacturer", - "product_name": "Test Product", - "query_stage": "Dynamic", - "is_awake": True, - "is_ready": False, - "is_failed": False, - "is_info_received": True, - "max_baud_rate": 40000, - "is_zwave_plus": False, - "battery_level": 42, - "wake_up_interval": 1800, - "averageRequestRTT": 2462, - "averageResponseRTT": 2443, - "lastRequestRTT": 1591, - "lastResponseRTT": 3679, - "receivedCnt": 4, - "receivedDups": 1, - "receivedTS": "2017-03-27 15:38:19:298 ", - "receivedUnsolicited": 0, - "retries": 0, - "sentCnt": 7, - "sentFailed": 1, - "sentTS": "2017-03-27 15:38:15:620 ", - } == entity.extra_state_attributes - - node.can_wake_up_value = False - entity.node_changed() - - assert "wake_up_interval" not in entity.extra_state_attributes - - -async def test_name(hass, mock_openzwave): - """Test name property.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - assert entity.name == "Mock Node" - - -async def test_state_before_update(hass, mock_openzwave): - """Test state before update was called.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - assert entity.state is None - - -async def test_state_not_ready(hass, mock_openzwave): - """Test state property.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode( - query_stage="Dynamic", - is_awake=True, - is_ready=False, - is_failed=False, - is_info_received=True, - ) - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - - node.is_ready = False - entity.node_changed() - assert entity.state == "initializing" - - node.is_failed = True - node.query_stage = "Complete" - entity.node_changed() - assert entity.state == "dead" - - node.is_failed = False - node.is_awake = False - entity.node_changed() - assert entity.state == "sleeping" - - -async def test_state_ready(hass, mock_openzwave): - """Test state property.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode( - query_stage="Dynamic", - is_awake=True, - is_ready=False, - is_failed=False, - is_info_received=True, - ) - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - - node.query_stage = "Complete" - node.is_ready = True - entity.node_changed() - await hass.async_block_till_done() - assert entity.state == "ready" - - node.is_failed = True - entity.node_changed() - assert entity.state == "dead" - - node.is_failed = False - node.is_awake = False - entity.node_changed() - assert entity.state == "sleeping" - - -async def test_not_polled(hass, mock_openzwave): - """Test should_poll property.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - assert not entity.should_poll - - -async def test_unique_id(hass, mock_openzwave): - """Test unique_id.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - assert entity.unique_id == "node-567" - - -async def test_unique_id_missing_data(hass, mock_openzwave): - """Test unique_id.""" - zwave_network = MagicMock() - node = mock_zwave.MockNode() - node.manufacturer_name = None - node.name = None - node.is_ready = False - entity = node_entity.ZWaveNodeEntity(node, zwave_network) - - assert entity.unique_id is None diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py deleted file mode 100644 index 21944fe8f7e..00000000000 --- a/tests/components/zwave/test_sensor.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Test Z-Wave sensor.""" -import pytest - -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.zwave import const, sensor -import homeassistant.const - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_detects_none(mock_openzwave): - """Test get_device returns None.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - assert device is None - - -def test_get_device_detects_alarmsensor(mock_openzwave): - """Test get_device returns a Z-Wave alarmsensor.""" - node = MockNode( - command_classes=[const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM] - ) - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, sensor.ZWaveAlarmSensor) - - -def test_get_device_detects_multilevelsensor(mock_openzwave): - """Test get_device returns a Z-Wave multilevel sensor.""" - node = MockNode( - command_classes=[ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - ] - ) - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, sensor.ZWaveMultilevelSensor) - assert device.force_update - - -def test_get_device_detects_multilevel_meter(mock_openzwave): - """Test get_device returns a Z-Wave multilevel sensor.""" - node = MockNode(command_classes=[const.COMMAND_CLASS_METER]) - value = MockValue(data=0, node=node, type=const.TYPE_DECIMAL) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, sensor.ZWaveMultilevelSensor) - - -def test_get_device_detects_battery_sensor(mock_openzwave): - """Test get_device returns a Z-Wave battery sensor.""" - - node = MockNode(command_classes=[const.COMMAND_CLASS_BATTERY]) - value = MockValue( - data=0, - node=node, - type=const.TYPE_DECIMAL, - command_class=const.COMMAND_CLASS_BATTERY, - ) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - assert isinstance(device, sensor.ZWaveBatterySensor) - assert device.device_class is SensorDeviceClass.BATTERY - - -def test_multilevelsensor_value_changed_temp_fahrenheit(hass, mock_openzwave): - """Test value changed for Z-Wave multilevel sensor for temperature.""" - hass.config.units.temperature_unit = homeassistant.const.TEMP_FAHRENHEIT - - node = MockNode( - command_classes=[ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - ] - ) - value = MockValue(data=190.95555, units="F", node=node) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - device.hass = hass - assert device.state == 191.0 - assert device.unit_of_measurement == homeassistant.const.TEMP_FAHRENHEIT - assert device.device_class is SensorDeviceClass.TEMPERATURE - value.data = 197.95555 - value_changed(value) - assert device.state == 198.0 - - -def test_multilevelsensor_value_changed_temp_celsius(hass, mock_openzwave): - """Test value changed for Z-Wave multilevel sensor for temperature.""" - hass.config.units.temperature_unit = homeassistant.const.TEMP_CELSIUS - node = MockNode( - command_classes=[ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - ] - ) - value = MockValue(data=38.85555, units="C", node=node) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - device.hass = hass - assert device.state == 38.9 - assert device.unit_of_measurement == homeassistant.const.TEMP_CELSIUS - assert device.device_class is SensorDeviceClass.TEMPERATURE - value.data = 37.95555 - value_changed(value) - assert device.state == 38.0 - - -def test_multilevelsensor_value_changed_other_units(hass, mock_openzwave): - """Test value changed for Z-Wave multilevel sensor for other units.""" - node = MockNode( - command_classes=[ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - ] - ) - value = MockValue( - data=190.95555, units=homeassistant.const.ENERGY_KILO_WATT_HOUR, node=node - ) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - device.hass = hass - assert device.state == 190.96 - assert device.unit_of_measurement == homeassistant.const.ENERGY_KILO_WATT_HOUR - assert device.device_class is None - value.data = 197.95555 - value_changed(value) - assert device.state == 197.96 - - -def test_multilevelsensor_value_changed_integer(hass, mock_openzwave): - """Test value changed for Z-Wave multilevel sensor for other units.""" - node = MockNode( - command_classes=[ - const.COMMAND_CLASS_SENSOR_MULTILEVEL, - const.COMMAND_CLASS_METER, - ] - ) - value = MockValue(data=5, units="counts", node=node) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - device.hass = hass - assert device.state == 5 - assert device.unit_of_measurement == "counts" - assert device.device_class is None - value.data = 6 - value_changed(value) - assert device.state == 6 - - -def test_alarm_sensor_value_changed(hass, mock_openzwave): - """Test value changed for Z-Wave sensor.""" - node = MockNode( - command_classes=[const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM] - ) - value = MockValue(data=12.34, node=node, units=homeassistant.const.PERCENTAGE) - values = MockEntityValues(primary=value) - - device = sensor.get_device(node=node, values=values, node_config={}) - device.hass = hass - assert device.state == 12.34 - assert device.unit_of_measurement == homeassistant.const.PERCENTAGE - assert device.device_class is None - value.data = 45.67 - value_changed(value) - assert device.state == 45.67 diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py deleted file mode 100644 index 4c3efbe61fd..00000000000 --- a/tests/components/zwave/test_switch.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Test Z-Wave switches.""" -from unittest.mock import patch - -import pytest - -from homeassistant.components.zwave import switch - -from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_detects_switch(mock_openzwave): - """Test get_device returns a Z-Wave switch.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - - device = switch.get_device(node=node, values=values, node_config={}) - assert isinstance(device, switch.ZwaveSwitch) - - -def test_switch_turn_on_and_off(mock_openzwave): - """Test turning on a Z-Wave switch.""" - node = MockNode() - value = MockValue(data=0, node=node) - values = MockEntityValues(primary=value) - device = switch.get_device(node=node, values=values, node_config={}) - - device.turn_on() - - assert node.set_switch.called - value_id, state = node.set_switch.mock_calls[0][1] - assert value_id == value.value_id - assert state is True - node.reset_mock() - - device.turn_off() - - assert node.set_switch.called - value_id, state = node.set_switch.mock_calls[0][1] - assert value_id == value.value_id - assert state is False - - -def test_switch_value_changed(mock_openzwave): - """Test value changed for Z-Wave switch.""" - node = MockNode() - value = MockValue(data=False, node=node) - values = MockEntityValues(primary=value) - device = switch.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - value.data = True - value_changed(value) - - assert device.is_on - - -@patch("time.perf_counter") -def test_switch_refresh_on_update(mock_counter, mock_openzwave): - """Test value changed for refresh on update Z-Wave switch.""" - mock_counter.return_value = 10 - node = MockNode(manufacturer_id="013c", product_type="0001", product_id="0005") - value = MockValue(data=False, node=node, instance=1) - values = MockEntityValues(primary=value) - device = switch.get_device(node=node, values=values, node_config={}) - - assert not device.is_on - - mock_counter.return_value = 15 - value.data = True - value_changed(value) - - assert device.is_on - assert not node.request_state.called - - mock_counter.return_value = 45 - value.data = False - value_changed(value) - - assert not device.is_on - assert node.request_state.called diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py deleted file mode 100644 index 2ffe5d61715..00000000000 --- a/tests/components/zwave/test_websocket_api.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Test Z-Wave Websocket API.""" -from unittest.mock import call, patch - -import pytest - -from homeassistant import config_entries -from homeassistant.bootstrap import async_setup_component -from homeassistant.components.zwave.const import ( - CONF_AUTOHEAL, - CONF_NETWORK_KEY, - CONF_POLLING_INTERVAL, - CONF_USB_STICK_PATH, -) -from homeassistant.components.zwave.websocket_api import ID, TYPE - -NETWORK_KEY = "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" - - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): - """Test Z-Wave websocket API.""" - - await async_setup_component( - hass, - "zwave", - { - "zwave": { - CONF_AUTOHEAL: False, - CONF_USB_STICK_PATH: "/dev/zwave", - CONF_POLLING_INTERVAL: 6000, - CONF_NETWORK_KEY: NETWORK_KEY, - } - }, - ) - - await hass.async_block_till_done() - - client = await hass_ws_client(hass) - - await client.send_json({ID: 5, TYPE: "zwave/get_config"}) - - msg = await client.receive_json() - result = msg["result"] - - assert result[CONF_USB_STICK_PATH] == "/dev/zwave" - assert not result[CONF_AUTOHEAL] - assert result[CONF_POLLING_INTERVAL] == 6000 - - -async def test_zwave_zwave_js_migration_api(hass, mock_openzwave, hass_ws_client): - """Test Z-Wave to Z-Wave JS websocket migration API.""" - - await async_setup_component( - hass, - "zwave", - { - "zwave": { - CONF_AUTOHEAL: False, - CONF_USB_STICK_PATH: "/dev/zwave", - CONF_POLLING_INTERVAL: 6000, - CONF_NETWORK_KEY: NETWORK_KEY, - } - }, - ) - - await hass.async_block_till_done() - - client = await hass_ws_client(hass) - - await client.send_json({ID: 6, TYPE: "zwave/get_migration_config"}) - msg = await client.receive_json() - result = msg["result"] - - assert result[CONF_USB_STICK_PATH] == "/dev/zwave" - assert result[CONF_NETWORK_KEY] == NETWORK_KEY - - with patch( - "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" - ) as async_init: - - async_init.return_value = {"flow_id": "mock_flow_id"} - await client.send_json({ID: 7, TYPE: "zwave/start_zwave_js_config_flow"}) - msg = await client.receive_json() - - result = msg["result"] - - assert result["flow_id"] == "mock_flow_id" - assert async_init.call_args == call( - "zwave_js", - context={"source": config_entries.SOURCE_IMPORT}, - data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY}, - ) diff --git a/tests/components/zwave/test_workaround.py b/tests/components/zwave/test_workaround.py deleted file mode 100644 index 8f84fd6b949..00000000000 --- a/tests/components/zwave/test_workaround.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Test Z-Wave workarounds.""" -import pytest - -from homeassistant.components.zwave import const, workaround - -from tests.mock.zwave import MockNode, MockValue - -# Integration is disabled -pytest.skip("Integration has been disabled in the manifest", allow_module_level=True) - - -def test_get_device_no_component_mapping(): - """Test that None is returned.""" - node = MockNode(manufacturer_id=" ") - value = MockValue(data=0, node=node) - assert workaround.get_device_component_mapping(value) is None - - -def test_get_device_component_mapping(): - """Test that component is returned.""" - node = MockNode(manufacturer_id="010f", product_type="0b00") - value = MockValue(data=0, node=node, command_class=const.COMMAND_CLASS_SENSOR_ALARM) - assert workaround.get_device_component_mapping(value) == "binary_sensor" - - -def test_get_device_component_mapping_mti(): - """Test that component is returned.""" - # GE Fan controller - node = MockNode(manufacturer_id="0063", product_type="4944", product_id="3034") - value = MockValue( - data=0, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - assert workaround.get_device_component_mapping(value) == "fan" - - # GE Dimmer - node = MockNode(manufacturer_id="0063", product_type="4944", product_id="3031") - value = MockValue( - data=0, node=node, command_class=const.COMMAND_CLASS_SWITCH_MULTILEVEL - ) - assert workaround.get_device_component_mapping(value) is None - - -def test_get_device_no_mapping(): - """Test that no device mapping is returned.""" - node = MockNode(manufacturer_id=" ") - value = MockValue(data=0, node=node) - assert workaround.get_device_mapping(value) is None - - -def test_get_device_mapping_mt(): - """Test that device mapping mt is returned.""" - node = MockNode(manufacturer_id="0047", product_type="5a52") - value = MockValue(data=0, node=node) - assert workaround.get_device_mapping(value) == "workaround_no_position" - - -def test_get_device_mapping_mtii(): - """Test that device mapping mtii is returned.""" - node = MockNode(manufacturer_id="013c", product_type="0002", product_id="0002") - value = MockValue(data=0, node=node, index=0) - assert workaround.get_device_mapping(value) == "trigger_no_off_event" - - -def test_get_device_mapping_mti_instance(): - """Test that device mapping mti_instance is returned.""" - node = MockNode(manufacturer_id="013c", product_type="0001", product_id="0005") - value = MockValue(data=0, node=node, instance=1) - assert workaround.get_device_mapping(value) == "refresh_node_on_update" - - value = MockValue(data=0, node=node, instance=2) - assert workaround.get_device_mapping(value) is None diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index 95f969a9586..79201ebbede 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -217,6 +217,7 @@ def zwave_integration_fixture(hass, zwave_migration_data): yield zwave_config_entry +@pytest.mark.skip(reason="The old zwave integration has been removed.") async def test_migrate_zwave( hass, zwave_integration, @@ -353,7 +354,7 @@ async def test_migrate_zwave( assert not await hass.config_entries.async_setup(zwave_config_entry.entry_id) -@pytest.mark.skip(reason="The old zwave integration has been disabled.") +@pytest.mark.skip(reason="The old zwave integration has been removed.") async def test_migrate_zwave_dry_run( hass, zwave_integration, diff --git a/tests/mock/__init__.py b/tests/mock/__init__.py deleted file mode 100644 index acf1fe50f54..00000000000 --- a/tests/mock/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Mock helpers.""" diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py deleted file mode 100644 index 89c70eaf83c..00000000000 --- a/tests/mock/zwave.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Mock helpers for Z-Wave component.""" -from unittest.mock import MagicMock - -# Integration & integration tests are disabled -# from pydispatch import dispatcher -dispatcher = MagicMock() - - -def value_changed(value): - """Fire a value changed.""" - dispatcher.send( - MockNetwork.SIGNAL_VALUE_CHANGED, - value=value, - node=value.node, - network=value.node._network, - ) - - -def node_changed(node): - """Fire a node changed.""" - dispatcher.send(MockNetwork.SIGNAL_NODE, node=node, network=node._network) - - -def notification(node_id, network=None): - """Fire a notification.""" - dispatcher.send( - MockNetwork.SIGNAL_NOTIFICATION, args={"nodeId": node_id}, network=network - ) - - -class MockOption(MagicMock): - """Mock Z-Wave options.""" - - def __init__(self, device=None, config_path=None, user_path=None, cmd_line=None): - """Initialize a Z-Wave mock options.""" - super().__init__() - self.device = device - self.config_path = config_path - self.user_path = user_path - self.cmd_line = cmd_line - - def _get_child_mock(self, **kw): - """Create child mocks with right MagicMock class.""" - return MagicMock(**kw) - - -class MockNetwork(MagicMock): - """Mock Z-Wave network.""" - - SIGNAL_NETWORK_FAILED = "mock_NetworkFailed" - SIGNAL_NETWORK_STARTED = "mock_NetworkStarted" - SIGNAL_NETWORK_READY = "mock_NetworkReady" - SIGNAL_NETWORK_STOPPED = "mock_NetworkStopped" - SIGNAL_NETWORK_RESETTED = "mock_DriverResetted" - SIGNAL_NETWORK_AWAKED = "mock_DriverAwaked" - SIGNAL_DRIVER_FAILED = "mock_DriverFailed" - SIGNAL_DRIVER_READY = "mock_DriverReady" - SIGNAL_DRIVER_RESET = "mock_DriverReset" - SIGNAL_DRIVER_REMOVED = "mock_DriverRemoved" - SIGNAL_GROUP = "mock_Group" - SIGNAL_NODE = "mock_Node" - SIGNAL_NODE_ADDED = "mock_NodeAdded" - SIGNAL_NODE_EVENT = "mock_NodeEvent" - SIGNAL_NODE_NAMING = "mock_NodeNaming" - SIGNAL_NODE_NEW = "mock_NodeNew" - SIGNAL_NODE_PROTOCOL_INFO = "mock_NodeProtocolInfo" - SIGNAL_NODE_READY = "mock_NodeReady" - SIGNAL_NODE_REMOVED = "mock_NodeRemoved" - SIGNAL_SCENE_EVENT = "mock_SceneEvent" - SIGNAL_VALUE = "mock_Value" - SIGNAL_VALUE_ADDED = "mock_ValueAdded" - SIGNAL_VALUE_CHANGED = "mock_ValueChanged" - SIGNAL_VALUE_REFRESHED = "mock_ValueRefreshed" - SIGNAL_VALUE_REMOVED = "mock_ValueRemoved" - SIGNAL_POLLING_ENABLED = "mock_PollingEnabled" - SIGNAL_POLLING_DISABLED = "mock_PollingDisabled" - SIGNAL_CREATE_BUTTON = "mock_CreateButton" - SIGNAL_DELETE_BUTTON = "mock_DeleteButton" - SIGNAL_BUTTON_ON = "mock_ButtonOn" - SIGNAL_BUTTON_OFF = "mock_ButtonOff" - SIGNAL_ESSENTIAL_NODE_QUERIES_COMPLETE = "mock_EssentialNodeQueriesComplete" - SIGNAL_NODE_QUERIES_COMPLETE = "mock_NodeQueriesComplete" - SIGNAL_AWAKE_NODES_QUERIED = "mock_AwakeNodesQueried" - SIGNAL_ALL_NODES_QUERIED = "mock_AllNodesQueried" - SIGNAL_ALL_NODES_QUERIED_SOME_DEAD = "mock_AllNodesQueriedSomeDead" - SIGNAL_MSG_COMPLETE = "mock_MsgComplete" - SIGNAL_NOTIFICATION = "mock_Notification" - SIGNAL_CONTROLLER_COMMAND = "mock_ControllerCommand" - SIGNAL_CONTROLLER_WAITING = "mock_ControllerWaiting" - - STATE_STOPPED = 0 - STATE_FAILED = 1 - STATE_RESETTED = 3 - STATE_STARTED = 5 - STATE_AWAKED = 7 - STATE_READY = 10 - - def __init__(self, options=None, *args, **kwargs): - """Initialize a Z-Wave mock network.""" - super().__init__() - self.options = options - self.state = MockNetwork.STATE_STOPPED - - -class MockNode(MagicMock): - """Mock Z-Wave node.""" - - def __init__( - self, - *, - node_id=567, - name="Mock Node", - manufacturer_id="ABCD", - product_id="123", - product_type="678", - command_classes=None, - can_wake_up_value=True, - manufacturer_name="Test Manufacturer", - product_name="Test Product", - network=None, - **kwargs, - ): - """Initialize a Z-Wave mock node.""" - super().__init__() - self.node_id = node_id - self.name = name - self.manufacturer_id = manufacturer_id - self.product_id = product_id - self.product_type = product_type - self.manufacturer_name = manufacturer_name - self.product_name = product_name - self.can_wake_up_value = can_wake_up_value - self._command_classes = command_classes or [] - if network is not None: - self._network = network - for attr_name in kwargs: - setattr(self, attr_name, kwargs[attr_name]) - - def has_command_class(self, command_class): - """Test if mock has a command class.""" - return command_class in self._command_classes - - def get_battery_level(self): - """Return mock battery level.""" - return 42 - - def can_wake_up(self): - """Return whether the node can wake up.""" - return self.can_wake_up_value - - def _get_child_mock(self, **kw): - """Create child mocks with right MagicMock class.""" - return MagicMock(**kw) - - -class MockValue(MagicMock): - """Mock Z-Wave value.""" - - _mock_value_id = 1234 - - def __init__( - self, - *, - label="Mock Value", - node=None, - instance=0, - index=0, - value_id=None, - **kwargs, - ): - """Initialize a Z-Wave mock value.""" - super().__init__() - self.label = label - self.node = node - self.instance = instance - self.index = index - if value_id is None: - MockValue._mock_value_id += 1 - value_id = MockValue._mock_value_id - self.value_id = value_id - self.object_id = value_id - for attr_name in kwargs: - setattr(self, attr_name, kwargs[attr_name]) - - def _get_child_mock(self, **kw): - """Create child mocks with right MagicMock class.""" - return MagicMock(**kw) - - def refresh(self): - """Mock refresh of node value.""" - value_changed(self) - - -class MockEntityValues: - """Mock Z-Wave entity values.""" - - def __init__(self, **kwargs): - """Initialize the mock zwave values.""" - self.primary = None - self.wakeup = None - self.battery = None - self.power = None - for name in kwargs: - setattr(self, name, kwargs[name]) - - def __iter__(self): - """Allow iteration over all values.""" - return iter(self.__dict__.values()) From 41a032e3e35001a01fc610d72c611ae9b877912a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 18 Mar 2022 07:40:09 -0700 Subject: [PATCH 0523/1054] Add diagnostics to stream's Stream objects (#68020) * Add diagnostics to stream's Stream objects Add diagnostics key/value pair to the Stream object. Diagnostics support in camera integration will be added in a follow up and will access the diagnostics on the Stream object, similar to the examples in the unit test. * Rename to audio/video codec * Fix test codec names * Update tests/components/stream/test_worker.py Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> Co-authored-by: Paulus Schoutsen Co-authored-by: uvjustin <46082645+uvjustin@users.noreply.github.com> --- homeassistant/components/stream/__init__.py | 16 +++++++-- .../components/stream/diagnostics.py | 33 +++++++++++++++++++ homeassistant/components/stream/worker.py | 12 +++++++ tests/components/stream/test_hls.py | 8 +++++ tests/components/stream/test_worker.py | 10 +++++- 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/stream/diagnostics.py diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 157f20b5b37..9ec389fb4a8 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,7 +23,7 @@ import secrets import threading import time from types import MappingProxyType -from typing import cast +from typing import Any, cast import voluptuous as vol @@ -51,6 +51,7 @@ from .const import ( TARGET_SEGMENT_DURATION_NON_LL_HLS, ) from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings +from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls _LOGGER = logging.getLogger(__name__) @@ -225,6 +226,7 @@ class Stream: if stream_label else _LOGGER ) + self._diagnostics = Diagnostics() def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" @@ -259,6 +261,7 @@ class Stream: self.hass, IdleTimer(self.hass, timeout, idle_callback) ) self._outputs[fmt] = provider + return provider def remove_provider(self, provider: StreamOutput) -> None: @@ -310,6 +313,7 @@ class Stream: def update_source(self, new_source: str) -> None: """Restart the stream with a new stream source.""" + self._diagnostics.increment("update_source") self._logger.debug( "Updating stream source %s", redact_credentials(str(new_source)) ) @@ -323,11 +327,13 @@ class Stream: # pylint: disable=import-outside-toplevel from .worker import StreamState, StreamWorkerError, stream_worker - stream_state = StreamState(self.hass, self.outputs) + stream_state = StreamState(self.hass, self.outputs, self._diagnostics) wait_timeout = 0 while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() self.hass.add_job(self._async_update_state, True) + self._diagnostics.set_value("keepalive", self.keepalive) + self._diagnostics.increment("start_worker") try: stream_worker( self.source, @@ -337,6 +343,7 @@ class Stream: self._thread_quit, ) except StreamWorkerError as err: + self._diagnostics.increment("worker_error") self._logger.error("Error from stream worker: %s", str(err)) stream_state.discontinuity() @@ -358,6 +365,7 @@ class Stream: if time.time() - start_time > STREAM_RESTART_RESET_TIME: wait_timeout = 0 wait_timeout += STREAM_RESTART_INCREMENT + self._diagnostics.set_value("retry_timeout", wait_timeout) self._logger.debug( "Restarting stream worker in %d seconds: %s", wait_timeout, @@ -447,6 +455,10 @@ class Stream: width=width, height=height ) + def get_diagnostics(self) -> dict[str, Any]: + """Return diagnostics information for the stream.""" + return self._diagnostics.as_dict() + def _should_retry() -> bool: """Return true if worker failures should be retried, for disabling during tests.""" diff --git a/homeassistant/components/stream/diagnostics.py b/homeassistant/components/stream/diagnostics.py new file mode 100644 index 00000000000..47370eeb5f9 --- /dev/null +++ b/homeassistant/components/stream/diagnostics.py @@ -0,0 +1,33 @@ +"""Diagnostics for debugging. + +The stream component does not have config entries itself, and all diagnostics +information is managed by dependent components (e.g. camera) +""" + +from __future__ import annotations + +from collections import Counter +from typing import Any + + +class Diagnostics: + """Holds diagnostics counters and key/values.""" + + def __init__(self) -> None: + """Initialize Diagnostics.""" + self._counter: Counter = Counter() + self._values: dict[str, Any] = {} + + def increment(self, key: str) -> None: + """Increment a counter for the spcified key/event.""" + self._counter.update(Counter({key: 1})) + + def set_value(self, key: str, value: Any) -> None: + """Update a key/value pair.""" + self._values[key] = value + + def as_dict(self) -> dict[str, Any]: + """Return diagnostics as a debug dictionary.""" + result = {k: self._counter[k] for k in self._counter} + result.update(self._values) + return result diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 42a34cf4e8c..bde5ab0fb05 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -27,6 +27,7 @@ from .const import ( SOURCE_TIMEOUT, ) from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings +from .diagnostics import Diagnostics from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -52,6 +53,7 @@ class StreamState: self, hass: HomeAssistant, outputs_callback: Callable[[], Mapping[str, StreamOutput]], + diagnostics: Diagnostics, ) -> None: """Initialize StreamState.""" self._stream_id: int = 0 @@ -62,6 +64,7 @@ class StreamState: # sequence gets incremented before the first segment so the first segment # has a sequence number of 0. self._sequence = -1 + self._diagnostics = diagnostics @property def sequence(self) -> int: @@ -93,6 +96,11 @@ class StreamState: """Return the active stream outputs.""" return list(self._outputs_callback().values()) + @property + def diagnostics(self) -> Diagnostics: + """Return diagnostics object.""" + return self._diagnostics + class StreamMuxer: """StreamMuxer re-packages video/audio packets for output.""" @@ -468,6 +476,10 @@ def stream_worker( # Some audio streams do not have a profile and throw errors when remuxing if audio_stream and audio_stream.profile is None: audio_stream = None + stream_state.diagnostics.set_value("container_format", container.format.name) + stream_state.diagnostics.set_value("video_codec", video_stream.name) + if audio_stream: + stream_state.diagnostics.set_value("audio_codec", audio_stream.name) dts_validator = TimestampValidator() container_packets = PeekIterator( diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index a2bb328826d..8e01c55de84 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -177,6 +177,14 @@ async def test_hls_stream( fail_response = await hls_client.get() assert fail_response.status == HTTPStatus.NOT_FOUND + assert stream.get_diagnostics() == { + "container_format": "mov,mp4,m4a,3gp,3g2,mj2", + "keepalive": False, + "start_worker": 1, + "video_codec": "h264", + "worker_error": 1, + } + async def test_stream_timeout( hass, hass_client, setup_component, stream_worker_sync, h264_video diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index e5477c66f52..2a44dd64455 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -270,7 +270,7 @@ class MockPyAv: def run_worker(hass, stream, stream_source): """Run the stream worker under test.""" - stream_state = StreamState(hass, stream.outputs) + stream_state = StreamState(hass, stream.outputs, stream._diagnostics) stream_worker( stream_source, {}, stream_state, KeyFrameConverter(hass), threading.Event() ) @@ -873,6 +873,14 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): stream.stop() + assert stream.get_diagnostics() == { + "container_format": "mov,mp4,m4a,3gp,3g2,mj2", + "keepalive": False, + "start_worker": 1, + "video_codec": "hevc", + "worker_error": 1, + } + async def test_get_image(hass, record_worker_sync): """Test that the has_keyframe metadata matches the media.""" From 9864090e0b414b0c610cd0c33c6a49f81940bb9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 04:44:37 -1000 Subject: [PATCH 0524/1054] Cache parsing attr in LazyState (#68232) --- homeassistant/components/recorder/history.py | 12 ++++++++---- homeassistant/components/recorder/models.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 13574bc654f..c4d15863a5b 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -321,7 +321,9 @@ def _get_states_with_session( query = query.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) - return [LazyState(row) for row in execute(query)] + + attr_cache = {} + return [LazyState(row, attr_cache) for row in execute(query)] def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): @@ -397,15 +399,17 @@ def _sorted_states_to_dict( for ent_id, group in groupby(states, lambda state: state.entity_id): domain = split_entity_id(ent_id)[0] ent_results = result[ent_id] + attr_cache = {} + if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend(LazyState(db_state) for db_state in group) + ent_results.extend(LazyState(db_state, attr_cache) for db_state in group) # With minimal response we only provide a native # State for the first and last response. All the states # in-between only provide the "state" and the # "last_changed". if not ent_results: - ent_results.append(LazyState(next(group))) + ent_results.append(LazyState(next(group), attr_cache)) prev_state = ent_results[-1] initial_state_count = len(ent_results) @@ -430,7 +434,7 @@ def _sorted_states_to_dict( # There was at least one state change # replace the last minimal state with # a full state - ent_results[-1] = LazyState(prev_state) + ent_results[-1] = LazyState(prev_state, attr_cache) # Filter out the empty lists if some states had 0 results. return {key: val for key, val in result.items() if val} diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 329af989568..ab874081055 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -533,9 +533,10 @@ class LazyState(State): "_last_changed", "_last_updated", "_context", + "_attr_cache", ] - def __init__(self, row): # pylint: disable=super-init-not-called + def __init__(self, row, attr_cache=None): # pylint: disable=super-init-not-called """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id @@ -544,12 +545,18 @@ class LazyState(State): self._last_changed = None self._last_updated = None self._context = None + self._attr_cache = attr_cache @property # type: ignore[override] def attributes(self): """State attributes.""" if self._attributes is None: source = self._row.shared_attrs or self._row.attributes + if self._attr_cache is not None and ( + attributes := self._attr_cache.get(source) + ): + self._attributes = attributes + return attributes if source == EMPTY_JSON_OBJECT or source is None: self._attributes = {} return self._attributes @@ -561,6 +568,8 @@ class LazyState(State): "Error converting row to state attributes: %s", self._row ) self._attributes = {} + if self._attr_cache is not None: + self._attr_cache[source] = self._attributes return self._attributes @attributes.setter From 7174e7897c7a1a5182d06d02f5291b46577380bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Mar 2022 16:22:32 +0100 Subject: [PATCH 0525/1054] Update mypy to 0.941 (#68305) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9e16676092a..2c4016e9b12 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.3.2 freezegun==1.2.0 mock-open==1.4.0 -mypy==0.940 +mypy==0.941 pre-commit==2.17.0 pylint==2.12.2 pipdeptree==2.2.1 From dbb79e2937381dc4284f8ab1cc3c0f720ba762b6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 18 Mar 2022 12:12:10 -0500 Subject: [PATCH 0526/1054] Add support for Sonos subwoofer gain controls (#68334) --- homeassistant/components/sonos/number.py | 1 + homeassistant/components/sonos/speaker.py | 3 ++- tests/components/sonos/conftest.py | 1 + tests/components/sonos/test_number.py | 17 +++++++++++++++-- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index c9b8ec47583..b0a10690ce6 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -19,6 +19,7 @@ LEVEL_TYPES = { "audio_delay": (0, 5), "bass": (-10, 10), "treble": (-10, 10), + "sub_gain": (-15, 15), } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 3a2bac51684..98c6d3bba26 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -150,6 +150,7 @@ class SonosSpeaker: self.dialog_level: bool | None = None self.night_mode: bool | None = None self.sub_enabled: bool | None = None + self.sub_gain: int | None = None self.surround_enabled: bool | None = None # Misc features @@ -490,7 +491,7 @@ class SonosSpeaker: if bool_var in variables: setattr(self, bool_var, variables[bool_var] == "1") - for int_var in ("audio_delay", "bass", "treble"): + for int_var in ("audio_delay", "bass", "treble", "sub_gain"): if int_var in variables: setattr(self, int_var, variables[int_var]) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 8e133f76ac1..ebff338b39a 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -114,6 +114,7 @@ def soco_fixture( mock_soco.treble = -1 mock_soco.mic_enabled = False mock_soco.sub_enabled = False + mock_soco.sub_gain = 5 mock_soco.surround_enabled = True mock_soco.soundbar_audio_input_format = "Dolby 5.1" mock_soco.get_battery_info.return_value = battery_info diff --git a/tests/components/sonos/test_number.py b/tests/components/sonos/test_number.py index 91c00e05390..5829a7a6724 100644 --- a/tests/components/sonos/test_number.py +++ b/tests/components/sonos/test_number.py @@ -6,8 +6,8 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers import entity_registry as ent_reg -async def test_audio_input_sensor(hass, async_autosetup_sonos, soco): - """Test audio input sensor.""" +async def test_number_entities(hass, async_autosetup_sonos, soco): + """Test number entities.""" entity_registry = ent_reg.async_get(hass) bass_number = entity_registry.entities["number.zone_a_bass"] @@ -30,3 +30,16 @@ async def test_audio_input_sensor(hass, async_autosetup_sonos, soco): blocking=True, ) assert mock_audio_delay.called_with(3) + + sub_gain_number = entity_registry.entities["number.zone_a_sub_gain"] + sub_gain_state = hass.states.get(sub_gain_number.entity_id) + assert sub_gain_state.state == "5" + + with patch("soco.SoCo.sub_gain") as mock_sub_gain: + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: sub_gain_number.entity_id, "value": -8}, + blocking=True, + ) + assert mock_sub_gain.called_with(-8) From ffcc02e93dd210b86090d135b157f7b81982a583 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 Mar 2022 19:06:44 +0100 Subject: [PATCH 0527/1054] Add zha typing [api] (2) (#68335) --- homeassistant/components/zha/api.py | 210 +++++++++++++++------------- 1 file changed, 112 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index feebff87c8b..ac028597ea8 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -69,11 +69,13 @@ from .core.helpers import ( get_matched_clusters, qr_to_install_code, ) -from .core.typing import ZhaDeviceType, ZhaGatewayType +from .core.typing import ZhaDeviceType if TYPE_CHECKING: from homeassistant.components.websocket_api.connection import ActiveConnection + from .core.gateway import ZHAGateway + _LOGGER = logging.getLogger(__name__) TYPE = "type" @@ -210,9 +212,9 @@ async def websocket_permit_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Permit ZHA zigbee devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - duration = msg.get(ATTR_DURATION) - ieee = msg.get(ATTR_IEEE) + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + duration: int = msg[ATTR_DURATION] + ieee: EUI64 | None = msg.get(ATTR_IEEE) async def forward_messages(data): """Forward events to websocket.""" @@ -230,6 +232,8 @@ async def websocket_permit_devices( connection.subscriptions[msg["id"]] = async_cleanup zha_gateway.async_enable_debug_mode() + src_ieee: EUI64 + code: bytes if ATTR_SOURCE_IEEE in msg: src_ieee = msg[ATTR_SOURCE_IEEE] code = msg[ATTR_INSTALL_CODE] @@ -255,10 +259,8 @@ async def websocket_get_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] devices = [device.zha_device_info for device in zha_gateway.devices.values()] - connection.send_result(msg[ID], devices) @@ -269,7 +271,7 @@ async def websocket_get_groupable_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA devices that can be grouped.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] devices = [device for device in zha_gateway.devices.values() if device.is_groupable] groupable_devices = [] @@ -309,7 +311,7 @@ async def websocket_get_groups( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA groups.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] groups = [group.group_info for group in zha_gateway.groups.values()] connection.send_result(msg[ID], groups) @@ -326,8 +328,8 @@ async def websocket_get_device( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] device = None if ieee in zha_gateway.devices: device = zha_gateway.devices[ieee].zha_device_info @@ -353,8 +355,8 @@ async def websocket_get_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_id = msg[GROUP_ID] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_id: int = msg[GROUP_ID] group = None if group_id in zha_gateway.groups: @@ -397,10 +399,10 @@ async def websocket_add_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Add a new ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_name = msg[GROUP_NAME] - members = msg.get(ATTR_MEMBERS) - group_id = msg.get(GROUP_ID) + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_name: str = msg[GROUP_NAME] + group_id: int | None = msg.get(GROUP_ID) + members: list[GroupMember] | None = msg.get(ATTR_MEMBERS) group = await zha_gateway.async_create_zigpy_group(group_name, members, group_id) connection.send_result(msg[ID], group.group_info) @@ -417,8 +419,8 @@ async def websocket_remove_groups( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Remove the specified ZHA groups.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_ids = msg[GROUP_IDS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_ids: list[int] = msg[GROUP_IDS] if len(group_ids) > 1: tasks = [] @@ -444,9 +446,9 @@ async def websocket_add_group_members( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Add members to a ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_id = msg[GROUP_ID] - members = msg[ATTR_MEMBERS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_id: int = msg[GROUP_ID] + members: list[GroupMember] = msg[ATTR_MEMBERS] zha_group = None if group_id in zha_gateway.groups: @@ -476,9 +478,9 @@ async def websocket_remove_group_members( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Remove members from a ZHA group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - group_id = msg[GROUP_ID] - members = msg[ATTR_MEMBERS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + group_id: int = msg[GROUP_ID] + members: list[GroupMember] = msg[ATTR_MEMBERS] zha_group = None if group_id in zha_gateway.groups: @@ -507,8 +509,8 @@ async def websocket_reconfigure_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Reconfigure a ZHA nodes entities by its ieee address.""" - zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] device: ZhaDeviceType = zha_gateway.get_device(ieee) async def forward_messages(data): @@ -541,21 +543,24 @@ async def websocket_update_topology( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Update the ZHA network topology.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] hass.async_create_task(zha_gateway.application_controller.topology.scan()) @websocket_api.require_admin @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert} + { + vol.Required(TYPE): "zha/devices/clusters", + vol.Required(ATTR_IEEE): EUI64.convert, + } ) @websocket_api.async_response async def websocket_device_clusters( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Return a list of device clusters.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] zha_device = zha_gateway.get_device(ieee) response_clusters = [] if zha_device is not None: @@ -598,12 +603,12 @@ async def websocket_device_cluster_attributes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Return a list of cluster attributes.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] - endpoint_id = msg[ATTR_ENDPOINT_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - cluster_attributes = [] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + endpoint_id: int = msg[ATTR_ENDPOINT_ID] + cluster_id: int = msg[ATTR_CLUSTER_ID] + cluster_type: str = msg[ATTR_CLUSTER_TYPE] + cluster_attributes: list[dict[str, Any]] = [] zha_device = zha_gateway.get_device(ieee) attributes = None if zha_device is not None: @@ -645,11 +650,11 @@ async def websocket_device_cluster_commands( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Return a list of cluster commands.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - ieee = msg[ATTR_IEEE] - endpoint_id = msg[ATTR_ENDPOINT_ID] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + endpoint_id: int = msg[ATTR_ENDPOINT_ID] + cluster_id: int = msg[ATTR_CLUSTER_ID] + cluster_type: str = msg[ATTR_CLUSTER_TYPE] zha_device = zha_gateway.get_device(ieee) cluster_commands = [] commands = None @@ -707,13 +712,13 @@ async def websocket_read_zigbee_cluster_attributes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Read zigbee attribute for cluster on zha entity.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee = msg[ATTR_IEEE] - endpoint_id = msg[ATTR_ENDPOINT_ID] - cluster_id = msg[ATTR_CLUSTER_ID] - cluster_type = msg[ATTR_CLUSTER_TYPE] - attribute = msg[ATTR_ATTRIBUTE] - manufacturer = msg.get(ATTR_MANUFACTURER) or None + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = msg[ATTR_IEEE] + endpoint_id: int = msg[ATTR_ENDPOINT_ID] + cluster_id: int = msg[ATTR_CLUSTER_ID] + cluster_type: str = msg[ATTR_CLUSTER_TYPE] + attribute: int = msg[ATTR_ATTRIBUTE] + manufacturer: Any | None = msg.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: manufacturer = zha_device.manufacturer_code @@ -747,15 +752,18 @@ async def websocket_read_zigbee_cluster_attributes( @websocket_api.require_admin @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert} + { + vol.Required(TYPE): "zha/devices/bindable", + vol.Required(ATTR_IEEE): EUI64.convert, + } ) @websocket_api.async_response async def websocket_get_bindable_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Directly bind devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_IEEE] source_device = zha_gateway.get_device(source_ieee) devices = [ @@ -788,9 +796,9 @@ async def websocket_bind_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Directly bind devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - target_ieee = msg[ATTR_TARGET_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + target_ieee: EUI64 = msg[ATTR_TARGET_IEEE] await async_binding_operation( zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req ) @@ -816,9 +824,9 @@ async def websocket_unbind_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Remove a direct binding between devices.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - target_ieee = msg[ATTR_TARGET_IEEE] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + target_ieee: EUI64 = msg[ATTR_TARGET_IEEE] await async_binding_operation( zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req ) @@ -862,12 +870,11 @@ async def websocket_bind_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Directly bind a device to a group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - group_id = msg[GROUP_ID] - bindings = msg[BINDINGS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + group_id: int = msg[GROUP_ID] + bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) - await source_device.async_bind_to_group(group_id, bindings) @@ -885,15 +892,20 @@ async def websocket_unbind_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Unbind a device from a group.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - source_ieee = msg[ATTR_SOURCE_IEEE] - group_id = msg[GROUP_ID] - bindings = msg[BINDINGS] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE] + group_id: int = msg[GROUP_ID] + bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) await source_device.async_unbind_from_group(group_id, bindings) -async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation): +async def async_binding_operation( + zha_gateway: ZHAGateway, + source_ieee: EUI64, + target_ieee: EUI64, + operation: zdo_types.ZDOCmd, +) -> None: """Create or remove a direct zigbee binding between 2 devices.""" source_device = zha_gateway.get_device(source_ieee) @@ -982,7 +994,7 @@ async def websocket_update_zha_configuration( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Update the ZHA configuration.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] options = zha_gateway.config_entry.options data_to_save = {**options, **{CUSTOM_CONFIGURATION: msg["data"]}} @@ -1002,13 +1014,15 @@ async def websocket_update_zha_configuration( @callback def async_load_api(hass: HomeAssistant) -> None: """Set up the web socket API.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] application_controller = zha_gateway.application_controller async def permit(service: ServiceCall) -> None: """Allow devices to join this network.""" - duration = service.data[ATTR_DURATION] - ieee = service.data.get(ATTR_IEEE) + duration: int = service.data[ATTR_DURATION] + ieee: EUI64 | None = service.data.get(ATTR_IEEE) + src_ieee: EUI64 + code: bytes if ATTR_SOURCE_IEEE in service.data: src_ieee = service.data[ATTR_SOURCE_IEEE] code = service.data[ATTR_INSTALL_CODE] @@ -1038,8 +1052,8 @@ def async_load_api(hass: HomeAssistant) -> None: async def remove(service: ServiceCall) -> None: """Remove a node from the network.""" - ieee = service.data[ATTR_IEEE] - zha_gateway: ZhaGatewayType = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee: EUI64 = service.data[ATTR_IEEE] zha_device: ZhaDeviceType = zha_gateway.get_device(ieee) if zha_device is not None and ( zha_device.is_coordinator @@ -1056,13 +1070,13 @@ def async_load_api(hass: HomeAssistant) -> None: async def set_zigbee_cluster_attributes(service: ServiceCall) -> None: """Set zigbee attribute for cluster on zha entity.""" - ieee = service.data.get(ATTR_IEEE) - endpoint_id = service.data.get(ATTR_ENDPOINT_ID) - cluster_id = service.data.get(ATTR_CLUSTER_ID) - cluster_type = service.data.get(ATTR_CLUSTER_TYPE) - attribute = service.data.get(ATTR_ATTRIBUTE) - value = service.data.get(ATTR_VALUE) - manufacturer = service.data.get(ATTR_MANUFACTURER) or None + ieee: EUI64 = service.data[ATTR_IEEE] + endpoint_id: int = service.data[ATTR_ENDPOINT_ID] + cluster_id: int = service.data[ATTR_CLUSTER_ID] + cluster_type: str = service.data[ATTR_CLUSTER_TYPE] + attribute: int | str = service.data[ATTR_ATTRIBUTE] + value: int | bool | str = service.data[ATTR_VALUE] + manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: manufacturer = zha_device.manufacturer_code @@ -1104,14 +1118,14 @@ def async_load_api(hass: HomeAssistant) -> None: async def issue_zigbee_cluster_command(service: ServiceCall) -> None: """Issue command on zigbee cluster on zha entity.""" - ieee = service.data.get(ATTR_IEEE) - endpoint_id = service.data.get(ATTR_ENDPOINT_ID) - cluster_id = service.data.get(ATTR_CLUSTER_ID) - cluster_type = service.data.get(ATTR_CLUSTER_TYPE) - command = service.data.get(ATTR_COMMAND) - command_type = service.data.get(ATTR_COMMAND_TYPE) - args = service.data.get(ATTR_ARGS) - manufacturer = service.data.get(ATTR_MANUFACTURER) or None + ieee: EUI64 = service.data[ATTR_IEEE] + endpoint_id: int = service.data[ATTR_ENDPOINT_ID] + cluster_id: int = service.data[ATTR_CLUSTER_ID] + cluster_type: str = service.data[ATTR_CLUSTER_TYPE] + command: int = service.data[ATTR_COMMAND] + command_type: str = service.data[ATTR_COMMAND_TYPE] + args: list = service.data[ATTR_ARGS] + manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: manufacturer = zha_device.manufacturer_code @@ -1156,11 +1170,11 @@ def async_load_api(hass: HomeAssistant) -> None: async def issue_zigbee_group_command(service: ServiceCall) -> None: """Issue command on zigbee cluster on a zigbee group.""" - group_id = service.data.get(ATTR_GROUP) - cluster_id = service.data.get(ATTR_CLUSTER_ID) - command = service.data.get(ATTR_COMMAND) - args = service.data.get(ATTR_ARGS) - manufacturer = service.data.get(ATTR_MANUFACTURER) or None + group_id: int = service.data[ATTR_GROUP] + cluster_id: int = service.data[ATTR_CLUSTER_ID] + command: int = service.data[ATTR_COMMAND] + args: list = service.data[ATTR_ARGS] + manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) group = zha_gateway.get_group(group_id) if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: _LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id) @@ -1203,10 +1217,10 @@ def async_load_api(hass: HomeAssistant) -> None: async def warning_device_squawk(service: ServiceCall) -> None: """Issue the squawk command for an IAS warning device.""" - ieee = service.data[ATTR_IEEE] - mode = service.data.get(ATTR_WARNING_DEVICE_MODE) - strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) - level = service.data.get(ATTR_LEVEL) + ieee: EUI64 = service.data[ATTR_IEEE] + mode: int = service.data[ATTR_WARNING_DEVICE_MODE] + strobe: int = service.data[ATTR_WARNING_DEVICE_STROBE] + level: int = service.data[ATTR_LEVEL] if (zha_device := zha_gateway.get_device(ieee)) is not None: if channel := _get_ias_wd_channel(zha_device): From 619c1f014b194ea3d5c563af164b4137d9b565c2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 18 Mar 2022 14:33:01 -0400 Subject: [PATCH 0528/1054] Remove deprecated yaml config from trafikverket_weatherstation (#68336) --- .../config_flow.py | 28 ++------ .../trafikverket_weatherstation/sensor.py | 39 +---------- .../test_config_flow.py | 65 +------------------ 3 files changed, 9 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/trafikverket_weatherstation/config_flow.py b/homeassistant/components/trafikverket_weatherstation/config_flow.py index 5f364bb7472..345d625c7c1 100644 --- a/homeassistant/components/trafikverket_weatherstation/config_flow.py +++ b/homeassistant/components/trafikverket_weatherstation/config_flow.py @@ -1,8 +1,6 @@ """Adds config flow for Trafikverket Weather integration.""" from __future__ import annotations -from typing import Any - from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol @@ -14,13 +12,6 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_STATION, DOMAIN -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_STATION): cv.string, - } -) - class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Trafikverket Weatherstation integration.""" @@ -39,18 +30,8 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return str(err) return "connected" - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a configuration from config.yaml.""" - - self.context.update( - {"title_placeholders": {CONF_STATION: f"YAML import {DOMAIN}"}} - ) - - self._async_abort_entries_match({CONF_STATION: config[CONF_STATION]}) - return await self.async_step_user(user_input=config) - async def async_step_user( - self, user_input: dict[str, Any] | None = None + self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle the initial step.""" errors = {} @@ -80,6 +61,11 @@ class TVWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=DATA_SCHEMA, + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_STATION): cv.string, + } + ), errors=errors, ) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 92211b4d681..0a5f069824c 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -8,20 +8,16 @@ import logging import aiohttp from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, - CONF_MONITORED_CONDITIONS, - CONF_NAME, DEGREE, LENGTH_MILLIMETERS, PERCENTAGE, @@ -30,11 +26,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from .const import ( @@ -151,37 +145,6 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), ) -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_STATION): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): [vol.In(SENSOR_KEYS)], - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Trafikverket Weather configuration from YAML.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2021.12, remove import flow in 2022.4 - "Loading Trafikverket Weather via platform setup is deprecated; Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index b8a18181a11..458652f8175 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -6,12 +6,10 @@ from unittest.mock import patch import pytest from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_FORM -from tests.common import MockConfigEntry - DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" @@ -49,67 +47,6 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", - ), patch( - "homeassistant.components.trafikverket_weatherstation.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_NAME: "Vallby", - CONF_API_KEY: "1234567890", - CONF_STATION: "Vallby", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "Vallby" - assert result2["data"] == { - "api_key": "1234567890", - "station": "Vallby", - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "1234567890", - CONF_STATION: "Vallby", - }, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.trafikverket_weatherstation.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.trafikverket_weatherstation.config_flow.TrafikverketWeather.async_get_weather", - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_NAME: "Vallby", - CONF_API_KEY: "1234567890", - CONF_STATION: "Vallby", - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == "abort" - assert result3["reason"] == "already_configured" - - @pytest.mark.parametrize( "error_message,base_error", [ From 4cc8998ea7beecad22decdd00157a5f64eb06845 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 18 Mar 2022 10:23:26 -1000 Subject: [PATCH 0529/1054] Make powerwall attribute sensors their own sensors (#68345) --- homeassistant/components/powerwall/sensor.py | 143 +++++++++++++------ tests/components/powerwall/test_sensor.py | 97 +++++-------- 2 files changed, 140 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index b0282983629..9e66c61a2bb 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -1,28 +1,30 @@ """Support for powerwall sensors.""" from __future__ import annotations -from typing import Any +from collections.abc import Callable +from dataclasses import dataclass from tesla_powerwall import Meter, MeterType from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_KILO_WATT +from homeassistant.const import ( + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + PERCENTAGE, + POWER_KILO_WATT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTR_FREQUENCY, - ATTR_INSTANT_AVERAGE_VOLTAGE, - ATTR_INSTANT_TOTAL_CURRENT, - ATTR_IS_ACTIVE, - DOMAIN, - POWERWALL_COORDINATOR, -) +from .const import DOMAIN, POWERWALL_COORDINATOR from .entity import PowerWallEntity from .models import PowerwallData, PowerwallRuntimeData @@ -30,6 +32,79 @@ _METER_DIRECTION_EXPORT = "export" _METER_DIRECTION_IMPORT = "import" +@dataclass +class PowerwallRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Meter], float] + + +@dataclass +class PowerwallSensorEntityDescription( + SensorEntityDescription, PowerwallRequiredKeysMixin +): + """Describes Powerwall entity.""" + + +def _get_meter_power(meter: Meter) -> float: + """Get the current value in kW.""" + return meter.get_power(precision=3) + + +def _get_meter_frequency(meter: Meter) -> float: + """Get the current value in Hz.""" + return round(meter.frequency, 1) + + +def _get_meter_total_current(meter: Meter) -> float: + """Get the current value in A.""" + return meter.get_instant_total_current() + + +def _get_meter_average_voltage(meter: Meter) -> float: + """Get the current value in V.""" + return round(meter.average_voltage, 1) + + +POWERWALL_INSTANT_SENSORS = ( + PowerwallSensorEntityDescription( + key="instant_power", + name="Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_KILO_WATT, + value_fn=_get_meter_power, + ), + PowerwallSensorEntityDescription( + key="instant_frequency", + name="Frequency Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.FREQUENCY, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_registry_enabled_default=False, + value_fn=_get_meter_frequency, + ), + PowerwallSensorEntityDescription( + key="instant_current", + name="Average Current Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + entity_registry_enabled_default=False, + value_fn=_get_meter_total_current, + ), + PowerwallSensorEntityDescription( + key="instant_voltage", + name="Average Voltage Now", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + entity_registry_enabled_default=False, + value_fn=_get_meter_average_voltage, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -40,24 +115,17 @@ async def async_setup_entry( coordinator = powerwall_data[POWERWALL_COORDINATOR] assert coordinator is not None data: PowerwallData = coordinator.data - entities: list[ - PowerWallEnergySensor - | PowerWallImportSensor - | PowerWallExportSensor - | PowerWallChargeSensor - | PowerWallBackupReserveSensor - ] = [ + entities: list[PowerWallEntity] = [ PowerWallChargeSensor(powerwall_data), PowerWallBackupReserveSensor(powerwall_data), ] for meter in data.meters.meters: + entities.append(PowerWallExportSensor(powerwall_data, meter)) + entities.append(PowerWallImportSensor(powerwall_data, meter)) entities.extend( - [ - PowerWallEnergySensor(powerwall_data, meter), - PowerWallExportSensor(powerwall_data, meter), - PowerWallImportSensor(powerwall_data, meter), - ] + PowerWallEnergySensor(powerwall_data, meter, description) + for description in POWERWALL_INSTANT_SENSORS ) async_add_entities(entities) @@ -85,34 +153,27 @@ class PowerWallChargeSensor(PowerWallEntity, SensorEntity): class PowerWallEnergySensor(PowerWallEntity, SensorEntity): """Representation of an Powerwall Energy sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = POWER_KILO_WATT - _attr_device_class = SensorDeviceClass.POWER + entity_description: PowerwallSensorEntityDescription - def __init__(self, powerwall_data: PowerwallRuntimeData, meter: MeterType) -> None: + def __init__( + self, + powerwall_data: PowerwallRuntimeData, + meter: MeterType, + description: PowerwallSensorEntityDescription, + ) -> None: """Initialize the sensor.""" + self.entity_description = description super().__init__(powerwall_data) self._meter = meter - self._attr_name = f"Powerwall {self._meter.value.title()} Now" + self._attr_name = f"Powerwall {self._meter.value.title()} {description.name}" self._attr_unique_id = ( - f"{self.base_unique_id}_{self._meter.value}_instant_power" + f"{self.base_unique_id}_{self._meter.value}_{description.key}" ) @property def native_value(self) -> float: - """Get the current value in kW.""" - return self.data.meters.get_meter(self._meter).get_power(precision=3) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the device specific state attributes.""" - meter = self.data.meters.get_meter(self._meter) - return { - ATTR_FREQUENCY: round(meter.frequency, 1), - ATTR_INSTANT_AVERAGE_VOLTAGE: round(meter.average_voltage, 1), - ATTR_INSTANT_TOTAL_CURRENT: meter.get_instant_total_current(), - ATTR_IS_ACTIVE: meter.is_active(), - } + """Get the current value.""" + return self.entity_description.value_fn(self.data.meters.get_meter(self._meter)) class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity): diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 9ff5bd19e39..c40d88fb252 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -2,7 +2,14 @@ from unittest.mock import patch from homeassistant.components.powerwall.const import DOMAIN -from homeassistant.const import CONF_IP_ADDRESS, PERCENTAGE +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + CONF_IP_ADDRESS, + PERCENTAGE, +) from homeassistant.helpers import device_registry as dr from .mocks import _mock_powerwall_with_fixtures @@ -10,7 +17,7 @@ from .mocks import _mock_powerwall_with_fixtures from tests.common import MockConfigEntry -async def test_sensors(hass): +async def test_sensors(hass, entity_registry_enabled_by_default): """Test creation of the sensors.""" mock_powerwall = await _mock_powerwall_with_fixtures(hass) @@ -35,77 +42,49 @@ async def test_sensors(hass): assert reg_device.manufacturer == "Tesla" assert reg_device.name == "MySite" - state = hass.states.get("sensor.powerwall_site_now") - assert state.state == "0.032" - expected_attributes = { - "frequency": 60, - "instant_average_voltage": 120.7, - "unit_of_measurement": "kW", - "friendly_name": "Powerwall Site Now", - "device_class": "power", - "is_active": False, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - for key, value in expected_attributes.items(): - assert state.attributes[key] == value - - assert float(hass.states.get("sensor.powerwall_site_export").state) == 10429.5 - assert float(hass.states.get("sensor.powerwall_site_import").state) == 4824.2 - - export_attributes = hass.states.get("sensor.powerwall_site_export").attributes - assert export_attributes["unit_of_measurement"] == "kWh" - state = hass.states.get("sensor.powerwall_load_now") assert state.state == "1.971" - expected_attributes = { - "frequency": 60, - "instant_average_voltage": 120.7, - "unit_of_measurement": "kW", - "friendly_name": "Powerwall Load Now", - "device_class": "power", - "is_active": True, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - for key, value in expected_attributes.items(): - assert state.attributes[key] == value + attributes = state.attributes + assert attributes[ATTR_DEVICE_CLASS] == "power" + assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "kW" + assert attributes[ATTR_STATE_CLASS] == "measurement" + assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Now" + + state = hass.states.get("sensor.powerwall_load_frequency_now") + assert state.state == "60" + attributes = state.attributes + assert attributes[ATTR_DEVICE_CLASS] == "frequency" + assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "Hz" + assert attributes[ATTR_STATE_CLASS] == "measurement" + assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Frequency Now" + + state = hass.states.get("sensor.powerwall_load_average_voltage_now") + assert state.state == "120.7" + attributes = state.attributes + assert attributes[ATTR_DEVICE_CLASS] == "voltage" + assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert attributes[ATTR_STATE_CLASS] == "measurement" + assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Average Voltage Now" + + state = hass.states.get("sensor.powerwall_load_average_current_now") + assert state.state == "0" + attributes = state.attributes + assert attributes[ATTR_DEVICE_CLASS] == "current" + assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "A" + assert attributes[ATTR_STATE_CLASS] == "measurement" + assert attributes[ATTR_FRIENDLY_NAME] == "Powerwall Load Average Current Now" assert float(hass.states.get("sensor.powerwall_load_export").state) == 1056.8 assert float(hass.states.get("sensor.powerwall_load_import").state) == 4693.0 state = hass.states.get("sensor.powerwall_battery_now") assert state.state == "-8.55" - expected_attributes = { - "frequency": 60.0, - "instant_average_voltage": 240.6, - "unit_of_measurement": "kW", - "friendly_name": "Powerwall Battery Now", - "device_class": "power", - "is_active": True, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - for key, value in expected_attributes.items(): - assert state.attributes[key] == value assert float(hass.states.get("sensor.powerwall_battery_export").state) == 3620.0 assert float(hass.states.get("sensor.powerwall_battery_import").state) == 4216.2 state = hass.states.get("sensor.powerwall_solar_now") assert state.state == "10.49" - expected_attributes = { - "frequency": 60, - "instant_average_voltage": 120.7, - "unit_of_measurement": "kW", - "friendly_name": "Powerwall Solar Now", - "device_class": "power", - "is_active": True, - } - # Only test for a subset of attributes in case - # HA changes the implementation and a new one appears - for key, value in expected_attributes.items(): - assert state.attributes[key] == value assert float(hass.states.get("sensor.powerwall_solar_export").state) == 9864.2 assert float(hass.states.get("sensor.powerwall_solar_import").state) == 28.2 From 171c58fed2483b471d3b76cc223dd68812ebcde0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 18 Mar 2022 23:29:57 +0100 Subject: [PATCH 0530/1054] Rename Lovelace to Dashboards (#68346) --- homeassistant/components/lovelace/cast.py | 2 +- homeassistant/components/lovelace/manifest.json | 2 +- tests/components/lovelace/test_cast.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 02280ebd182..bd06d142bd3 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -33,7 +33,7 @@ async def async_get_media_browser_root_object( return [] return [ BrowseMedia( - title="Lovelace", + title="Dashboards", media_class=MEDIA_CLASS_APP, media_content_id="", media_content_type=DOMAIN, diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index cc8f6ddab08..6f91a61b08c 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -1,6 +1,6 @@ { "domain": "lovelace", - "name": "Lovelace", + "name": "Dashboards", "documentation": "https://www.home-assistant.io/integrations/lovelace", "codeowners": ["@home-assistant/frontend"] } diff --git a/tests/components/lovelace/test_cast.py b/tests/components/lovelace/test_cast.py index d5b8e43d2bb..6f6035b54b6 100644 --- a/tests/components/lovelace/test_cast.py +++ b/tests/components/lovelace/test_cast.py @@ -70,7 +70,7 @@ async def test_root_object(hass): ) assert len(root) == 1 item = root[0] - assert item.title == "Lovelace" + assert item.title == "Dashboards" assert item.media_class == lovelace_cast.MEDIA_CLASS_APP assert item.media_content_id == "" assert item.media_content_type == lovelace_cast.DOMAIN From f9dcf5afa253b0ecbaf7d52b20cd517125e21325 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 19 Mar 2022 03:08:19 +0100 Subject: [PATCH 0531/1054] Add zha typing [core.group] (#68350) --- homeassistant/components/zha/core/group.py | 47 +++++++++++----------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index c8970b2d393..16202291860 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -4,21 +4,20 @@ from __future__ import annotations import asyncio import collections import logging -from typing import Any +from typing import TYPE_CHECKING, Any +import zigpy.endpoint import zigpy.exceptions +import zigpy.group from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_entries_for_device from .helpers import LogMixin -from .typing import ( - ZhaDeviceType, - ZhaGatewayType, - ZhaGroupType, - ZigpyEndpointType, - ZigpyGroupType, -) + +if TYPE_CHECKING: + from .device import ZHADevice + from .gateway import ZHAGateway _LOGGER = logging.getLogger(__name__) @@ -32,15 +31,15 @@ class ZHAGroupMember(LogMixin): """Composite object that represents a device endpoint in a Zigbee group.""" def __init__( - self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int + self, zha_group: ZHAGroup, zha_device: ZHADevice, endpoint_id: int ) -> None: """Initialize the group member.""" - self._zha_group: ZhaGroupType = zha_group - self._zha_device: ZhaDeviceType = zha_device - self._endpoint_id: int = endpoint_id + self._zha_group = zha_group + self._zha_device = zha_device + self._endpoint_id = endpoint_id @property - def group(self) -> ZhaGroupType: + def group(self) -> ZHAGroup: """Return the group this member belongs to.""" return self._zha_group @@ -50,12 +49,12 @@ class ZHAGroupMember(LogMixin): return self._endpoint_id @property - def endpoint(self) -> ZigpyEndpointType: + def endpoint(self) -> zigpy.endpoint.Endpoint: """Return the endpoint for this group member.""" return self._zha_device.device.endpoints.get(self.endpoint_id) @property - def device(self) -> ZhaDeviceType: + def device(self) -> ZHADevice: """Return the zha device for this group member.""" return self._zha_device @@ -101,7 +100,7 @@ class ZHAGroupMember(LogMixin): str(ex), ) - def log(self, level: int, msg: str, *args) -> None: + def log(self, level: int, msg: str, *args: Any) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args @@ -114,13 +113,13 @@ class ZHAGroup(LogMixin): def __init__( self, hass: HomeAssistant, - zha_gateway: ZhaGatewayType, - zigpy_group: ZigpyGroupType, + zha_gateway: ZHAGateway, + zigpy_group: zigpy.group.Group, ) -> None: """Initialize the group.""" - self.hass: HomeAssistant = hass - self._zigpy_group: ZigpyGroupType = zigpy_group - self._zha_gateway: ZhaGatewayType = zha_gateway + self.hass = hass + self._zha_gateway = zha_gateway + self._zigpy_group = zigpy_group @property def name(self) -> str: @@ -133,7 +132,7 @@ class ZHAGroup(LogMixin): return self._zigpy_group.group_id @property - def endpoint(self) -> ZigpyEndpointType: + def endpoint(self) -> zigpy.endpoint.Endpoint: """Return the endpoint for this group.""" return self._zigpy_group.endpoint @@ -192,7 +191,7 @@ class ZHAGroup(LogMixin): all_entity_ids.append(entity_reference["entity_id"]) return all_entity_ids - def get_domain_entity_ids(self, domain) -> list[str]: + def get_domain_entity_ids(self, domain: str) -> list[str]: """Return entity ids from the entity domain for this group.""" domain_entity_ids: list[str] = [] for member in self.members: @@ -217,7 +216,7 @@ class ZHAGroup(LogMixin): group_info["members"] = [member.member_info for member in self.members] return group_info - def log(self, level: int, msg: str, *args): + def log(self, level: int, msg: str, *args: Any) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.name, self.group_id) + args From e0b577f8bd6def01d91227b4960b051636f465d9 Mon Sep 17 00:00:00 2001 From: Michael Kowalchuk Date: Fri, 18 Mar 2022 19:56:05 -0700 Subject: [PATCH 0532/1054] Add zwave_js fan preset modes and enable them for Inovelli LZW36 (#60947) * Rework fan data templates to support preset modes, and define data for the Inovelli LZW36 * Add tests. Add dispatching to async_set_preset_mode in async_turn_on. * Add a test case for invalid preset modes * Remove unintended merge artifact * Fix indentation * Fix merge error * rm blank line * Add tests for invalid fan config data, and fix an issue where this prevented the node from being added. * Fix tests and improve error handling --- .../components/zwave_js/discovery.py | 32 ++-- .../zwave_js/discovery_data_template.py | 138 +++++++------- homeassistant/components/zwave_js/fan.py | 126 +++++++++---- tests/components/zwave_js/test_fan.py | 170 +++++++++++++++++- 4 files changed, 350 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index e580833da9d..7d9c235c00a 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -49,10 +49,11 @@ from homeassistant.helpers.device_registry import DeviceEntry from .const import LOGGER from .discovery_data_template import ( BaseDiscoverySchemaDataTemplate, - ConfigurableFanSpeedDataTemplate, + ConfigurableFanValueMappingDataTemplate, CoverTiltDataTemplate, DynamicCurrentTempClimateDataTemplate, - FixedFanSpeedDataTemplate, + FanValueMapping, + FixedFanValueMappingDataTemplate, NumericSensorDataTemplate, ZwaveValueID, ) @@ -239,25 +240,25 @@ DISCOVERY_SCHEMAS = [ # GE/Jasco - In-Wall Smart Fan Control - 12730 / ZW4002 ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", manufacturer_id={0x0063}, product_id={0x3034}, product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - data_template=FixedFanSpeedDataTemplate( - speeds=[33, 67, 99], + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping(speeds=[(1, 33), (34, 67), (68, 99)]), ), ), # GE/Jasco - In-Wall Smart Fan Control - 14287 / ZW4002 ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", manufacturer_id={0x0063}, product_id={0x3131}, product_type={0x4944}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - data_template=FixedFanSpeedDataTemplate( - speeds=[32, 66, 99], + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping(speeds=[(1, 32), (33, 66), (67, 99)]), ), ), # GE/Jasco - In-Wall Smart Fan Control - 14314 / ZW4002 @@ -280,6 +281,7 @@ DISCOVERY_SCHEMAS = [ # The fan is endpoint 2, the light is endpoint 1. ZWaveDiscoverySchema( platform="fan", + hint="has_fan_value_mapping", manufacturer_id={0x031E}, product_id={0x0001}, product_type={0x000E}, @@ -289,20 +291,28 @@ DISCOVERY_SCHEMAS = [ property={CURRENT_VALUE_PROPERTY}, type={"number"}, ), + data_template=FixedFanValueMappingDataTemplate( + FanValueMapping( + presets={1: "breeze"}, speeds=[(2, 33), (34, 66), (67, 99)] + ), + ), ), # HomeSeer HS-FC200+ ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", manufacturer_id={0x000C}, product_id={0x0001}, product_type={0x0203}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, - data_template=ConfigurableFanSpeedDataTemplate( + data_template=ConfigurableFanValueMappingDataTemplate( configuration_option=ZwaveValueID( 5, CommandClass.CONFIGURATION, endpoint=0 ), - configuration_value_to_speeds={0: [33, 66, 99], 1: [24, 49, 74, 99]}, + configuration_value_to_fan_value_mapping={ + 0: FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]), + 1: FanValueMapping(speeds=[(1, 24), (25, 49), (50, 74), (75, 99)]), + }, ), ), # Fibaro Shutter Fibaro FGR222 diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 3e7db7cdcd9..bfcc1ed87a5 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -432,27 +432,11 @@ class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): @dataclass -class FanSpeedDataTemplate: - """Mixin to define get_speed_config.""" +class FanValueMapping: + """Data class to represent how a fan's values map to features.""" - def get_speed_config(self, resolved_data: dict[str, Any]) -> list[int] | None: - """ - Get the fan speed configuration for this device. - - Values should indicate the highest allowed device setting for each - actual speed, and should be sorted in ascending order. - - Empty lists are not permissible. - """ - raise NotImplementedError - - -@dataclass -class ConfigurableFanSpeedValueMix: - """Mixin data class for defining configurable fan speeds.""" - - configuration_option: ZwaveValueID - configuration_value_to_speeds: dict[int, list[int]] + presets: dict[int, str] = field(default_factory=dict) + speeds: list[tuple[int, int]] = field(default_factory=list) def __post_init__(self) -> None: """ @@ -461,14 +445,36 @@ class ConfigurableFanSpeedValueMix: These inputs are hardcoded in `discovery.py`, so these checks should only fail due to developer error. """ - for speeds in self.configuration_value_to_speeds.values(): - assert len(speeds) > 0 - assert sorted(speeds) == speeds + assert len(self.speeds) > 0, "At least one speed must be specified" + for speed_range in self.speeds: + (low, high) = speed_range + assert high >= low, "Speed range values must be ordered" @dataclass -class ConfigurableFanSpeedDataTemplate( - BaseDiscoverySchemaDataTemplate, FanSpeedDataTemplate, ConfigurableFanSpeedValueMix +class FanValueMappingDataTemplate: + """Mixin to define `get_fan_value_mapping`.""" + + def get_fan_value_mapping( + self, resolved_data: dict[str, Any] + ) -> FanValueMapping | None: + """Get the value mappings for this device.""" + raise NotImplementedError + + +@dataclass +class ConfigurableFanValueMappingValueMix: + """Mixin data class for defining fan properties that change based on a device configuration option.""" + + configuration_option: ZwaveValueID + configuration_value_to_fan_value_mapping: dict[int, FanValueMapping] + + +@dataclass +class ConfigurableFanValueMappingDataTemplate( + BaseDiscoverySchemaDataTemplate, + FanValueMappingDataTemplate, + ConfigurableFanValueMappingValueMix, ): """ Gets fan speeds based on a configuration value. @@ -476,22 +482,23 @@ class ConfigurableFanSpeedDataTemplate( Example: ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", ... - data_template=ConfigurableFanSpeedDataTemplate( + data_template=ConfigurableFanValueMappingDataTemplate( configuration_option=ZwaveValueID( 5, CommandClass.CONFIGURATION, endpoint=0 ), - configuration_value_to_speeds={0: [32, 65, 99], 1: [24, 49, 74, 99]}, + configuration_value_to_fan_value_mapping={ + 0: FanValueMapping(speeds=[(1,33), (34,66), (67,99)]), + 1: FanValueMapping(speeds=[(1,24), (25,49), (50,74), (75,99)]), + }, ), - ), - `configuration_option` is a reference to the setting that determines how - many speeds are supported. + `configuration_option` is a reference to the setting that determines which + value mapping to use (e.g., 3 speeds or 4 speeds). - `configuration_value_to_speeds` maps the values from `configuration_option` - to a list of speeds. The specified speeds indicate the maximum setting on - the underlying switch for each actual speed. + `configuration_value_to_fan_value_mapping` maps the values from + `configuration_option` to the value mapping object. """ def resolve_data(self, value: ZwaveValue) -> dict[str, ZwaveConfigurationValue]: @@ -507,64 +514,61 @@ class ConfigurableFanSpeedDataTemplate( resolved_data["configuration_value"], ] - def get_speed_config( + def get_fan_value_mapping( self, resolved_data: dict[str, ZwaveConfigurationValue] - ) -> list[int] | None: - """Get current speed configuration from resolved data.""" + ) -> FanValueMapping | None: + """Get current fan properties from resolved data.""" zwave_value: ZwaveValue = resolved_data["configuration_value"] + if zwave_value is None: + _LOGGER.warning("Unable to read device configuration value") + return None + if zwave_value.value is None: - _LOGGER.warning("Unable to read fan speed configuration value") + _LOGGER.warning("Fan configuration value is missing") return None - speed_config = self.configuration_value_to_speeds.get(zwave_value.value) - if speed_config is None: - _LOGGER.warning("Unrecognized speed configuration value") + fan_value_mapping = self.configuration_value_to_fan_value_mapping.get( + zwave_value.value + ) + if fan_value_mapping is None: + _LOGGER.warning("Unrecognized fan configuration value") return None - return speed_config + return fan_value_mapping @dataclass -class FixedFanSpeedValueMix: +class FixedFanValueMappingValueMix: """Mixin data class for defining supported fan speeds.""" - speeds: list[int] - - def __post_init__(self) -> None: - """ - Validate inputs. - - These inputs are hardcoded in `discovery.py`, so these checks should - only fail due to developer error. - """ - assert len(self.speeds) > 0 - assert sorted(self.speeds) == self.speeds + fan_value_mapping: FanValueMapping @dataclass -class FixedFanSpeedDataTemplate( - BaseDiscoverySchemaDataTemplate, FanSpeedDataTemplate, FixedFanSpeedValueMix +class FixedFanValueMappingDataTemplate( + BaseDiscoverySchemaDataTemplate, + FanValueMappingDataTemplate, + FixedFanValueMappingValueMix, ): """ - Specifies a fixed set of fan speeds. + Specifies a fixed set of properties for a fan. Example: ZWaveDiscoverySchema( platform="fan", - hint="configured_fan_speed", + hint="has_fan_value_mapping", ... - data_template=FixedFanSpeedDataTemplate( - speeds=[32,65,99] + data_template=FixedFanValueMappingDataTemplate( + config=FanValueMapping( + speeds=[(1, 32), (33, 65), (66, 99)] + ) ), ), - - `speeds` indicates the maximum setting on the underlying fan controller - for each actual speed. """ - def get_speed_config( + def get_fan_value_mapping( self, resolved_data: dict[str, ZwaveConfigurationValue] - ) -> list[int]: - """Get the fan speed configuration for this device.""" - return self.speeds + ) -> FanValueMapping: + """Get the fan properties for this device.""" + return self.fan_value_mapping diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 585a72fc6de..21c45bbbf42 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, + NotValidPresetModeError, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -31,12 +32,10 @@ from homeassistant.util.percentage import ( from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo -from .discovery_data_template import FanSpeedDataTemplate +from .discovery_data_template import FanValueMapping, FanValueMappingDataTemplate from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value -SUPPORTED_FEATURES = SUPPORT_SET_SPEED - DEFAULT_SPEED_RANGE = (1, 99) # off is not included ATTR_FAN_STATE = "fan_state" @@ -54,8 +53,8 @@ async def async_setup_entry( def async_add_fan(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave fan.""" entities: list[ZWaveBaseEntity] = [] - if info.platform_hint == "configured_fan_speed": - entities.append(ConfiguredSpeedRangeZwaveFan(config_entry, client, info)) + if info.platform_hint == "has_fan_value_mapping": + entities.append(ValueMappingZwaveFan(config_entry, client, info)) elif info.platform_hint == "thermostat_fan": entities.append(ZwaveThermostatFan(config_entry, client, info)) else: @@ -100,11 +99,13 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): **kwargs: Any, ) -> None: """Turn the device on.""" - if percentage is None: + if percentage is not None: + await self.async_set_percentage(percentage) + elif preset_mode is not None: + await self.async_set_preset_mode(preset_mode) + else: # Value 255 tells device to return to previous value await self.info.node.async_set_value(self._target_value, 255) - else: - await self.async_set_percentage(percentage) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" @@ -141,11 +142,11 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORTED_FEATURES + return SUPPORT_SET_SPEED -class ConfiguredSpeedRangeZwaveFan(ZwaveFan): - """A Zwave fan with a configured speed range (e.g., 1-24 is low).""" +class ValueMappingZwaveFan(ZwaveFan): + """A Zwave fan with a value mapping data (e.g., 1-24 is low).""" def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo @@ -153,7 +154,7 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): """Initialize the fan.""" super().__init__(config_entry, client, info) self.data_template = cast( - FanSpeedDataTemplate, self.info.platform_data_template + FanValueMappingDataTemplate, self.info.platform_data_template ) async def async_set_percentage(self, percentage: int) -> None: @@ -161,10 +162,21 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): zwave_speed = self.percentage_to_zwave_speed(percentage) await self.info.node.async_set_value(self._target_value, zwave_speed) + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + for zwave_value, mapped_preset_mode in self.fan_value_mapping.presets.items(): + if preset_mode == mapped_preset_mode: + await self.info.node.async_set_value(self._target_value, zwave_value) + return + + raise NotValidPresetModeError( + f"The preset_mode {preset_mode} is not a valid preset_mode: {self.preset_modes}" + ) + @property def available(self) -> bool: """Return whether the entity is available.""" - return super().available and self.has_speed_configuration + return super().available and self.has_fan_value_mapping @property def percentage(self) -> int | None: @@ -173,6 +185,9 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): # guard missing value return None + if self.preset_mode is not None: + return None + return self.zwave_speed_to_percentage(self.info.primary_value.value) @property @@ -184,26 +199,51 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): return 100 / self.speed_count @property - def has_speed_configuration(self) -> bool: - """Check if the speed configuration is valid.""" - return self.data_template.get_speed_config(self.info.platform_data) is not None + def preset_modes(self) -> list[str]: + """Return the available preset modes.""" + if not self.has_fan_value_mapping: + return [] + + return list(self.fan_value_mapping.presets.values()) @property - def speed_configuration(self) -> list[int]: + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self.fan_value_mapping.presets.get(self.info.primary_value.value) + + @property + def has_fan_value_mapping(self) -> bool: + """Check if the speed configuration is valid.""" + return ( + self.data_template.get_fan_value_mapping(self.info.platform_data) + is not None + ) + + @property + def fan_value_mapping(self) -> FanValueMapping: """Return the speed configuration for this fan.""" - speed_configuration = self.data_template.get_speed_config( + fan_value_mapping = self.data_template.get_fan_value_mapping( self.info.platform_data ) # Entity should be unavailable if this isn't set - assert speed_configuration is not None + assert fan_value_mapping is not None - return speed_configuration + return fan_value_mapping @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return len(self.speed_configuration) + return len(self.fan_value_mapping.speeds) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = SUPPORT_SET_SPEED + if self.has_fan_value_mapping and self.fan_value_mapping.presets: + flags |= SUPPORT_PRESET_MODE + + return flags def percentage_to_zwave_speed(self, percentage: int) -> int: """Map a percentage to a ZWave speed.""" @@ -212,30 +252,46 @@ class ConfiguredSpeedRangeZwaveFan(ZwaveFan): # Since the percentage steps are computed with rounding, we have to # search to find the appropriate speed. - for speed_limit in self.speed_configuration: - step_percentage = self.zwave_speed_to_percentage(speed_limit) + for speed_range in self.fan_value_mapping.speeds: + (_, max_speed) = speed_range + step_percentage = self.zwave_speed_to_percentage(max_speed) + + # zwave_speed_to_percentage will only return None if + # `self.fan_value_mapping.speeds` doesn't contain the + # specified speed. This can't happen here, because + # the input is coming from the same data structure. + assert step_percentage + if percentage <= step_percentage: - return speed_limit + return max_speed # This shouldn't actually happen; the last entry in - # `self.speed_configuration` should map to 100%. - return self.speed_configuration[-1] + # `self.fan_value_mapping.speeds` should map to 100%. + (_, last_max_speed) = self.fan_value_mapping.speeds[-1] + return last_max_speed - def zwave_speed_to_percentage(self, zwave_speed: int) -> int: - """Convert a Zwave speed to a percentage.""" + def zwave_speed_to_percentage(self, zwave_speed: int) -> int | None: + """ + Convert a Zwave speed to a percentage. + + This method may return None if the device's value mapping doesn't cover + the specified Z-Wave speed. + """ if zwave_speed == 0: return 0 percentage = 0.0 - for speed_limit in self.speed_configuration: + for speed_range in self.fan_value_mapping.speeds: + (min_speed, max_speed) = speed_range percentage += self.percentage_step - if zwave_speed <= speed_limit: - break + if min_speed <= zwave_speed <= max_speed: + # This choice of rounding function is to provide consistency with how + # the UI handles steps e.g., for a 3-speed fan, you get steps at 33, + # 67, and 100. + return round(percentage) - # This choice of rounding function is to provide consistency with how - # the UI handles steps e.g., for a 3-speed fan, you get steps at 33, - # 67, and 100. - return round(percentage) + # The specified Z-Wave device value doesn't map to a defined speed. + return None class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py index 2535377e9d3..140d9fb3d83 100644 --- a/tests/components/zwave_js/test_fan.py +++ b/tests/components/zwave_js/test_fan.py @@ -1,10 +1,12 @@ """Test the Z-Wave JS fan platform.""" +import copy import math import pytest from voluptuous.error import MultipleInvalid from zwave_js_server.const import CommandClass from zwave_js_server.event import Event +from zwave_js_server.model.node import Node from homeassistant.components.fan import ( ATTR_PERCENTAGE, @@ -14,6 +16,7 @@ from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, SERVICE_SET_PRESET_MODE, SUPPORT_PRESET_MODE, + NotValidPresetModeError, ) from homeassistant.components.zwave_js.fan import ATTR_FAN_STATE from homeassistant.const import ( @@ -23,6 +26,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.exceptions import HomeAssistantError @@ -63,7 +67,6 @@ async def test_generic_fan(hass, client, fan_generic, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", }, } assert args["value"] == 66 @@ -106,7 +109,6 @@ async def test_generic_fan(hass, client, fan_generic, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", }, } assert args["value"] == 255 @@ -138,7 +140,6 @@ async def test_generic_fan(hass, client, fan_generic, integration): "type": "number", "readable": True, "writeable": True, - "label": "Target value", }, } assert args["value"] == 0 @@ -259,6 +260,65 @@ async def test_configurable_speeds_fan(hass, client, hs_fc200, integration): state = hass.states.get(entity_id) assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + assert state.attributes[ATTR_PRESET_MODES] == [] + + +async def test_configurable_speeds_fan_with_missing_config_value( + hass, client, hs_fc200_state, integration +): + """Test a fan entity with configurable speeds.""" + entity_id = "fan.scene_capable_fan_control_switch" + + # Attach a modified version of the node with a bad config + bad_node_data = copy.deepcopy(hs_fc200_state) + fan_type_value = next( + ( + v + for v in bad_node_data["values"] + if v["endpoint"] == 0 and v["commandClass"] == 112 and v["property"] == 5 + ), + None, + ) + assert fan_type_value is not None + bad_node_data["values"].remove(fan_type_value) + + node = Node(client, bad_node_data) + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + + +async def test_configurable_speeds_fan_with_bad_config_value( + hass, client, hs_fc200_state, integration +): + """Test a fan entity with configurable speeds.""" + entity_id = "fan.scene_capable_fan_control_switch" + + # Attach a modified version of the node with a bad config + bad_node_data = copy.deepcopy(hs_fc200_state) + fan_type_value = next( + ( + v + for v in bad_node_data["values"] + if v["endpoint"] == 0 and v["commandClass"] == 112 and v["property"] == 5 + ), + None, + ) + assert fan_type_value is not None + + # 42 is not a valid configuration option with this device + fan_type_value["value"] = 42 + + node = Node(client, bad_node_data) + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE async def test_fixed_speeds_fan(hass, client, ge_12730, integration): @@ -325,6 +385,110 @@ async def test_fixed_speeds_fan(hass, client, ge_12730, integration): state = hass.states.get(entity_id) assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + assert state.attributes[ATTR_PRESET_MODES] == [] + + +async def test_inovelli_lzw36(hass, client, inovelli_lzw36, integration): + """Test an LZW36.""" + node = inovelli_lzw36 + node_id = 19 + entity_id = "fan.family_room_combo_2" + + async def get_zwave_speed_from_percentage(percentage): + """Set the fan to a particular percentage and get the resulting Zwave speed.""" + client.async_send_command.reset_mock() + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": entity_id, "percentage": percentage}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node_id + return args["value"] + + async def set_zwave_speed(zwave_speed): + """Set the underlying device speed.""" + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node_id, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 2, + "property": "currentValue", + "newValue": zwave_speed, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + async def get_percentage_from_zwave_speed(zwave_speed): + """Set the underlying device speed and get the resulting percentage.""" + await set_zwave_speed(zwave_speed) + state = hass.states.get(entity_id) + return state.attributes[ATTR_PERCENTAGE] + + # This device has the speeds: + # low = 2-33, med = 34-66, high = 67-99 + percentages_to_zwave_speeds = [ + [[0], [0]], + [range(1, 34), range(2, 34)], + [range(34, 68), range(34, 67)], + [range(68, 101), range(67, 100)], + ] + + for percentages, zwave_speeds in percentages_to_zwave_speeds: + for percentage in percentages: + actual_zwave_speed = await get_zwave_speed_from_percentage(percentage) + assert actual_zwave_speed in zwave_speeds + for zwave_speed in zwave_speeds: + actual_percentage = await get_percentage_from_zwave_speed(zwave_speed) + assert actual_percentage in percentages + + # Check static entity properties + state = hass.states.get(entity_id) + assert math.isclose(state.attributes[ATTR_PERCENTAGE_STEP], 33.3333, rel_tol=1e-3) + assert state.attributes[ATTR_PRESET_MODES] == ["breeze"] + + # This device has one preset, where a device level of "1" is the + # "breeze" mode + await set_zwave_speed(1) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == "breeze" + assert state.attributes[ATTR_PERCENTAGE] is None + + client.async_send_command.reset_mock() + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": entity_id, "preset_mode": "breeze"}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == node_id + assert args["value"] == 1 + + client.async_send_command.reset_mock() + with pytest.raises(NotValidPresetModeError): + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": entity_id, "preset_mode": "wheeze"}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 0 async def test_thermostat_fan(hass, client, climate_adc_t3000, integration): From 4578de68e7486d33fff6b1117293777af91679c4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 19 Mar 2022 02:46:01 -0400 Subject: [PATCH 0533/1054] Redact data from zwave_js diagnostics (#68348) * Redact home ID and location from zwave_js diagnostics * Switch to set --- homeassistant/components/zwave_js/diagnostics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 8b59c38d405..dd9860edeb0 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -5,6 +5,7 @@ from zwave_js_server.client import Client from zwave_js_server.dump import dump_msgs from zwave_js_server.model.node import NodeDataType +from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant @@ -14,6 +15,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DATA_CLIENT, DOMAIN from .helpers import get_home_and_node_id_from_device_entry +TO_REDACT = ("homeId", "location") + async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry @@ -22,7 +25,7 @@ async def async_get_config_entry_diagnostics( msgs: list[dict] = await dump_msgs( config_entry.data[CONF_URL], async_get_clientsession(hass) ) - return msgs + return async_redact_data(msgs, TO_REDACT) async def async_get_device_diagnostics( @@ -42,5 +45,5 @@ async def async_get_device_diagnostics( "minSchemaVersion": client.version.min_schema_version, "maxSchemaVersion": client.version.max_schema_version, }, - "state": node.data, + "state": async_redact_data(node.data, TO_REDACT), } From 4cd4fbefbf7142ecf0734fcf6365a034b53ec4ff Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 19 Mar 2022 03:42:22 -0400 Subject: [PATCH 0534/1054] Add new tomorrow.io integration to replace Climacell (#68156) * Add new tomorrow.io integration to replace Climacell - Part 1/3 (#57121) * Add new tomorrow.io integration to replace Climacell - Part 1/3 * remove unused code * remove extra test * remove more unused code * Remove even more unused code * Feedback * clean up options flow * clean up options flow * tweaks and fix tests * remove device_class from tomorrowio entity description class * use timestep * fix tests * always use default name but add zone name if location is in a zone * revert change that will go into future PR * review comments * move code out of try block * bump max requests to 500 as per docs * fix tests * Add new tomorrow.io integration to replace Climacell - Part 2/3 (#57124) * Add new tomorrow.io integration to replace Climacell - Part 2/3 * translations * set config flow to false in manifest * Cleanup more code and re-add options flow test * fixes * patch I/O calls * Update tests/components/climacell/test_config_flow.py Co-authored-by: Martin Hjelmare * remove unused import Co-authored-by: Martin Hjelmare * Fix codeowners * fix mypy and pylint * Switch to DeviceInfo * Fix fixture location and improve sensor entities in tomorrowio integration (#63527) * Add new tomorrow.io integration to replace Climacell - Part 3/3 (#59698) * Switch to DeviceInfo * Add new tomorrow.io integration to replace Climacell - Part 1/3 (#57121) * Add new tomorrow.io integration to replace Climacell - Part 1/3 * remove unused code * remove extra test * remove more unused code * Remove even more unused code * Feedback * clean up options flow * clean up options flow * tweaks and fix tests * remove device_class from tomorrowio entity description class * use timestep * fix tests * always use default name but add zone name if location is in a zone * revert change that will go into future PR * review comments * move code out of try block * bump max requests to 500 as per docs * fix tests * Migrate ClimaCell entries to Tomorrow.io * tweaks * pylint * Apply fix from #60454 to tomorrowio integration * lint and mypy * use speed conversion instead of distance conversion * Use SensorDeviceClass enum * Use built in conversions and remove unused loggers * fix requirements * Update homeassistant/components/tomorrowio/__init__.py Co-authored-by: Martin Hjelmare * Use constants Co-authored-by: Martin Hjelmare * Black * Update logic and add coverage * remove extra line * Do patching correctly Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + .../components/climacell/__init__.py | 181 +- .../components/climacell/config_flow.py | 117 +- homeassistant/components/climacell/const.py | 277 +- .../components/climacell/manifest.json | 3 +- homeassistant/components/climacell/sensor.py | 58 +- .../components/climacell/strings.json | 20 - .../components/climacell/translations/en.json | 23 +- homeassistant/components/climacell/weather.py | 180 +- .../components/tomorrowio/__init__.py | 351 +++ .../components/tomorrowio/config_flow.py | 214 ++ homeassistant/components/tomorrowio/const.py | 143 + .../components/tomorrowio/manifest.json | 9 + homeassistant/components/tomorrowio/sensor.py | 365 +++ .../components/tomorrowio/strings.json | 32 + .../components/tomorrowio/strings.sensor.json | 27 + .../tomorrowio/translations/en.json | 32 + .../tomorrowio/translations/sensor.en.json | 27 + .../components/tomorrowio/weather.py | 253 ++ homeassistant/generated/config_flows.py | 2 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/climacell/conftest.py | 24 +- tests/components/climacell/const.py | 19 - tests/components/climacell/fixtures/v4.json | 2384 ----------------- .../components/climacell/test_config_flow.py | 164 +- tests/components/climacell/test_init.py | 45 +- tests/components/climacell/test_sensor.py | 44 +- tests/components/climacell/test_weather.py | 176 +- tests/components/tomorrowio/__init__.py | 1 + tests/components/tomorrowio/conftest.py | 46 + tests/components/tomorrowio/const.py | 21 + tests/components/tomorrowio/fixtures/v4.json | 2384 +++++++++++++++++ .../components/tomorrowio/test_config_flow.py | 279 ++ tests/components/tomorrowio/test_init.py | 151 ++ tests/components/tomorrowio/test_sensor.py | 166 ++ tests/components/tomorrowio/test_weather.py | 245 ++ 37 files changed, 4895 insertions(+), 3576 deletions(-) create mode 100644 homeassistant/components/tomorrowio/__init__.py create mode 100644 homeassistant/components/tomorrowio/config_flow.py create mode 100644 homeassistant/components/tomorrowio/const.py create mode 100644 homeassistant/components/tomorrowio/manifest.json create mode 100644 homeassistant/components/tomorrowio/sensor.py create mode 100644 homeassistant/components/tomorrowio/strings.json create mode 100644 homeassistant/components/tomorrowio/strings.sensor.json create mode 100644 homeassistant/components/tomorrowio/translations/en.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.en.json create mode 100644 homeassistant/components/tomorrowio/weather.py delete mode 100644 tests/components/climacell/fixtures/v4.json create mode 100644 tests/components/tomorrowio/__init__.py create mode 100644 tests/components/tomorrowio/conftest.py create mode 100644 tests/components/tomorrowio/const.py create mode 100644 tests/components/tomorrowio/fixtures/v4.json create mode 100644 tests/components/tomorrowio/test_config_flow.py create mode 100644 tests/components/tomorrowio/test_init.py create mode 100644 tests/components/tomorrowio/test_sensor.py create mode 100644 tests/components/tomorrowio/test_weather.py diff --git a/CODEOWNERS b/CODEOWNERS index 12a7b42f0d2..1a7c960730b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1026,6 +1026,8 @@ homeassistant/components/todoist/* @boralyl tests/components/todoist/* @boralyl homeassistant/components/tolo/* @MatthiasLohr tests/components/tolo/* @MatthiasLohr +homeassistant/components/tomorrowio/* @raman325 +tests/components/tomorrowio/* @raman325 homeassistant/components/totalconnect/* @austinmroczek tests/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index e3edc778955..e408476aad3 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -15,7 +15,8 @@ from pyclimacell.exceptions import ( UnknownException, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.components.tomorrowio import DOMAIN as TOMORROW_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_API_VERSION, @@ -36,22 +37,6 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( ATTRIBUTION, - CC_ATTR_CLOUD_COVER, - CC_ATTR_CONDITION, - CC_ATTR_HUMIDITY, - CC_ATTR_OZONE, - CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_PROBABILITY, - CC_ATTR_PRECIPITATION_TYPE, - CC_ATTR_PRESSURE, - CC_ATTR_TEMPERATURE, - CC_ATTR_TEMPERATURE_HIGH, - CC_ATTR_TEMPERATURE_LOW, - CC_ATTR_VISIBILITY, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_WIND_GUST, - CC_ATTR_WIND_SPEED, - CC_SENSOR_TYPES, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, @@ -142,8 +127,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if params: hass.config_entries.async_update_entry(entry, **params) - api_class = ClimaCellV3 if entry.data[CONF_API_VERSION] == 3 else ClimaCellV4 - api = api_class( + hass.async_create_task( + hass.config_entries.flow.async_init( + TOMORROW_DOMAIN, + context={"source": SOURCE_IMPORT, "old_config_entry_id": entry.entry_id}, + data=entry.data, + ) + ) + + # Eventually we will remove the code that sets up the platforms and force users to + # migrate. This will only impact users still on the V3 API because we can't + # automatically migrate them, but for V4 users, we can skip the platform setup. + if entry.data[CONF_API_VERSION] == 4: + return True + + api = ClimaCellV3( entry.data[CONF_API_KEY], entry.data.get(CONF_LATITUDE, hass.config.latitude), entry.data.get(CONF_LONGITUDE, hass.config.longitude), @@ -172,7 +170,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry, PLATFORMS ) - hass.data[DOMAIN].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(config_entry.entry_id, None) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) @@ -208,89 +206,62 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" data: dict[str, Any] = {FORECASTS: {}} try: - if self._api_version == 3: - data[CURRENT] = await self._api.realtime( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_PRECIPITATION_TYPE, - *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), - ] - ) - data[FORECASTS][HOURLY] = await self._api.forecast_hourly( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(hours=24), - ) + data[CURRENT] = await self._api.realtime( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_HUMIDITY, + CC_V3_ATTR_PRESSURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_VISIBILITY, + CC_V3_ATTR_OZONE, + CC_V3_ATTR_WIND_GUST, + CC_V3_ATTR_CLOUD_COVER, + CC_V3_ATTR_PRECIPITATION_TYPE, + *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), + ] + ) + data[FORECASTS][HOURLY] = await self._api.forecast_hourly( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(hours=24), + ) - data[FORECASTS][DAILY] = await self._api.forecast_daily( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(days=14), - ) + data[FORECASTS][DAILY] = await self._api.forecast_daily( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION_DAILY, + CC_V3_ATTR_PRECIPITATION_PROBABILITY, + ], + None, + timedelta(days=14), + ) - data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - ], - None, - timedelta( - minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) - ), - self._config_entry.options[CONF_TIMESTEP], - ) - else: - return await self._api.realtime_and_all_forecasts( - [ - CC_ATTR_TEMPERATURE, - CC_ATTR_HUMIDITY, - CC_ATTR_PRESSURE, - CC_ATTR_WIND_SPEED, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_CONDITION, - CC_ATTR_VISIBILITY, - CC_ATTR_OZONE, - CC_ATTR_WIND_GUST, - CC_ATTR_CLOUD_COVER, - CC_ATTR_PRECIPITATION_TYPE, - *(sensor_type.key for sensor_type in CC_SENSOR_TYPES), - ], - [ - CC_ATTR_TEMPERATURE_LOW, - CC_ATTR_TEMPERATURE_HIGH, - CC_ATTR_WIND_SPEED, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_CONDITION, - CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_PROBABILITY, - ], - ) + data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( + [ + CC_V3_ATTR_TEMPERATURE, + CC_V3_ATTR_WIND_SPEED, + CC_V3_ATTR_WIND_DIRECTION, + CC_V3_ATTR_CONDITION, + CC_V3_ATTR_PRECIPITATION, + ], + None, + timedelta( + minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) + ), + self._config_entry.options[CONF_TIMESTEP], + ) except ( CantConnectException, InvalidAPIKeyException, @@ -341,14 +312,6 @@ class ClimaCellEntity(CoordinatorEntity): return items.get("value") - def _get_current_property(self, property_name: str) -> int | str | float | None: - """ - Get property from current conditions. - - Used for V4 API. - """ - return self.coordinator.data.get(CURRENT, {}).get(property_name) - @property def attribution(self): """Return the attribution.""" diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index 61cae798ff1..ffc76479a4d 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -1,84 +1,15 @@ """Config flow for ClimaCell integration.""" from __future__ import annotations -import logging from typing import Any -from pyclimacell import ClimaCellV3 -from pyclimacell.exceptions import ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, -) -from pyclimacell.pyclimacell import ClimaCellV4 import voluptuous as vol -from homeassistant import config_entries, core -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from .const import ( - CC_ATTR_TEMPERATURE, - CC_V3_ATTR_TEMPERATURE, - CONF_TIMESTEP, - DEFAULT_NAME, - DEFAULT_TIMESTEP, - DOMAIN, -) - -_LOGGER = logging.getLogger(__name__) - - -def _get_config_schema( - hass: core.HomeAssistant, input_dict: dict[str, Any] = None -) -> vol.Schema: - """ - Return schema defaults for init step based on user input/config dict. - - Retain info already provided for future form views by setting them as - defaults in schema. - """ - if input_dict is None: - input_dict = {} - - return vol.Schema( - { - vol.Required( - CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME) - ): str, - vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, - vol.Required(CONF_API_VERSION, default=4): vol.In([3, 4]), - vol.Inclusive( - CONF_LATITUDE, - "location", - default=input_dict.get(CONF_LATITUDE, hass.config.latitude), - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, - "location", - default=input_dict.get(CONF_LONGITUDE, hass.config.longitude), - ): cv.longitude, - }, - extra=vol.REMOVE_EXTRA, - ) - - -def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): - """Return unique ID from config data.""" - return ( - f"{input_dict[CONF_API_KEY]}" - f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}" - f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}" - ) +from .const import CONF_TIMESTEP, DEFAULT_TIMESTEP, DOMAIN class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): @@ -117,45 +48,3 @@ class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> ClimaCellOptionsConfigFlow: """Get the options flow for this handler.""" return ClimaCellOptionsConfigFlow(config_entry) - - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: - """Handle the initial step.""" - errors = {} - if user_input is not None: - await self.async_set_unique_id( - unique_id=_get_unique_id(self.hass, user_input) - ) - self._abort_if_unique_id_configured() - - try: - if user_input[CONF_API_VERSION] == 3: - api_class = ClimaCellV3 - field = CC_V3_ATTR_TEMPERATURE - else: - api_class = ClimaCellV4 - field = CC_ATTR_TEMPERATURE - await api_class( - user_input[CONF_API_KEY], - str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)), - str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)), - session=async_get_clientsession(self.hass), - ).realtime([field]) - - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - except CantConnectException: - errors["base"] = "cannot_connect" - except InvalidAPIKeyException: - errors[CONF_API_KEY] = "invalid_api_key" - except RateLimitedException: - errors[CONF_API_KEY] = "rate_limited" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="user", - data_schema=_get_config_schema(self.hass, user_input), - errors=errors, - ) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py index 7ee804f42f1..f7ca21259e1 100644 --- a/homeassistant/components/climacell/const.py +++ b/homeassistant/components/climacell/const.py @@ -5,17 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from enum import IntEnum -from pyclimacell.const import ( - DAILY, - HOURLY, - NOWCAST, - HealthConcernType, - PollenIndex, - PrecipitationType, - PrimaryPollutantType, - V3PollenIndex, - WeatherCode, -) +from pyclimacell.const import DAILY, HOURLY, NOWCAST, V3PollenIndex from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.components.weather import ( @@ -37,22 +27,7 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, - IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - PERCENTAGE, - PRESSURE_HPA, - PRESSURE_INHG, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) -from homeassistant.util.distance import convert as distance_convert -from homeassistant.util.pressure import convert as pressure_convert -from homeassistant.util.temperature import convert as temp_convert CONF_TIMESTEP = "timestep" FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] @@ -78,75 +53,6 @@ ATTR_WIND_GUST = "wind_gust" ATTR_CLOUD_COVER = "cloud_cover" ATTR_PRECIPITATION_TYPE = "precipitation_type" -# V4 constants -CONDITIONS = { - WeatherCode.WIND: ATTR_CONDITION_WINDY, - WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY, - WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY, - WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY, - WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL, - WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL, - WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL, - WeatherCode.SNOW: ATTR_CONDITION_SNOWY, - WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, - WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, - WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, - WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, - WeatherCode.RAIN: ATTR_CONDITION_POURING, - WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, - WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, - WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, - WeatherCode.FOG: ATTR_CONDITION_FOG, - WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG, - WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY, - WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY, - WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, -} - -# Weather constants -CC_ATTR_TIMESTAMP = "startTime" -CC_ATTR_TEMPERATURE = "temperature" -CC_ATTR_TEMPERATURE_HIGH = "temperatureMax" -CC_ATTR_TEMPERATURE_LOW = "temperatureMin" -CC_ATTR_PRESSURE = "pressureSeaLevel" -CC_ATTR_HUMIDITY = "humidity" -CC_ATTR_WIND_SPEED = "windSpeed" -CC_ATTR_WIND_DIRECTION = "windDirection" -CC_ATTR_OZONE = "pollutantO3" -CC_ATTR_CONDITION = "weatherCode" -CC_ATTR_VISIBILITY = "visibility" -CC_ATTR_PRECIPITATION = "precipitationIntensityAvg" -CC_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" -CC_ATTR_WIND_GUST = "windGust" -CC_ATTR_CLOUD_COVER = "cloudCover" -CC_ATTR_PRECIPITATION_TYPE = "precipitationType" - -# Sensor attributes -CC_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" -CC_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" -CC_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" -CC_ATTR_CARBON_MONOXIDE = "pollutantCO" -CC_ATTR_SULFUR_DIOXIDE = "pollutantSO2" -CC_ATTR_EPA_AQI = "epaIndex" -CC_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" -CC_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" -CC_ATTR_CHINA_AQI = "mepIndex" -CC_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" -CC_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" -CC_ATTR_POLLEN_TREE = "treeIndex" -CC_ATTR_POLLEN_WEED = "weedIndex" -CC_ATTR_POLLEN_GRASS = "grassIndex" -CC_ATTR_FIRE_INDEX = "fireIndex" -CC_ATTR_FEELS_LIKE = "temperatureApparent" -CC_ATTR_DEW_POINT = "dewPoint" -CC_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" -CC_ATTR_SOLAR_GHI = "solarGHI" -CC_ATTR_CLOUD_BASE = "cloudBase" -CC_ATTR_CLOUD_CEILING = "cloudCeiling" - @dataclass class ClimaCellSensorEntityDescription(SensorEntityDescription): @@ -169,187 +75,6 @@ class ClimaCellSensorEntityDescription(SensorEntityDescription): ) -CC_SENSOR_TYPES = ( - ClimaCellSensorEntityDescription( - key=CC_ATTR_FEELS_LIKE, - name="Feels Like", - unit_imperial=TEMP_FAHRENHEIT, - unit_metric=TEMP_CELSIUS, - metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), - is_metric_check=True, - device_class=SensorDeviceClass.TEMPERATURE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_DEW_POINT, - name="Dew Point", - unit_imperial=TEMP_FAHRENHEIT, - unit_metric=TEMP_CELSIUS, - metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), - is_metric_check=True, - device_class=SensorDeviceClass.TEMPERATURE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PRESSURE_SURFACE_LEVEL, - name="Pressure (Surface Level)", - unit_imperial=PRESSURE_INHG, - unit_metric=PRESSURE_HPA, - metric_conversion=lambda val: pressure_convert( - val, PRESSURE_INHG, PRESSURE_HPA - ), - is_metric_check=True, - device_class=SensorDeviceClass.PRESSURE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_SOLAR_GHI, - name="Global Horizontal Irradiance", - unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, - unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, - metric_conversion=3.15459, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CLOUD_BASE, - name="Cloud Base", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, - metric_conversion=lambda val: distance_convert( - val, LENGTH_MILES, LENGTH_KILOMETERS - ), - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CLOUD_CEILING, - name="Cloud Ceiling", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, - metric_conversion=lambda val: distance_convert( - val, LENGTH_MILES, LENGTH_KILOMETERS - ), - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CLOUD_COVER, - name="Cloud Cover", - unit_imperial=PERCENTAGE, - unit_metric=PERCENTAGE, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_WIND_GUST, - name="Wind Gust", - unit_imperial=SPEED_MILES_PER_HOUR, - unit_metric=SPEED_METERS_PER_SECOND, - metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) - / 3600, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PRECIPITATION_TYPE, - name="Precipitation Type", - value_map=PrecipitationType, - device_class="climacell__precipitation_type", - icon="mdi:weather-snowy-rainy", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_OZONE, - name="Ozone", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PARTICULATE_MATTER_25, - name="Particulate Matter < 2.5 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_PARTICULATE_MATTER_10, - name="Particulate Matter < 10 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=True, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_NITROGEN_DIOXIDE, - name="Nitrogen Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CARBON_MONOXIDE, - name="Carbon Monoxide", - unit_imperial=CONCENTRATION_PARTS_PER_MILLION, - unit_metric=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_SULFUR_DIOXIDE, - name="Sulfur Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_EPA_AQI, - name="US EPA Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_EPA_PRIMARY_POLLUTANT, - name="US EPA Primary Pollutant", - value_map=PrimaryPollutantType, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_EPA_HEALTH_CONCERN, - name="US EPA Health Concern", - value_map=HealthConcernType, - device_class="climacell__health_concern", - icon="mdi:hospital", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CHINA_AQI, - name="China MEP Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CHINA_PRIMARY_POLLUTANT, - name="China MEP Primary Pollutant", - value_map=PrimaryPollutantType, - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_CHINA_HEALTH_CONCERN, - name="China MEP Health Concern", - value_map=HealthConcernType, - device_class="climacell__health_concern", - icon="mdi:hospital", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_POLLEN_TREE, - name="Tree Pollen Index", - value_map=PollenIndex, - device_class="climacell__pollen_index", - icon="mdi:flower-pollen", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_POLLEN_WEED, - name="Weed Pollen Index", - value_map=PollenIndex, - device_class="climacell__pollen_index", - icon="mdi:flower-pollen", - ), - ClimaCellSensorEntityDescription( - key=CC_ATTR_POLLEN_GRASS, - name="Grass Pollen Index", - value_map=PollenIndex, - device_class="climacell__pollen_index", - icon="mdi:flower-pollen", - ), - ClimaCellSensorEntityDescription( - CC_ATTR_FIRE_INDEX, - name="Fire Index", - icon="mdi:fire", - ), -) - # V3 constants CONDITIONS_V3 = { "breezy": ATTR_CONDITION_WINDY, diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json index 4928d92447e..73594c37dfb 100644 --- a/homeassistant/components/climacell/manifest.json +++ b/homeassistant/components/climacell/manifest.json @@ -1,9 +1,10 @@ { "domain": "climacell", "name": "ClimaCell", - "config_flow": true, + "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/climacell", "requirements": ["pyclimacell==0.18.2"], + "after_dependencies": ["tomorrowio"], "codeowners": ["@raman325"], "iot_class": "cloud_polling", "loggers": ["pyclimacell"] diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 597e1095f89..4eb9dddb9c3 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -1,8 +1,6 @@ """Sensor component that handles additional ClimaCell data for your location.""" from __future__ import annotations -from abc import abstractmethod - from pyclimacell.const import CURRENT from homeassistant.components.sensor import SensorEntity @@ -13,12 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import ( - CC_SENSOR_TYPES, - CC_V3_SENSOR_TYPES, - DOMAIN, - ClimaCellSensorEntityDescription, -) +from .const import CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorEntityDescription async def async_setup_entry( @@ -28,24 +21,18 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_class: type[BaseClimaCellSensorEntity] - sensor_types: tuple[ClimaCellSensorEntityDescription, ...] - - if (api_version := config_entry.data[CONF_API_VERSION]) == 3: - api_class = ClimaCellV3SensorEntity - sensor_types = CC_V3_SENSOR_TYPES - else: - api_class = ClimaCellSensorEntity - sensor_types = CC_SENSOR_TYPES + api_version = config_entry.data[CONF_API_VERSION] entities = [ - api_class(hass, config_entry, coordinator, api_version, description) - for description in sensor_types + ClimaCellV3SensorEntity( + hass, config_entry, coordinator, api_version, description + ) + for description in CC_V3_SENSOR_TYPES ] async_add_entities(entities) -class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): - """Base ClimaCell sensor entity.""" +class ClimaCellV3SensorEntity(ClimaCellEntity, SensorEntity): + """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" entity_description: ClimaCellSensorEntityDescription @@ -72,15 +59,12 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): else description.unit_imperial ) - @property - @abstractmethod - def _state(self) -> str | int | float | None: - """Return the raw state.""" - @property def native_value(self) -> str | int | float | None: """Return the state.""" - state = self._state + state = self._get_cc_value( + self.coordinator.data[CURRENT], self.entity_description.key + ) if ( state is not None and not isinstance(state, str) @@ -102,23 +86,3 @@ class BaseClimaCellSensorEntity(ClimaCellEntity, SensorEntity): return self.entity_description.value_map(state).name.lower() # type: ignore[misc] return state - - -class ClimaCellSensorEntity(BaseClimaCellSensorEntity): - """Sensor entity that talks to ClimaCell v4 API to retrieve non-weather data.""" - - @property - def _state(self) -> str | int | float | None: - """Return the raw state.""" - return self._get_current_property(self.entity_description.key) - - -class ClimaCellV3SensorEntity(BaseClimaCellSensorEntity): - """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" - - @property - def _state(self) -> str | int | float | None: - """Return the raw state.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], self.entity_description.key - ) diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json index 7b6e01b8dd4..25ddee09dd0 100644 --- a/homeassistant/components/climacell/strings.json +++ b/homeassistant/components/climacell/strings.json @@ -1,24 +1,4 @@ { - "config": { - "step": { - "user": { - "description": "If [%key:common::config_flow::data::latitude%] and [%key:common::config_flow::data::longitude%] are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default.", - "data": { - "name": "[%key:common::config_flow::data::name%]", - "api_key": "[%key:common::config_flow::data::api_key%]", - "api_version": "API Version", - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "rate_limited": "Currently rate limited, please try again later." - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index 3e5cd436ba8..a35be85d5b2 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key", - "rate_limited": "Currently rate limited, please try again later.", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "api_key": "API Key", - "api_version": "API Version", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name" - }, - "description": "If Latitude and Longitude are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Update ClimaCell Options" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index e62ed4bab7c..0167cb72513 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -6,15 +6,7 @@ from collections.abc import Mapping from datetime import datetime from typing import Any, cast -from pyclimacell.const import ( - CURRENT, - DAILY, - FORECASTS, - HOURLY, - NOWCAST, - PrecipitationType, - WeatherCode, -) +from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -54,22 +46,6 @@ from .const import ( ATTR_CLOUD_COVER, ATTR_PRECIPITATION_TYPE, ATTR_WIND_GUST, - CC_ATTR_CLOUD_COVER, - CC_ATTR_CONDITION, - CC_ATTR_HUMIDITY, - CC_ATTR_OZONE, - CC_ATTR_PRECIPITATION, - CC_ATTR_PRECIPITATION_PROBABILITY, - CC_ATTR_PRECIPITATION_TYPE, - CC_ATTR_PRESSURE, - CC_ATTR_TEMPERATURE, - CC_ATTR_TEMPERATURE_HIGH, - CC_ATTR_TEMPERATURE_LOW, - CC_ATTR_TIMESTAMP, - CC_ATTR_VISIBILITY, - CC_ATTR_WIND_DIRECTION, - CC_ATTR_WIND_GUST, - CC_ATTR_WIND_SPEED, CC_V3_ATTR_CLOUD_COVER, CC_V3_ATTR_CONDITION, CC_V3_ATTR_HUMIDITY, @@ -88,12 +64,10 @@ from .const import ( CC_V3_ATTR_WIND_GUST, CC_V3_ATTR_WIND_SPEED, CLEAR_CONDITIONS, - CONDITIONS, CONDITIONS_V3, CONF_TIMESTEP, DEFAULT_FORECAST_TYPE, DOMAIN, - MAX_FORECASTS, ) @@ -105,10 +79,8 @@ async def async_setup_entry( """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] api_version = config_entry.data[CONF_API_VERSION] - - api_class = ClimaCellV3WeatherEntity if api_version == 3 else ClimaCellWeatherEntity entities = [ - api_class(config_entry, coordinator, api_version, forecast_type) + ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type) for forecast_type in (DAILY, HOURLY, NOWCAST) ] async_add_entities(entities) @@ -267,154 +239,6 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): return self._visibility -class ClimaCellWeatherEntity(BaseClimaCellWeatherEntity): - """Entity that talks to ClimaCell v4 API to retrieve weather data.""" - - _attr_temperature_unit = TEMP_FAHRENHEIT - - @staticmethod - def _translate_condition( - condition: int | str | None, sun_is_up: bool = True - ) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - if condition is None: - return None - # We won't guard here, instead we will fail hard - condition = WeatherCode(condition) - if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR): - if sun_is_up: - return CLEAR_CONDITIONS["day"] - return CLEAR_CONDITIONS["night"] - return CONDITIONS[condition] - - @property - def temperature(self): - """Return the platform temperature.""" - return self._get_current_property(CC_ATTR_TEMPERATURE) - - @property - def _pressure(self): - """Return the raw pressure.""" - return self._get_current_property(CC_ATTR_PRESSURE) - - @property - def humidity(self): - """Return the humidity.""" - return self._get_current_property(CC_ATTR_HUMIDITY) - - @property - def wind_gust(self): - """Return the wind gust speed.""" - return self._get_current_property(CC_ATTR_WIND_GUST) - - @property - def cloud_cover(self): - """Return the cloud cover.""" - return self._get_current_property(CC_ATTR_CLOUD_COVER) - - @property - def precipitation_type(self): - """Return precipitation type.""" - precipitation_type = self._get_current_property(CC_ATTR_PRECIPITATION_TYPE) - if precipitation_type is None: - return None - return PrecipitationType(precipitation_type).name.lower() - - @property - def _wind_speed(self): - """Return the raw wind speed.""" - return self._get_current_property(CC_ATTR_WIND_SPEED) - - @property - def wind_bearing(self): - """Return the wind bearing.""" - return self._get_current_property(CC_ATTR_WIND_DIRECTION) - - @property - def ozone(self): - """Return the O3 (ozone) level.""" - return self._get_current_property(CC_ATTR_OZONE) - - @property - def condition(self): - """Return the condition.""" - return self._translate_condition( - self._get_current_property(CC_ATTR_CONDITION), - is_up(self.hass), - ) - - @property - def _visibility(self): - """Return the raw visibility.""" - return self._get_current_property(CC_ATTR_VISIBILITY) - - @property - def forecast(self): - """Return the forecast.""" - # Check if forecasts are available - raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) - if not raw_forecasts: - return None - - forecasts = [] - max_forecasts = MAX_FORECASTS[self.forecast_type] - forecast_count = 0 - - # Set default values (in cases where keys don't exist), None will be - # returned. Override properties per forecast type as needed - for forecast in raw_forecasts: - forecast_dt = dt_util.parse_datetime(forecast[CC_ATTR_TIMESTAMP]) - - # Throw out past data - if forecast_dt.date() < dt_util.utcnow().date(): - continue - - values = forecast["values"] - use_datetime = True - - condition = values.get(CC_ATTR_CONDITION) - precipitation = values.get(CC_ATTR_PRECIPITATION) - precipitation_probability = values.get(CC_ATTR_PRECIPITATION_PROBABILITY) - - temp = values.get(CC_ATTR_TEMPERATURE_HIGH) - temp_low = None - wind_direction = values.get(CC_ATTR_WIND_DIRECTION) - wind_speed = values.get(CC_ATTR_WIND_SPEED) - - if self.forecast_type == DAILY: - use_datetime = False - temp_low = values.get(CC_ATTR_TEMPERATURE_LOW) - if precipitation: - precipitation = precipitation * 24 - elif self.forecast_type == NOWCAST: - # Precipitation is forecasted in CONF_TIMESTEP increments but in a - # per hour rate, so value needs to be converted to an amount. - if precipitation: - precipitation = ( - precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] - ) - - forecasts.append( - self._forecast_dict( - forecast_dt, - use_datetime, - condition, - precipitation, - precipitation_probability, - temp, - temp_low, - wind_direction, - wind_speed, - ) - ) - - forecast_count += 1 - if forecast_count == max_forecasts: - break - - return forecasts - - class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): """Entity that talks to ClimaCell v3 API to retrieve weather data.""" diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py new file mode 100644 index 00000000000..a46f9a9222e --- /dev/null +++ b/homeassistant/components/tomorrowio/__init__.py @@ -0,0 +1,351 @@ +"""The Tomorrow.io integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from math import ceil +from typing import Any + +from pytomorrowio import TomorrowioV4 +from pytomorrowio.const import CURRENT, FORECASTS +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DOMAIN, + INTEGRATION_NAME, + MAX_REQUESTS_PER_DAY, + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_CONDITION, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_WIND_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] + + +def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: + """Recalculate update_interval based on existing Tomorrow.io instances and update them.""" + api_calls = 2 + # We check how many Tomorrow.io configured instances are using the same API key and + # calculate interval to not exceed allowed numbers of requests. Divide 90% of + # MAX_REQUESTS_PER_DAY by the number of API calls because we want a buffer in the + # number of API calls left at the end of the day. + other_instance_entry_ids = [ + entry.entry_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id != current_entry.entry_id + and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY] + ] + + interval = timedelta( + minutes=( + ceil( + (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) + / (MAX_REQUESTS_PER_DAY * 0.9) + ) + ) + ) + + for entry_id in other_instance_entry_ids: + if entry_id in hass.data[DOMAIN]: + hass.data[DOMAIN][entry_id].update_interval = interval + + return interval + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Tomorrow.io API from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + # Let's precreate the device so that if this is a first time setup for a config + # entry imported from a ClimaCell entry, we can apply customizations from the old + # device. + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.data[CONF_API_KEY])}, + name=INTEGRATION_NAME, + manufacturer=INTEGRATION_NAME, + sw_version="v4", + entry_type=dr.DeviceEntryType.SERVICE, + ) + + # If this is an import and we still have the old config entry ID in the entry data, + # it means we are setting this entry up for the first time after a migration from + # ClimaCell to Tomorrow.io. In order to preserve any customizations on the ClimaCell + # entities, we need to remove each old entity, creating a new entity in its place + # but attached to this entry. + if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: + # Remove the old config entry ID from the entry data so we don't try this again + # on the next setup + data = entry.data.copy() + old_config_entry_id = data.pop("old_config_entry_id") + hass.config_entries.async_update_entry(entry, data=data) + _LOGGER.debug( + ( + "Setting up imported climacell entry %s for the first time as " + "tomorrowio entry %s" + ), + old_config_entry_id, + entry.entry_id, + ) + + ent_reg = er.async_get(hass) + for entity_entry in er.async_entries_for_config_entry( + ent_reg, old_config_entry_id + ): + _LOGGER.debug("Removing %s", entity_entry.entity_id) + ent_reg.async_remove(entity_entry.entity_id) + # In case the API key has changed due to a V3 -> V4 change, we need to + # generate the new entity's unique ID + new_unique_id = ( + f"{entry.data[CONF_API_KEY]}_" + f"{'_'.join(entity_entry.unique_id.split('_')[1:])}" + ) + _LOGGER.debug( + "Re-creating %s for the new config entry", entity_entry.entity_id + ) + # We will precreate the entity so that any customizations can be preserved + new_entity_entry = ent_reg.async_get_or_create( + entity_entry.domain, + DOMAIN, + new_unique_id, + suggested_object_id=entity_entry.entity_id.split(".")[1], + disabled_by=entity_entry.disabled_by, + config_entry=entry, + original_name=entity_entry.original_name, + original_icon=entity_entry.original_icon, + ) + _LOGGER.debug("Re-created %s", new_entity_entry.entity_id) + # If there are customizations on the old entity, apply them to the new one + if entity_entry.name or entity_entry.icon: + ent_reg.async_update_entity( + new_entity_entry.entity_id, + name=entity_entry.name, + icon=entity_entry.icon, + ) + + # We only have one device in the registry but we will do a loop just in case + for old_device in dr.async_entries_for_config_entry( + dev_reg, old_config_entry_id + ): + if old_device.name_by_user: + dev_reg.async_update_device( + device.id, name_by_user=old_device.name_by_user + ) + + # Remove the old config entry and now the entry is fully migrated + hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) + + api = TomorrowioV4( + entry.data[CONF_API_KEY], + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], + session=async_get_clientsession(hass), + ) + + coordinator = TomorrowioDataUpdateCoordinator( + hass, + entry, + api, + _set_update_interval(hass, entry), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok + + +class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Tomorrow.io data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + api: TomorrowioV4, + update_interval: timedelta, + ) -> None: + """Initialize.""" + + self._config_entry = config_entry + self._api = api + self.name = config_entry.data[CONF_NAME] + self.data = {CURRENT: {}, FORECASTS: {}} + + super().__init__( + hass, + _LOGGER, + name=config_entry.data[CONF_NAME], + update_interval=update_interval, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + return await self._api.realtime_and_all_forecasts( + [ + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_PRECIPITATION_TYPE, + *( + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_WIND_GUST, + ), + ], + [ + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + ], + nowcast_timestep=self._config_entry.options[CONF_TIMESTEP], + ) + except ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, + ) as error: + raise UpdateFailed from error + + +class TomorrowioEntity(CoordinatorEntity): + """Base Tomorrow.io Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + ) -> None: + """Initialize Tomorrow.io Entity.""" + super().__init__(coordinator) + self.api_version = api_version + self._config_entry = config_entry + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, + name="Tomorrow.io", + manufacturer="Tomorrow.io", + sw_version=f"v{self.api_version}", + entry_type=dr.DeviceEntryType.SERVICE, + ) + + def _get_current_property(self, property_name: str) -> int | str | float | None: + """ + Get property from current conditions. + + Used for V4 API. + """ + return self.coordinator.data.get(CURRENT, {}).get(property_name) + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py new file mode 100644 index 00000000000..ac8ceeaa9d5 --- /dev/null +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -0,0 +1,214 @@ +"""Config flow for Tomorrow.io integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, +) +from pytomorrowio.pytomorrowio import TomorrowioV4 +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.components.zone import async_active_zone +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_FRIENDLY_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import ( + AUTO_MIGRATION_MESSAGE, + CC_DOMAIN, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, + INTEGRATION_NAME, + MANUAL_MIGRATION_MESSAGE, + TMRW_ATTR_TEMPERATURE, +) + +_LOGGER = logging.getLogger(__name__) + + +def _get_config_schema( + hass: core.HomeAssistant, source: str | None, input_dict: dict[str, Any] = None +) -> vol.Schema: + """ + Return schema defaults for init step based on user input/config dict. + + Retain info already provided for future form views by setting them as + defaults in schema. + """ + if input_dict is None: + input_dict = {} + + api_key_schema = { + vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str, + } + + # For imports we just need to ask for the API key + if source == config_entries.SOURCE_IMPORT: + return vol.Schema(api_key_schema, extra=vol.REMOVE_EXTRA) + + return vol.Schema( + { + **api_key_schema, + vol.Required( + CONF_LATITUDE, + "location", + default=input_dict.get(CONF_LATITUDE, hass.config.latitude), + ): cv.latitude, + vol.Required( + CONF_LONGITUDE, + "location", + default=input_dict.get(CONF_LONGITUDE, hass.config.longitude), + ): cv.longitude, + }, + ) + + +def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]): + """Return unique ID from config data.""" + return ( + f"{input_dict[CONF_API_KEY]}" + f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}" + f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}" + ) + + +class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): + """Handle Tomorrow.io options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize Tomorrow.io options flow.""" + self._config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + """Manage the Tomorrow.io options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options_schema = { + vol.Required( + CONF_TIMESTEP, + default=self._config_entry.options[CONF_TIMESTEP], + ): vol.In([1, 5, 15, 30]), + } + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options_schema) + ) + + +class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tomorrow.io Weather API.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize config flow.""" + self._showed_import_message = 0 + self._import_config: dict[str, Any] | None = None + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> TomorrowioOptionsConfigFlow: + """Get the options flow for this handler.""" + return TomorrowioOptionsConfigFlow(config_entry) + + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + """Handle the initial step.""" + errors = {} + if user_input is not None: + # Grab the API key and add it to the rest of the config before continuing + if self._import_config: + self._import_config[CONF_API_KEY] = user_input[CONF_API_KEY] + user_input = self._import_config.copy() + await self.async_set_unique_id( + unique_id=_get_unique_id(self.hass, user_input) + ) + self._abort_if_unique_id_configured() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + if CONF_NAME not in user_input: + user_input[CONF_NAME] = DEFAULT_NAME + # Append zone name if it exists and we are using the default name + if zone_state := async_active_zone(self.hass, latitude, longitude): + zone_name = zone_state.attributes[CONF_FRIENDLY_NAME] + user_input[CONF_NAME] += f" - {zone_name}" + try: + await TomorrowioV4( + user_input[CONF_API_KEY], + str(latitude), + str(longitude), + session=async_get_clientsession(self.hass), + ).realtime([TMRW_ATTR_TEMPERATURE]) + except CantConnectException: + errors["base"] = "cannot_connect" + except InvalidAPIKeyException: + errors[CONF_API_KEY] = "invalid_api_key" + except RateLimitedException: + errors[CONF_API_KEY] = "rate_limited" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + options: Mapping[str, Any] = {CONF_TIMESTEP: DEFAULT_TIMESTEP} + # Store the old config entry ID and retrieve options to recreate the entry + if self.source == config_entries.SOURCE_IMPORT: + old_config_entry_id = self.context["old_config_entry_id"] + old_config_entry = self.hass.config_entries.async_get_entry( + old_config_entry_id + ) + assert old_config_entry + options = dict(old_config_entry.options) + user_input["old_config_entry_id"] = old_config_entry_id + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + options=options, + ) + + return self.async_show_form( + step_id="user", + data_schema=_get_config_schema(self.hass, self.source, user_input), + errors=errors, + ) + + async def async_step_import(self, import_config: dict) -> FlowResult: + """Import from config.""" + # Store import config for later + self._import_config = dict(import_config) + if self._import_config.pop(CONF_API_VERSION, 3) == 3: + # Clear API key from import config + self._import_config[CONF_API_KEY] = "" + self.hass.components.persistent_notification.async_create( + MANUAL_MIGRATION_MESSAGE, + INTEGRATION_NAME, + f"{CC_DOMAIN}_to_{DOMAIN}_new_api_key_needed", + ) + return await self.async_step_user() + + self.hass.components.persistent_notification.async_create( + AUTO_MIGRATION_MESSAGE, + INTEGRATION_NAME, + f"{CC_DOMAIN}_to_{DOMAIN}", + ) + return await self.async_step_user(self._import_config) diff --git a/homeassistant/components/tomorrowio/const.py b/homeassistant/components/tomorrowio/const.py new file mode 100644 index 00000000000..5f49700e511 --- /dev/null +++ b/homeassistant/components/tomorrowio/const.py @@ -0,0 +1,143 @@ +"""Constants for the Tomorrow.io integration.""" +from __future__ import annotations + +from pytomorrowio.const import DAILY, HOURLY, NOWCAST, WeatherCode + +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_CONDITION_WINDY, +) + +CONF_TIMESTEP = "timestep" +FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] + +DEFAULT_TIMESTEP = 15 +DEFAULT_FORECAST_TYPE = DAILY +CC_DOMAIN = "climacell" +DOMAIN = "tomorrowio" +INTEGRATION_NAME = "Tomorrow.io" +DEFAULT_NAME = INTEGRATION_NAME +ATTRIBUTION = "Powered by Tomorrow.io" + +MAX_REQUESTS_PER_DAY = 500 + +CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} + +MAX_FORECASTS = { + DAILY: 14, + HOURLY: 24, + NOWCAST: 30, +} + +# Additional attributes +ATTR_WIND_GUST = "wind_gust" +ATTR_CLOUD_COVER = "cloud_cover" +ATTR_PRECIPITATION_TYPE = "precipitation_type" + +# V4 constants +CONDITIONS = { + WeatherCode.WIND: ATTR_CONDITION_WINDY, + WeatherCode.LIGHT_WIND: ATTR_CONDITION_WINDY, + WeatherCode.STRONG_WIND: ATTR_CONDITION_WINDY, + WeatherCode.FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.HEAVY_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.LIGHT_FREEZING_RAIN: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.FREEZING_DRIZZLE: ATTR_CONDITION_SNOWY_RAINY, + WeatherCode.ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.HEAVY_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.LIGHT_ICE_PELLETS: ATTR_CONDITION_HAIL, + WeatherCode.SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.HEAVY_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.LIGHT_SNOW: ATTR_CONDITION_SNOWY, + WeatherCode.FLURRIES: ATTR_CONDITION_SNOWY, + WeatherCode.THUNDERSTORM: ATTR_CONDITION_LIGHTNING, + WeatherCode.RAIN: ATTR_CONDITION_POURING, + WeatherCode.HEAVY_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.LIGHT_RAIN: ATTR_CONDITION_RAINY, + WeatherCode.DRIZZLE: ATTR_CONDITION_RAINY, + WeatherCode.FOG: ATTR_CONDITION_FOG, + WeatherCode.LIGHT_FOG: ATTR_CONDITION_FOG, + WeatherCode.CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY, + WeatherCode.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY, +} + +# Weather constants +TMRW_ATTR_TIMESTAMP = "startTime" +TMRW_ATTR_TEMPERATURE = "temperature" +TMRW_ATTR_TEMPERATURE_HIGH = "temperatureMax" +TMRW_ATTR_TEMPERATURE_LOW = "temperatureMin" +TMRW_ATTR_PRESSURE = "pressureSeaLevel" +TMRW_ATTR_HUMIDITY = "humidity" +TMRW_ATTR_WIND_SPEED = "windSpeed" +TMRW_ATTR_WIND_DIRECTION = "windDirection" +TMRW_ATTR_OZONE = "pollutantO3" +TMRW_ATTR_CONDITION = "weatherCode" +TMRW_ATTR_VISIBILITY = "visibility" +TMRW_ATTR_PRECIPITATION = "precipitationIntensityAvg" +TMRW_ATTR_PRECIPITATION_PROBABILITY = "precipitationProbability" +TMRW_ATTR_WIND_GUST = "windGust" +TMRW_ATTR_CLOUD_COVER = "cloudCover" +TMRW_ATTR_PRECIPITATION_TYPE = "precipitationType" + +# Sensor attributes +TMRW_ATTR_PARTICULATE_MATTER_25 = "particulateMatter25" +TMRW_ATTR_PARTICULATE_MATTER_10 = "particulateMatter10" +TMRW_ATTR_NITROGEN_DIOXIDE = "pollutantNO2" +TMRW_ATTR_CARBON_MONOXIDE = "pollutantCO" +TMRW_ATTR_SULPHUR_DIOXIDE = "pollutantSO2" +TMRW_ATTR_EPA_AQI = "epaIndex" +TMRW_ATTR_EPA_PRIMARY_POLLUTANT = "epaPrimaryPollutant" +TMRW_ATTR_EPA_HEALTH_CONCERN = "epaHealthConcern" +TMRW_ATTR_CHINA_AQI = "mepIndex" +TMRW_ATTR_CHINA_PRIMARY_POLLUTANT = "mepPrimaryPollutant" +TMRW_ATTR_CHINA_HEALTH_CONCERN = "mepHealthConcern" +TMRW_ATTR_POLLEN_TREE = "treeIndex" +TMRW_ATTR_POLLEN_WEED = "weedIndex" +TMRW_ATTR_POLLEN_GRASS = "grassIndex" +TMRW_ATTR_FIRE_INDEX = "fireIndex" +TMRW_ATTR_FEELS_LIKE = "temperatureApparent" +TMRW_ATTR_DEW_POINT = "dewPoint" +TMRW_ATTR_PRESSURE_SURFACE_LEVEL = "pressureSurfaceLevel" +TMRW_ATTR_SOLAR_GHI = "solarGHI" +TMRW_ATTR_CLOUD_BASE = "cloudBase" +TMRW_ATTR_CLOUD_CEILING = "cloudCeiling" + +MANUAL_MIGRATION_MESSAGE = ( + "As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) " + "we will migrate your existing ClimaCell config entry (or config " + "entries) to the new Tomorrow.io integration, but because **the " + " V3 API is now deprecated**, you will need to get a new V4 API " + "key from [Tomorrow.io](https://app.tomorrow.io/development/keys)." + " Once that is done, visit the " + "[Integrations Configuration](/config/integrations) page and " + "click Configure on the Tomorrow.io card(s) to submit the new " + "key. Once your key has been validated, your config entry will " + "automatically be migrated. The new integration is a drop in " + "replacement and your existing entities will be migrated over, " + "just note that the location of the integration card on the " + "[Integrations Configuration](/config/integrations) page has changed " + "since the integration name has changed." +) + +AUTO_MIGRATION_MESSAGE = ( + "As part of [ClimaCell's rebranding to Tomorrow.io](https://www.tomorrow.io/blog/my-last-day-as-ceo-of-climacell/) " + "we have automatically migrated your existing ClimaCell config entry " + "(or as many of your ClimaCell config entries as we could) to the new " + "Tomorrow.io integration. There is nothing you need to do since the " + "new integration is a drop in replacement and your existing entities " + "have been migrated over, just note that the location of the " + "integration card on the " + "[Integrations Configuration](/config/integrations) page has changed " + "since the integration name has changed." +) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json new file mode 100644 index 00000000000..6fc8a2f12ce --- /dev/null +++ b/homeassistant/components/tomorrowio/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "tomorrowio", + "name": "Tomorrow.io", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tomorrowio", + "requirements": ["pytomorrowio==0.1.0"], + "codeowners": ["@raman325"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py new file mode 100644 index 00000000000..11e10f0d7f9 --- /dev/null +++ b/homeassistant/components/tomorrowio/sensor.py @@ -0,0 +1,365 @@ +"""Sensor component that handles additional Tomorrowio data for your location.""" +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from pytomorrowio.const import ( + HealthConcernType, + PollenIndex, + PrecipitationType, + PrimaryPollutantType, +) + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + CONF_NAME, + IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + IRRADIATION_WATTS_PER_SQUARE_METER, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PERCENTAGE, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify +from homeassistant.util.distance import convert as distance_convert +from homeassistant.util.pressure import convert as pressure_convert +from homeassistant.util.temperature import convert as temp_convert + +from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity +from .const import ( + DOMAIN, + TMRW_ATTR_CARBON_MONOXIDE, + TMRW_ATTR_CHINA_AQI, + TMRW_ATTR_CHINA_HEALTH_CONCERN, + TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + TMRW_ATTR_CLOUD_BASE, + TMRW_ATTR_CLOUD_CEILING, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_DEW_POINT, + TMRW_ATTR_EPA_AQI, + TMRW_ATTR_EPA_HEALTH_CONCERN, + TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + TMRW_ATTR_FEELS_LIKE, + TMRW_ATTR_FIRE_INDEX, + TMRW_ATTR_NITROGEN_DIOXIDE, + TMRW_ATTR_OZONE, + TMRW_ATTR_PARTICULATE_MATTER_10, + TMRW_ATTR_PARTICULATE_MATTER_25, + TMRW_ATTR_POLLEN_GRASS, + TMRW_ATTR_POLLEN_TREE, + TMRW_ATTR_POLLEN_WEED, + TMRW_ATTR_PRECIPITATION_TYPE, + TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + TMRW_ATTR_SOLAR_GHI, + TMRW_ATTR_SULPHUR_DIOXIDE, + TMRW_ATTR_WIND_GUST, +) + + +@dataclass +class TomorrowioSensorEntityDescription(SensorEntityDescription): + """Describes a Tomorrow.io sensor entity.""" + + unit_imperial: str | None = None + unit_metric: str | None = None + metric_conversion: Callable[[float], float] | float = 1.0 + is_metric_check: bool | None = None + value_map: Any | None = None + + +SENSOR_TYPES = ( + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_FEELS_LIKE, + name="Feels Like", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=SensorDeviceClass.TEMPERATURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_DEW_POINT, + name="Dew Point", + unit_imperial=TEMP_FAHRENHEIT, + unit_metric=TEMP_CELSIUS, + metric_conversion=lambda val: temp_convert(val, TEMP_FAHRENHEIT, TEMP_CELSIUS), + is_metric_check=True, + device_class=SensorDeviceClass.TEMPERATURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, + name="Pressure (Surface Level)", + unit_metric=PRESSURE_HPA, + metric_conversion=lambda val: pressure_convert( + val, PRESSURE_INHG, PRESSURE_HPA + ), + is_metric_check=True, + device_class=SensorDeviceClass.PRESSURE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_SOLAR_GHI, + name="Global Horizontal Irradiance", + unit_imperial=IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, + unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, + metric_conversion=3.15459, + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_BASE, + name="Cloud Base", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_CEILING, + name="Cloud Ceiling", + unit_imperial=LENGTH_MILES, + unit_metric=LENGTH_KILOMETERS, + metric_conversion=lambda val: distance_convert( + val, LENGTH_MILES, LENGTH_KILOMETERS + ), + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CLOUD_COVER, + name="Cloud Cover", + unit_imperial=PERCENTAGE, + unit_metric=PERCENTAGE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_WIND_GUST, + name="Wind Gust", + unit_imperial=SPEED_MILES_PER_HOUR, + unit_metric=SPEED_METERS_PER_SECOND, + metric_conversion=lambda val: distance_convert(val, LENGTH_MILES, LENGTH_METERS) + / 3600, + is_metric_check=True, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PRECIPITATION_TYPE, + name="Precipitation Type", + value_map=PrecipitationType, + device_class="tomorrowio__precipitation_type", + icon="mdi:weather-snowy-rainy", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_OZONE, + name="Ozone", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=2.03, + is_metric_check=True, + device_class=SensorDeviceClass.OZONE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PARTICULATE_MATTER_25, + name="Particulate Matter < 2.5 μm", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399**3, + is_metric_check=True, + device_class=SensorDeviceClass.PM25, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_PARTICULATE_MATTER_10, + name="Particulate Matter < 10 μm", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=3.2808399**3, + is_metric_check=True, + device_class=SensorDeviceClass.PM10, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_NITROGEN_DIOXIDE, + name="Nitrogen Dioxide", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=1.95, + is_metric_check=True, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CARBON_MONOXIDE, + name="Carbon Monoxide", + unit_imperial=CONCENTRATION_PARTS_PER_MILLION, + unit_metric=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_SULPHUR_DIOXIDE, + name="Sulphur Dioxide", + unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + metric_conversion=2.71, + is_metric_check=True, + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_AQI, + name="US EPA Air Quality Index", + device_class=SensorDeviceClass.AQI, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_PRIMARY_POLLUTANT, + name="US EPA Primary Pollutant", + value_map=PrimaryPollutantType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_EPA_HEALTH_CONCERN, + name="US EPA Health Concern", + value_map=HealthConcernType, + device_class="tomorrowio__health_concern", + icon="mdi:hospital", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_AQI, + name="China MEP Air Quality Index", + device_class=SensorDeviceClass.AQI, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_PRIMARY_POLLUTANT, + name="China MEP Primary Pollutant", + value_map=PrimaryPollutantType, + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_CHINA_HEALTH_CONCERN, + name="China MEP Health Concern", + value_map=HealthConcernType, + device_class="tomorrowio__health_concern", + icon="mdi:hospital", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_TREE, + name="Tree Pollen Index", + value_map=PollenIndex, + device_class="tomorrowio__pollen_index", + icon="mdi:flower-pollen", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_WEED, + name="Weed Pollen Index", + value_map=PollenIndex, + device_class="tomorrowio__pollen_index", + icon="mdi:flower-pollen", + ), + TomorrowioSensorEntityDescription( + key=TMRW_ATTR_POLLEN_GRASS, + name="Grass Pollen Index", + value_map=PollenIndex, + device_class="tomorrowio__pollen_index", + icon="mdi:flower-pollen", + ), + TomorrowioSensorEntityDescription( + TMRW_ATTR_FIRE_INDEX, + name="Fire Index", + icon="mdi:fire", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + entities = [ + TomorrowioSensorEntity(hass, config_entry, coordinator, 4, description) + for description in SENSOR_TYPES + ] + async_add_entities(entities) + + +class BaseTomorrowioSensorEntity(TomorrowioEntity, SensorEntity): + """Base Tomorrow.io sensor entity.""" + + entity_description: TomorrowioSensorEntityDescription + _attr_entity_registry_enabled_default = False + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + description: TomorrowioSensorEntityDescription, + ) -> None: + """Initialize Tomorrow.io Sensor Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.entity_description = description + self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" + self._attr_unique_id = ( + f"{self._config_entry.unique_id}_{slugify(description.name)}" + ) + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} + # Fallback to metric always in case imperial isn't defined (for metric only + # sensors) + self._attr_native_unit_of_measurement = ( + description.unit_metric + if hass.config.units.is_metric + else description.unit_imperial + ) or description.unit_metric + + @property + @abstractmethod + def _state(self) -> str | int | float | None: + """Return the raw state.""" + + @property + def native_value(self) -> str | int | float | None: + """Return the state.""" + state = self._state + + # If an imperial unit isn't provided, we always want to convert to metric since + # that is what the UI expects + if state is not None and ( + ( + self.entity_description.metric_conversion != 1.0 + and self.entity_description.is_metric_check is not None + and self.hass.config.units.is_metric + == self.entity_description.is_metric_check + ) + or ( + self.entity_description.unit_imperial is None + and self.entity_description.unit_metric is not None + ) + ): + conversion = self.entity_description.metric_conversion + # When conversion is a callable, we assume it's a single input function + if callable(conversion): + return round(conversion(float(state)), 2) + + return round(float(state) * conversion, 2) + + if self.entity_description.value_map is not None and state is not None: + return self.entity_description.value_map(state).name.lower() + + return state + + +class TomorrowioSensorEntity(BaseTomorrowioSensorEntity): + """Sensor entity that talks to Tomorrow.io v4 API to retrieve non-weather data.""" + + @property + def _state(self) -> str | int | float | None: + """Return the raw state.""" + return self._get_current_property(self.entity_description.key) diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json new file mode 100644 index 00000000000..b681dc4d043 --- /dev/null +++ b/homeassistant/components/tomorrowio/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user": { + "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup).", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "rate_limited": "Currently rate limited, please try again later." + } + }, + "options": { + "step": { + "init": { + "title": "Update Tomorrow.io Options", + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "data": { + "timestep": "Min. Between NowCast Forecasts" + } + } + } + } +} diff --git a/homeassistant/components/tomorrowio/strings.sensor.json b/homeassistant/components/tomorrowio/strings.sensor.json new file mode 100644 index 00000000000..38535791522 --- /dev/null +++ b/homeassistant/components/tomorrowio/strings.sensor.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__pollen_index": { + "none": "None", + "very_low": "Very Low", + "low": "Low", + "medium": "Medium", + "high": "High", + "very_high": "Very High" + }, + "tomorrowio__health_concern": { + "good": "Good", + "moderate": "Moderate", + "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", + "unhealthy": "Unhealthy", + "very_unhealthy": "Very Unhealthy", + "hazardous": "Hazardous" + }, + "tomorrowio__precipitation_type": { + "none": "None", + "rain": "Rain", + "snow": "Snow", + "freezing_rain": "Freezing Rain", + "ice_pellets": "Ice Pellets" + } + } + } \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json new file mode 100644 index 00000000000..7c653b00574 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "rate_limited": "Currently rate limited, please try again later.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Between NowCast Forecasts" + }, + "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", + "title": "Update Tomorrow.io Options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.en.json b/homeassistant/components/tomorrowio/translations/sensor.en.json new file mode 100644 index 00000000000..52b767dec3c --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.en.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Good", + "hazardous": "Hazardous", + "moderate": "Moderate", + "unhealthy": "Unhealthy", + "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", + "very_unhealthy": "Very Unhealthy" + }, + "tomorrowio__pollen_index": { + "high": "High", + "low": "Low", + "medium": "Medium", + "none": "None", + "very_high": "Very High", + "very_low": "Very Low" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Freezing Rain", + "ice_pellets": "Ice Pellets", + "none": "None", + "rain": "Rain", + "snow": "Snow" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py new file mode 100644 index 00000000000..3006be989ba --- /dev/null +++ b/homeassistant/components/tomorrowio/weather.py @@ -0,0 +1,253 @@ +"""Weather component that handles meteorological data for your location.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + WeatherEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + LENGTH_INCHES, + LENGTH_MILES, + PRESSURE_INHG, + SPEED_MILES_PER_HOUR, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sun import is_up +from homeassistant.util import dt as dt_util + +from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity +from .const import ( + CLEAR_CONDITIONS, + CONDITIONS, + CONF_TIMESTEP, + DEFAULT_FORECAST_TYPE, + DOMAIN, + MAX_FORECASTS, + TMRW_ATTR_CONDITION, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TIMESTAMP, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_WIND_SPEED, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + entities = [ + TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type) + for forecast_type in (DAILY, HOURLY, NOWCAST) + ] + async_add_entities(entities) + + +class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): + """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" + + _attr_temperature_unit = TEMP_FAHRENHEIT + _attr_pressure_unit = PRESSURE_INHG + _attr_wind_speed_unit = SPEED_MILES_PER_HOUR + _attr_visibility_unit = LENGTH_MILES + _attr_precipitation_unit = LENGTH_INCHES + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: TomorrowioDataUpdateCoordinator, + api_version: int, + forecast_type: str, + ) -> None: + """Initialize Tomorrow.io Weather Entity.""" + super().__init__(config_entry, coordinator, api_version) + self.forecast_type = forecast_type + self._attr_entity_registry_enabled_default = ( + forecast_type == DEFAULT_FORECAST_TYPE + ) + self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}" + self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}" + + def _forecast_dict( + self, + forecast_dt: datetime, + use_datetime: bool, + condition: int, + precipitation: float | None, + precipitation_probability: float | None, + temp: float | None, + temp_low: float | None, + wind_direction: float | None, + wind_speed: float | None, + ) -> dict[str, Any]: + """Return formatted Forecast dict from Tomorrow.io forecast data.""" + if use_datetime: + translated_condition = self._translate_condition( + condition, is_up(self.hass, forecast_dt) + ) + else: + translated_condition = self._translate_condition(condition, True) + + data = { + ATTR_FORECAST_TIME: forecast_dt.isoformat(), + ATTR_FORECAST_CONDITION: translated_condition, + ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, + ATTR_FORECAST_TEMP: temp, + ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_WIND_BEARING: wind_direction, + ATTR_FORECAST_WIND_SPEED: wind_speed, + } + + return {k: v for k, v in data.items() if v is not None} + + @staticmethod + def _translate_condition( + condition: int | None, sun_is_up: bool = True + ) -> str | None: + """Translate Tomorrow.io condition into an HA condition.""" + if condition is None: + return None + # We won't guard here, instead we will fail hard + condition = WeatherCode(condition) + if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR): + if sun_is_up: + return CLEAR_CONDITIONS["day"] + return CLEAR_CONDITIONS["night"] + return CONDITIONS[condition] + + @property + def temperature(self): + """Return the platform temperature.""" + return self._get_current_property(TMRW_ATTR_TEMPERATURE) + + @property + def pressure(self): + """Return the raw pressure.""" + return self._get_current_property(TMRW_ATTR_PRESSURE) + + @property + def humidity(self): + """Return the humidity.""" + return self._get_current_property(TMRW_ATTR_HUMIDITY) + + @property + def wind_speed(self): + """Return the raw wind speed.""" + return self._get_current_property(TMRW_ATTR_WIND_SPEED) + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._get_current_property(TMRW_ATTR_WIND_DIRECTION) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self._get_current_property(TMRW_ATTR_OZONE) + + @property + def condition(self): + """Return the condition.""" + return self._translate_condition( + self._get_current_property(TMRW_ATTR_CONDITION), + is_up(self.hass), + ) + + @property + def visibility(self): + """Return the raw visibility.""" + return self._get_current_property(TMRW_ATTR_VISIBILITY) + + @property + def forecast(self): + """Return the forecast.""" + # Check if forecasts are available + raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + if not raw_forecasts: + return None + + forecasts = [] + max_forecasts = MAX_FORECASTS[self.forecast_type] + forecast_count = 0 + + # Set default values (in cases where keys don't exist), None will be + # returned. Override properties per forecast type as needed + for forecast in raw_forecasts: + forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP]) + + # Throw out past data + if forecast_dt.date() < dt_util.utcnow().date(): + continue + + values = forecast["values"] + use_datetime = True + + condition = values.get(TMRW_ATTR_CONDITION) + precipitation = values.get(TMRW_ATTR_PRECIPITATION) + precipitation_probability = values.get(TMRW_ATTR_PRECIPITATION_PROBABILITY) + + temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH) + temp_low = None + wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION) + wind_speed = values.get(TMRW_ATTR_WIND_SPEED) + + if self.forecast_type == DAILY: + use_datetime = False + temp_low = values.get(TMRW_ATTR_TEMPERATURE_LOW) + if precipitation: + precipitation = precipitation * 24 + elif self.forecast_type == NOWCAST: + # Precipitation is forecasted in CONF_TIMESTEP increments but in a + # per hour rate, so value needs to be converted to an amount. + if precipitation: + precipitation = ( + precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] + ) + + forecasts.append( + self._forecast_dict( + forecast_dt, + use_datetime, + condition, + precipitation, + precipitation_probability, + temp, + temp_low, + wind_direction, + wind_speed, + ) + ) + + forecast_count += 1 + if forecast_count == max_forecasts: + break + + return forecasts diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a96acda5824..3e6b8e10547 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -55,7 +55,6 @@ FLOWS = [ "canary", "cast", "cert_expiry", - "climacell", "cloudflare", "co2signal", "coinbase", @@ -341,6 +340,7 @@ FLOWS = [ "tibber", "tile", "tolo", + "tomorrowio", "toon", "totalconnect", "tplink", diff --git a/requirements_all.txt b/requirements_all.txt index 52ad3feb66a..25510ad8f59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1944,6 +1944,9 @@ pythonegardia==1.0.40 # homeassistant.components.tile pytile==2022.02.0 +# homeassistant.components.tomorrowio +pytomorrowio==0.1.0 + # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c68b9fcbc4..6ec5de4531f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1244,6 +1244,9 @@ python_awair==0.2.1 # homeassistant.components.tile pytile==2022.02.0 +# homeassistant.components.tomorrowio +pytomorrowio==0.1.0 + # homeassistant.components.traccar pytraccar==0.10.0 diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py index 88640c69c14..f762dc8d6f9 100644 --- a/tests/components/climacell/conftest.py +++ b/tests/components/climacell/conftest.py @@ -7,36 +7,20 @@ import pytest from tests.common import load_fixture -@pytest.fixture(name="climacell_config_flow_connect", autouse=True) -def climacell_config_flow_connect(): - """Mock valid climacell config flow setup.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV3.realtime", - return_value={}, - ), patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - return_value={}, - ): - yield - - @pytest.fixture(name="climacell_config_entry_update") def climacell_config_entry_update_fixture(): """Mock valid climacell config entry setup.""" with patch( "homeassistant.components.climacell.ClimaCellV3.realtime", - return_value=json.loads(load_fixture("climacell/v3_realtime.json")), + return_value=json.loads(load_fixture("v3_realtime.json", "climacell")), ), patch( "homeassistant.components.climacell.ClimaCellV3.forecast_hourly", - return_value=json.loads(load_fixture("climacell/v3_forecast_hourly.json")), + return_value=json.loads(load_fixture("v3_forecast_hourly.json", "climacell")), ), patch( "homeassistant.components.climacell.ClimaCellV3.forecast_daily", - return_value=json.loads(load_fixture("climacell/v3_forecast_daily.json")), + return_value=json.loads(load_fixture("v3_forecast_daily.json", "climacell")), ), patch( "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast", - return_value=json.loads(load_fixture("climacell/v3_forecast_nowcast.json")), - ), patch( - "homeassistant.components.climacell.ClimaCellV4.realtime_and_all_forecasts", - return_value=json.loads(load_fixture("climacell/v4.json")), + return_value=json.loads(load_fixture("v3_forecast_nowcast.json", "climacell")), ): yield diff --git a/tests/components/climacell/const.py b/tests/components/climacell/const.py index be933ecde29..88a9cbd54cd 100644 --- a/tests/components/climacell/const.py +++ b/tests/components/climacell/const.py @@ -10,17 +10,6 @@ from homeassistant.const import ( API_KEY = "aa" -MIN_CONFIG = { - CONF_API_KEY: API_KEY, -} - -V1_ENTRY_DATA = { - CONF_NAME: "ClimaCell", - CONF_API_KEY: API_KEY, - CONF_LATITUDE: 80, - CONF_LONGITUDE: 80, -} - API_V3_ENTRY_DATA = { CONF_NAME: "ClimaCell", CONF_API_KEY: API_KEY, @@ -28,11 +17,3 @@ API_V3_ENTRY_DATA = { CONF_LONGITUDE: 80, CONF_API_VERSION: 3, } - -API_V4_ENTRY_DATA = { - CONF_NAME: "ClimaCell", - CONF_API_KEY: API_KEY, - CONF_LATITUDE: 80, - CONF_LONGITUDE: 80, - CONF_API_VERSION: 4, -} diff --git a/tests/components/climacell/fixtures/v4.json b/tests/components/climacell/fixtures/v4.json deleted file mode 100644 index 02f76ab7d27..00000000000 --- a/tests/components/climacell/fixtures/v4.json +++ /dev/null @@ -1,2384 +0,0 @@ -{ - "current": { - "temperature": 44.13, - "humidity": 22.71, - "pressureSeaLevel": 30.35, - "windSpeed": 9.33, - "windDirection": 315.14, - "weatherCode": 1000, - "visibility": 8.15, - "pollutantO3": 46.53, - "windGust": 12.64, - "cloudCover": 100, - "precipitationType": 1, - "particulateMatter25": 0.15, - "particulateMatter10": 0.57, - "pollutantNO2": 10.67, - "pollutantCO": 0.63, - "pollutantSO2": 1.65, - "epaIndex": 24, - "epaPrimaryPollutant": 0, - "epaHealthConcern": 0, - "mepIndex": 23, - "mepPrimaryPollutant": 1, - "mepHealthConcern": 0, - "treeIndex": 0, - "weedIndex": 0, - "grassIndex": 0, - "fireIndex": 10, - "temperatureApparent": 101.3, - "dewPoint": 72.82, - "pressureSurfaceLevel": 29.47, - "solarGHI": 0, - "cloudBase": 0.74, - "cloudCeiling": 0.74 - }, - "forecasts": { - "nowcast": [ - { - "startTime": "2021-03-07T17:48:00Z", - "values": { - "temperatureMin": 44.13, - "temperatureMax": 44.13, - "windSpeed": 9.33, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T17:53:00Z", - "values": { - "temperatureMin": 43.9, - "temperatureMax": 43.9, - "windSpeed": 9.31, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T17:58:00Z", - "values": { - "temperatureMin": 43.68, - "temperatureMax": 43.68, - "windSpeed": 9.28, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:03:00Z", - "values": { - "temperatureMin": 43.66, - "temperatureMax": 43.66, - "windSpeed": 9.26, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:08:00Z", - "values": { - "temperatureMin": 43.79, - "temperatureMax": 43.79, - "windSpeed": 9.22, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:13:00Z", - "values": { - "temperatureMin": 43.92, - "temperatureMax": 43.92, - "windSpeed": 9.17, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:18:00Z", - "values": { - "temperatureMin": 44.04, - "temperatureMax": 44.04, - "windSpeed": 9.13, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:23:00Z", - "values": { - "temperatureMin": 44.17, - "temperatureMax": 44.17, - "windSpeed": 9.06, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:28:00Z", - "values": { - "temperatureMin": 44.31, - "temperatureMax": 44.31, - "windSpeed": 9.02, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:33:00Z", - "values": { - "temperatureMin": 44.44, - "temperatureMax": 44.44, - "windSpeed": 8.97, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:38:00Z", - "values": { - "temperatureMin": 44.56, - "temperatureMax": 44.56, - "windSpeed": 8.93, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:43:00Z", - "values": { - "temperatureMin": 44.69, - "temperatureMax": 44.69, - "windSpeed": 8.88, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:48:00Z", - "values": { - "temperatureMin": 44.82, - "temperatureMax": 44.82, - "windSpeed": 8.84, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:53:00Z", - "values": { - "temperatureMin": 44.94, - "temperatureMax": 44.94, - "windSpeed": 8.79, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:58:00Z", - "values": { - "temperatureMin": 45.07, - "temperatureMax": 45.07, - "windSpeed": 8.75, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:03:00Z", - "values": { - "temperatureMin": 45.16, - "temperatureMax": 45.16, - "windSpeed": 8.75, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:08:00Z", - "values": { - "temperatureMin": 45.23, - "temperatureMax": 45.23, - "windSpeed": 8.75, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:13:00Z", - "values": { - "temperatureMin": 45.28, - "temperatureMax": 45.28, - "windSpeed": 8.77, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:18:00Z", - "values": { - "temperatureMin": 45.36, - "temperatureMax": 45.36, - "windSpeed": 8.79, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:23:00Z", - "values": { - "temperatureMin": 45.43, - "temperatureMax": 45.43, - "windSpeed": 8.81, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:28:00Z", - "values": { - "temperatureMin": 45.5, - "temperatureMax": 45.5, - "windSpeed": 8.81, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:33:00Z", - "values": { - "temperatureMin": 45.55, - "temperatureMax": 45.55, - "windSpeed": 8.84, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:38:00Z", - "values": { - "temperatureMin": 45.63, - "temperatureMax": 45.63, - "windSpeed": 8.86, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:43:00Z", - "values": { - "temperatureMin": 45.7, - "temperatureMax": 45.7, - "windSpeed": 8.88, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:48:00Z", - "values": { - "temperatureMin": 45.75, - "temperatureMax": 45.75, - "windSpeed": 8.9, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:53:00Z", - "values": { - "temperatureMin": 45.82, - "temperatureMax": 45.82, - "windSpeed": 8.9, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:58:00Z", - "values": { - "temperatureMin": 45.9, - "temperatureMax": 45.9, - "windSpeed": 8.93, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:03:00Z", - "values": { - "temperatureMin": 45.88, - "temperatureMax": 45.88, - "windSpeed": 8.97, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:08:00Z", - "values": { - "temperatureMin": 45.82, - "temperatureMax": 45.82, - "windSpeed": 9.02, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:13:00Z", - "values": { - "temperatureMin": 45.75, - "temperatureMax": 45.75, - "windSpeed": 9.06, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:18:00Z", - "values": { - "temperatureMin": 45.7, - "temperatureMax": 45.7, - "windSpeed": 9.1, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:23:00Z", - "values": { - "temperatureMin": 45.63, - "temperatureMax": 45.63, - "windSpeed": 9.15, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:28:00Z", - "values": { - "temperatureMin": 45.57, - "temperatureMax": 45.57, - "windSpeed": 9.19, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:33:00Z", - "values": { - "temperatureMin": 45.5, - "temperatureMax": 45.5, - "windSpeed": 9.24, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:38:00Z", - "values": { - "temperatureMin": 45.45, - "temperatureMax": 45.45, - "windSpeed": 9.28, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:43:00Z", - "values": { - "temperatureMin": 45.39, - "temperatureMax": 45.39, - "windSpeed": 9.33, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:48:00Z", - "values": { - "temperatureMin": 45.32, - "temperatureMax": 45.32, - "windSpeed": 9.37, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:53:00Z", - "values": { - "temperatureMin": 45.27, - "temperatureMax": 45.27, - "windSpeed": 9.42, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:58:00Z", - "values": { - "temperatureMin": 45.19, - "temperatureMax": 45.19, - "windSpeed": 9.46, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:03:00Z", - "values": { - "temperatureMin": 45.14, - "temperatureMax": 45.14, - "windSpeed": 9.4, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:08:00Z", - "values": { - "temperatureMin": 45.07, - "temperatureMax": 45.07, - "windSpeed": 9.24, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:13:00Z", - "values": { - "temperatureMin": 45.01, - "temperatureMax": 45.01, - "windSpeed": 9.08, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:18:00Z", - "values": { - "temperatureMin": 44.94, - "temperatureMax": 44.94, - "windSpeed": 8.95, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:23:00Z", - "values": { - "temperatureMin": 44.89, - "temperatureMax": 44.89, - "windSpeed": 8.79, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:28:00Z", - "values": { - "temperatureMin": 44.82, - "temperatureMax": 44.82, - "windSpeed": 8.63, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:33:00Z", - "values": { - "temperatureMin": 44.76, - "temperatureMax": 44.76, - "windSpeed": 8.5, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:38:00Z", - "values": { - "temperatureMin": 44.69, - "temperatureMax": 44.69, - "windSpeed": 8.34, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:43:00Z", - "values": { - "temperatureMin": 44.64, - "temperatureMax": 44.64, - "windSpeed": 8.19, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:48:00Z", - "values": { - "temperatureMin": 44.56, - "temperatureMax": 44.56, - "windSpeed": 8.05, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:53:00Z", - "values": { - "temperatureMin": 44.51, - "temperatureMax": 44.51, - "windSpeed": 7.9, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:58:00Z", - "values": { - "temperatureMin": 44.44, - "temperatureMax": 44.44, - "windSpeed": 7.74, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:03:00Z", - "values": { - "temperatureMin": 44.26, - "temperatureMax": 44.26, - "windSpeed": 7.47, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:08:00Z", - "values": { - "temperatureMin": 44.01, - "temperatureMax": 44.01, - "windSpeed": 7.14, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:13:00Z", - "values": { - "temperatureMin": 43.74, - "temperatureMax": 43.74, - "windSpeed": 6.78, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:18:00Z", - "values": { - "temperatureMin": 43.48, - "temperatureMax": 43.48, - "windSpeed": 6.44, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:23:00Z", - "values": { - "temperatureMin": 43.23, - "temperatureMax": 43.23, - "windSpeed": 6.08, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:28:00Z", - "values": { - "temperatureMin": 42.98, - "temperatureMax": 42.98, - "windSpeed": 5.75, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:33:00Z", - "values": { - "temperatureMin": 42.71, - "temperatureMax": 42.71, - "windSpeed": 5.39, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:38:00Z", - "values": { - "temperatureMin": 42.46, - "temperatureMax": 42.46, - "windSpeed": 5.06, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:43:00Z", - "values": { - "temperatureMin": 42.21, - "temperatureMax": 42.21, - "windSpeed": 4.7, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:48:00Z", - "values": { - "temperatureMin": 41.94, - "temperatureMax": 41.94, - "windSpeed": 4.36, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:53:00Z", - "values": { - "temperatureMin": 41.68, - "temperatureMax": 41.68, - "windSpeed": 4, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:58:00Z", - "values": { - "temperatureMin": 41.43, - "temperatureMax": 41.43, - "windSpeed": 3.67, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:03:00Z", - "values": { - "temperatureMin": 41.16, - "temperatureMax": 41.16, - "windSpeed": 3.6, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:08:00Z", - "values": { - "temperatureMin": 40.91, - "temperatureMax": 40.91, - "windSpeed": 3.76, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:13:00Z", - "values": { - "temperatureMin": 40.66, - "temperatureMax": 40.66, - "windSpeed": 3.91, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:18:00Z", - "values": { - "temperatureMin": 40.41, - "temperatureMax": 40.41, - "windSpeed": 4.05, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:23:00Z", - "values": { - "temperatureMin": 40.14, - "temperatureMax": 40.14, - "windSpeed": 4.21, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:28:00Z", - "values": { - "temperatureMin": 39.88, - "temperatureMax": 39.88, - "windSpeed": 4.36, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:33:00Z", - "values": { - "temperatureMin": 39.63, - "temperatureMax": 39.63, - "windSpeed": 4.5, - "windDirection": 295.94, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:38:00Z", - "values": { - "temperatureMin": 39.38, - "temperatureMax": 39.38, - "windSpeed": 4.65, - "windDirection": 295.94, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:43:00Z", - "values": { - "temperatureMin": 39.11, - "temperatureMax": 39.11, - "windSpeed": 4.79, - "windDirection": 295.94, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - } - ], - "hourly": [ - { - "startTime": "2021-03-07T17:48:00Z", - "values": { - "temperatureMin": 44.13, - "temperatureMax": 44.13, - "windSpeed": 9.33, - "windDirection": 315.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T18:48:00Z", - "values": { - "temperatureMin": 44.82, - "temperatureMax": 44.82, - "windSpeed": 8.84, - "windDirection": 321.71, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T19:48:00Z", - "values": { - "temperatureMin": 45.75, - "temperatureMax": 45.75, - "windSpeed": 8.9, - "windDirection": 323.38, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T20:48:00Z", - "values": { - "temperatureMin": 45.32, - "temperatureMax": 45.32, - "windSpeed": 9.37, - "windDirection": 318.43, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T21:48:00Z", - "values": { - "temperatureMin": 44.56, - "temperatureMax": 44.56, - "windSpeed": 8.05, - "windDirection": 320.9, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T22:48:00Z", - "values": { - "temperatureMin": 41.94, - "temperatureMax": 41.94, - "windSpeed": 4.36, - "windDirection": 322.11, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-07T23:48:00Z", - "values": { - "temperatureMin": 38.86, - "temperatureMax": 38.86, - "windSpeed": 4.94, - "windDirection": 295.94, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T00:48:00Z", - "values": { - "temperatureMin": 36.18, - "temperatureMax": 36.18, - "windSpeed": 5.59, - "windDirection": 11.94, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T01:48:00Z", - "values": { - "temperatureMin": 34.3, - "temperatureMax": 34.3, - "windSpeed": 5.57, - "windDirection": 13.68, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T02:48:00Z", - "values": { - "temperatureMin": 32.88, - "temperatureMax": 32.88, - "windSpeed": 5.41, - "windDirection": 14.93, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T03:48:00Z", - "values": { - "temperatureMin": 31.91, - "temperatureMax": 31.91, - "windSpeed": 4.61, - "windDirection": 26.07, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T04:48:00Z", - "values": { - "temperatureMin": 29.17, - "temperatureMax": 29.17, - "windSpeed": 2.59, - "windDirection": 51.27, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T05:48:00Z", - "values": { - "temperatureMin": 27.37, - "temperatureMax": 27.37, - "windSpeed": 3.31, - "windDirection": 343.25, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T06:48:00Z", - "values": { - "temperatureMin": 26.73, - "temperatureMax": 26.73, - "windSpeed": 4.27, - "windDirection": 341.46, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T07:48:00Z", - "values": { - "temperatureMin": 26.38, - "temperatureMax": 26.38, - "windSpeed": 3.53, - "windDirection": 322.34, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T08:48:00Z", - "values": { - "temperatureMin": 26.15, - "temperatureMax": 26.15, - "windSpeed": 3.65, - "windDirection": 294.69, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T09:48:00Z", - "values": { - "temperatureMin": 30.07, - "temperatureMax": 30.07, - "windSpeed": 3.2, - "windDirection": 325.32, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T10:48:00Z", - "values": { - "temperatureMin": 31.03, - "temperatureMax": 31.03, - "windSpeed": 2.84, - "windDirection": 322.27, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T11:48:00Z", - "values": { - "temperatureMin": 27.23, - "temperatureMax": 27.23, - "windSpeed": 5.59, - "windDirection": 310.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T12:48:00Z", - "values": { - "temperatureMin": 29.21, - "temperatureMax": 29.21, - "windSpeed": 7.05, - "windDirection": 324.8, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T13:48:00Z", - "values": { - "temperatureMin": 33.19, - "temperatureMax": 33.19, - "windSpeed": 6.46, - "windDirection": 335.16, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T14:48:00Z", - "values": { - "temperatureMin": 37.02, - "temperatureMax": 37.02, - "windSpeed": 5.88, - "windDirection": 324.49, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T15:48:00Z", - "values": { - "temperatureMin": 40.01, - "temperatureMax": 40.01, - "windSpeed": 5.55, - "windDirection": 310.68, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T16:48:00Z", - "values": { - "temperatureMin": 42.37, - "temperatureMax": 42.37, - "windSpeed": 5.46, - "windDirection": 304.18, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T17:48:00Z", - "values": { - "temperatureMin": 44.62, - "temperatureMax": 44.62, - "windSpeed": 4.99, - "windDirection": 301.19, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T18:48:00Z", - "values": { - "temperatureMin": 46.78, - "temperatureMax": 46.78, - "windSpeed": 4.72, - "windDirection": 295.05, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T19:48:00Z", - "values": { - "temperatureMin": 48.42, - "temperatureMax": 48.42, - "windSpeed": 4.81, - "windDirection": 287.4, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T20:48:00Z", - "values": { - "temperatureMin": 49.28, - "temperatureMax": 49.28, - "windSpeed": 4.74, - "windDirection": 282.48, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T21:48:00Z", - "values": { - "temperatureMin": 48.72, - "temperatureMax": 48.72, - "windSpeed": 2.51, - "windDirection": 268.74, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T22:48:00Z", - "values": { - "temperatureMin": 44.37, - "temperatureMax": 44.37, - "windSpeed": 3.56, - "windDirection": 180.04, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T23:48:00Z", - "values": { - "temperatureMin": 39.9, - "temperatureMax": 39.9, - "windSpeed": 4.68, - "windDirection": 177.89, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T00:48:00Z", - "values": { - "temperatureMin": 37.87, - "temperatureMax": 37.87, - "windSpeed": 5.21, - "windDirection": 197.47, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T01:48:00Z", - "values": { - "temperatureMin": 36.91, - "temperatureMax": 36.91, - "windSpeed": 5.46, - "windDirection": 209.77, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T02:48:00Z", - "values": { - "temperatureMin": 36.64, - "temperatureMax": 36.64, - "windSpeed": 6.11, - "windDirection": 210.14, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T03:48:00Z", - "values": { - "temperatureMin": 36.63, - "temperatureMax": 36.63, - "windSpeed": 6.4, - "windDirection": 216, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T04:48:00Z", - "values": { - "temperatureMin": 36.23, - "temperatureMax": 36.23, - "windSpeed": 6.22, - "windDirection": 223.92, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T05:48:00Z", - "values": { - "temperatureMin": 35.58, - "temperatureMax": 35.58, - "windSpeed": 5.75, - "windDirection": 229.68, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T06:48:00Z", - "values": { - "temperatureMin": 34.68, - "temperatureMax": 34.68, - "windSpeed": 5.21, - "windDirection": 235.24, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T07:48:00Z", - "values": { - "temperatureMin": 33.69, - "temperatureMax": 33.69, - "windSpeed": 4.81, - "windDirection": 237.24, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T08:48:00Z", - "values": { - "temperatureMin": 32.74, - "temperatureMax": 32.74, - "windSpeed": 4.52, - "windDirection": 239.35, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T09:48:00Z", - "values": { - "temperatureMin": 32.05, - "temperatureMax": 32.05, - "windSpeed": 4.32, - "windDirection": 245.68, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T10:48:00Z", - "values": { - "temperatureMin": 31.57, - "temperatureMax": 31.57, - "windSpeed": 4.14, - "windDirection": 248.11, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T11:48:00Z", - "values": { - "temperatureMin": 32.92, - "temperatureMax": 32.92, - "windSpeed": 4.32, - "windDirection": 249.54, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T12:48:00Z", - "values": { - "temperatureMin": 38.5, - "temperatureMax": 38.5, - "windSpeed": 4.7, - "windDirection": 253.3, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T13:48:00Z", - "values": { - "temperatureMin": 46.08, - "temperatureMax": 46.08, - "windSpeed": 4.41, - "windDirection": 258.49, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T14:48:00Z", - "values": { - "temperatureMin": 53.26, - "temperatureMax": 53.26, - "windSpeed": 4.9, - "windDirection": 260.49, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T15:48:00Z", - "values": { - "temperatureMin": 58.15, - "temperatureMax": 58.15, - "windSpeed": 5.55, - "windDirection": 261.29, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T16:48:00Z", - "values": { - "temperatureMin": 61.56, - "temperatureMax": 61.56, - "windSpeed": 6.35, - "windDirection": 264.3, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T17:48:00Z", - "values": { - "temperatureMin": 64, - "temperatureMax": 64, - "windSpeed": 6.6, - "windDirection": 257.54, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T18:48:00Z", - "values": { - "temperatureMin": 65.79, - "temperatureMax": 65.79, - "windSpeed": 6.96, - "windDirection": 253.12, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T19:48:00Z", - "values": { - "temperatureMin": 66.74, - "temperatureMax": 66.74, - "windSpeed": 6.8, - "windDirection": 259.46, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T20:48:00Z", - "values": { - "temperatureMin": 66.96, - "temperatureMax": 66.96, - "windSpeed": 6.33, - "windDirection": 294.25, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T21:48:00Z", - "values": { - "temperatureMin": 64.35, - "temperatureMax": 64.35, - "windSpeed": 3.91, - "windDirection": 279.37, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T22:48:00Z", - "values": { - "temperatureMin": 61.07, - "temperatureMax": 61.07, - "windSpeed": 3.65, - "windDirection": 218.19, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T23:48:00Z", - "values": { - "temperatureMin": 56.3, - "temperatureMax": 56.3, - "windSpeed": 4.09, - "windDirection": 208.3, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T00:48:00Z", - "values": { - "temperatureMin": 53.19, - "temperatureMax": 53.19, - "windSpeed": 4.21, - "windDirection": 216.42, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T01:48:00Z", - "values": { - "temperatureMin": 51.94, - "temperatureMax": 51.94, - "windSpeed": 3.38, - "windDirection": 257.19, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T02:48:00Z", - "values": { - "temperatureMin": 49.82, - "temperatureMax": 49.82, - "windSpeed": 2.71, - "windDirection": 288.85, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T03:48:00Z", - "values": { - "temperatureMin": 48.24, - "temperatureMax": 48.24, - "windSpeed": 2.8, - "windDirection": 334.41, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T04:48:00Z", - "values": { - "temperatureMin": 47.44, - "temperatureMax": 47.44, - "windSpeed": 2.26, - "windDirection": 342.01, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T05:48:00Z", - "values": { - "temperatureMin": 45.59, - "temperatureMax": 45.59, - "windSpeed": 2.35, - "windDirection": 2.43, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T06:48:00Z", - "values": { - "temperatureMin": 43.43, - "temperatureMax": 43.43, - "windSpeed": 2.3, - "windDirection": 336.56, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T07:48:00Z", - "values": { - "temperatureMin": 41.11, - "temperatureMax": 41.11, - "windSpeed": 2.71, - "windDirection": 4.41, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T08:48:00Z", - "values": { - "temperatureMin": 39.58, - "temperatureMax": 39.58, - "windSpeed": 3.4, - "windDirection": 21.26, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T09:48:00Z", - "values": { - "temperatureMin": 39.85, - "temperatureMax": 39.85, - "windSpeed": 3.31, - "windDirection": 22.76, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T10:48:00Z", - "values": { - "temperatureMin": 37.85, - "temperatureMax": 37.85, - "windSpeed": 4.03, - "windDirection": 29.3, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T11:48:00Z", - "values": { - "temperatureMin": 38.97, - "temperatureMax": 38.97, - "windSpeed": 3.15, - "windDirection": 21.82, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T12:48:00Z", - "values": { - "temperatureMin": 44.31, - "temperatureMax": 44.31, - "windSpeed": 3.53, - "windDirection": 14.25, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T13:48:00Z", - "values": { - "temperatureMin": 50.25, - "temperatureMax": 50.25, - "windSpeed": 2.82, - "windDirection": 42.41, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T14:48:00Z", - "values": { - "temperatureMin": 54.97, - "temperatureMax": 54.97, - "windSpeed": 2.53, - "windDirection": 87.81, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T15:48:00Z", - "values": { - "temperatureMin": 58.46, - "temperatureMax": 58.46, - "windSpeed": 3.09, - "windDirection": 125.82, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T16:48:00Z", - "values": { - "temperatureMin": 61.21, - "temperatureMax": 61.21, - "windSpeed": 4.03, - "windDirection": 157.54, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T17:48:00Z", - "values": { - "temperatureMin": 63.36, - "temperatureMax": 63.36, - "windSpeed": 5.21, - "windDirection": 166.66, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T18:48:00Z", - "values": { - "temperatureMin": 64.83, - "temperatureMax": 64.83, - "windSpeed": 6.93, - "windDirection": 189.24, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T19:48:00Z", - "values": { - "temperatureMin": 65.23, - "temperatureMax": 65.23, - "windSpeed": 8.95, - "windDirection": 194.58, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T20:48:00Z", - "values": { - "temperatureMin": 64.98, - "temperatureMax": 64.98, - "windSpeed": 9.4, - "windDirection": 193.22, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T21:48:00Z", - "values": { - "temperatureMin": 64.06, - "temperatureMax": 64.06, - "windSpeed": 8.55, - "windDirection": 186.39, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T22:48:00Z", - "values": { - "temperatureMin": 61.9, - "temperatureMax": 61.9, - "windSpeed": 7.49, - "windDirection": 171.81, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T23:48:00Z", - "values": { - "temperatureMin": 59.4, - "temperatureMax": 59.4, - "windSpeed": 7.54, - "windDirection": 165.51, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T00:48:00Z", - "values": { - "temperatureMin": 57.63, - "temperatureMax": 57.63, - "windSpeed": 8.12, - "windDirection": 171.94, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T01:48:00Z", - "values": { - "temperatureMin": 56.17, - "temperatureMax": 56.17, - "windSpeed": 8.7, - "windDirection": 176.84, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T02:48:00Z", - "values": { - "temperatureMin": 55.36, - "temperatureMax": 55.36, - "windSpeed": 9.42, - "windDirection": 184.14, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T03:48:00Z", - "values": { - "temperatureMin": 54.88, - "temperatureMax": 54.88, - "windSpeed": 10, - "windDirection": 195.54, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T04:48:00Z", - "values": { - "temperatureMin": 54.14, - "temperatureMax": 54.14, - "windSpeed": 10.4, - "windDirection": 200.56, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T05:48:00Z", - "values": { - "temperatureMin": 53.46, - "temperatureMax": 53.46, - "windSpeed": 10.04, - "windDirection": 198.08, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T06:48:00Z", - "values": { - "temperatureMin": 52.11, - "temperatureMax": 52.11, - "windSpeed": 10.02, - "windDirection": 199.54, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T07:48:00Z", - "values": { - "temperatureMin": 51.64, - "temperatureMax": 51.64, - "windSpeed": 10.51, - "windDirection": 202.73, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T08:48:00Z", - "values": { - "temperatureMin": 50.79, - "temperatureMax": 50.79, - "windSpeed": 10.38, - "windDirection": 203.35, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T09:48:00Z", - "values": { - "temperatureMin": 49.93, - "temperatureMax": 49.93, - "windSpeed": 9.51, - "windDirection": 210.36, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T10:48:00Z", - "values": { - "temperatureMin": 49.1, - "temperatureMax": 49.1, - "windSpeed": 8.61, - "windDirection": 210.6, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T11:48:00Z", - "values": { - "temperatureMin": 48.42, - "temperatureMax": 48.42, - "windSpeed": 9.15, - "windDirection": 211.29, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T12:48:00Z", - "values": { - "temperatureMin": 48.9, - "temperatureMax": 48.9, - "windSpeed": 10.25, - "windDirection": 215.59, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T13:48:00Z", - "values": { - "temperatureMin": 50.54, - "temperatureMax": 50.54, - "windSpeed": 10.18, - "windDirection": 215.48, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T14:48:00Z", - "values": { - "temperatureMin": 53.19, - "temperatureMax": 53.19, - "windSpeed": 9.4, - "windDirection": 208.76, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T15:48:00Z", - "values": { - "temperatureMin": 56.19, - "temperatureMax": 56.19, - "windSpeed": 9.73, - "windDirection": 197.59, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T16:48:00Z", - "values": { - "temperatureMin": 59.34, - "temperatureMax": 59.34, - "windSpeed": 10.69, - "windDirection": 204.29, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T17:48:00Z", - "values": { - "temperatureMin": 62.35, - "temperatureMax": 62.35, - "windSpeed": 11.81, - "windDirection": 204.56, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T18:48:00Z", - "values": { - "temperatureMin": 64.6, - "temperatureMax": 64.6, - "windSpeed": 13.09, - "windDirection": 206.85, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T19:48:00Z", - "values": { - "temperatureMin": 65.91, - "temperatureMax": 65.91, - "windSpeed": 13.82, - "windDirection": 204.82, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T20:48:00Z", - "values": { - "temperatureMin": 66.22, - "temperatureMax": 66.22, - "windSpeed": 14.54, - "windDirection": 208.43, - "weatherCode": 1100, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T21:48:00Z", - "values": { - "temperatureMin": 65.46, - "temperatureMax": 65.46, - "windSpeed": 13.2, - "windDirection": 208.3, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T22:48:00Z", - "values": { - "temperatureMin": 64.35, - "temperatureMax": 64.35, - "windSpeed": 12.35, - "windDirection": 208.58, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T23:48:00Z", - "values": { - "temperatureMin": 62.85, - "temperatureMax": 62.85, - "windSpeed": 12.86, - "windDirection": 205.39, - "weatherCode": 1101, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-12T00:48:00Z", - "values": { - "temperatureMin": 61.75, - "temperatureMax": 61.75, - "windSpeed": 14.7, - "windDirection": 209.51, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-12T01:48:00Z", - "values": { - "temperatureMin": 61.2, - "temperatureMax": 61.2, - "windSpeed": 15.57, - "windDirection": 211.47, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-12T02:48:00Z", - "values": { - "temperatureMin": 60.46, - "temperatureMax": 60.46, - "windSpeed": 14.94, - "windDirection": 211.57, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-12T03:48:00Z", - "values": { - "temperatureMin": 59.94, - "temperatureMax": 59.94, - "windSpeed": 14.29, - "windDirection": 208.93, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-12T04:48:00Z", - "values": { - "temperatureMin": 59.52, - "temperatureMax": 59.52, - "windSpeed": 14.36, - "windDirection": 217.91, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - } - ], - "daily": [ - { - "startTime": "2021-03-07T11:00:00Z", - "values": { - "temperatureMin": 26.11, - "temperatureMax": 45.93, - "windSpeed": 9.49, - "windDirection": 239.6, - "weatherCode": 1000, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-08T11:00:00Z", - "values": { - "temperatureMin": 26.28, - "temperatureMax": 49.42, - "windSpeed": 7.24, - "windDirection": 262.82, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-09T11:00:00Z", - "values": { - "temperatureMin": 31.48, - "temperatureMax": 66.98, - "windSpeed": 7.05, - "windDirection": 229.3, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-10T11:00:00Z", - "values": { - "temperatureMin": 37.32, - "temperatureMax": 65.28, - "windSpeed": 10.64, - "windDirection": 149.91, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-11T11:00:00Z", - "values": { - "temperatureMin": 48.29, - "temperatureMax": 66.25, - "windSpeed": 15.69, - "windDirection": 210.45, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-12T11:00:00Z", - "values": { - "temperatureMin": 53.83, - "temperatureMax": 67.91, - "windSpeed": 12.3, - "windDirection": 217.98, - "weatherCode": 4000, - "precipitationIntensityAvg": 0.0002, - "precipitationProbability": 25 - } - }, - { - "startTime": "2021-03-13T11:00:00Z", - "values": { - "temperatureMin": 42.91, - "temperatureMax": 54.48, - "windSpeed": 9.72, - "windDirection": 58.79, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 25 - } - }, - { - "startTime": "2021-03-14T10:00:00Z", - "values": { - "temperatureMin": 33.35, - "temperatureMax": 42.91, - "windSpeed": 16.25, - "windDirection": 70.25, - "weatherCode": 5101, - "precipitationIntensityAvg": 0.0393, - "precipitationProbability": 95 - } - }, - { - "startTime": "2021-03-15T10:00:00Z", - "values": { - "temperatureMin": 29.35, - "temperatureMax": 43.67, - "windSpeed": 15.89, - "windDirection": 84.47, - "weatherCode": 5001, - "precipitationIntensityAvg": 0.0024, - "precipitationProbability": 55 - } - }, - { - "startTime": "2021-03-16T10:00:00Z", - "values": { - "temperatureMin": 29.1, - "temperatureMax": 43, - "windSpeed": 6.71, - "windDirection": 103.85, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-17T10:00:00Z", - "values": { - "temperatureMin": 34.32, - "temperatureMax": 52.4, - "windSpeed": 7.27, - "windDirection": 145.41, - "weatherCode": 1102, - "precipitationIntensityAvg": 0, - "precipitationProbability": 0 - } - }, - { - "startTime": "2021-03-18T10:00:00Z", - "values": { - "temperatureMin": 41.32, - "temperatureMax": 54.07, - "windSpeed": 6.58, - "windDirection": 62.99, - "weatherCode": 1001, - "precipitationIntensityAvg": 0, - "precipitationProbability": 10 - } - }, - { - "startTime": "2021-03-19T10:00:00Z", - "values": { - "temperatureMin": 39.4, - "temperatureMax": 48.94, - "windSpeed": 13.91, - "windDirection": 68.54, - "weatherCode": 4000, - "precipitationIntensityAvg": 0.0048, - "precipitationProbability": 55 - } - }, - { - "startTime": "2021-03-20T10:00:00Z", - "values": { - "temperatureMin": 35.06, - "temperatureMax": 40.12, - "windSpeed": 17.35, - "windDirection": 56.98, - "weatherCode": 5001, - "precipitationIntensityAvg": 0.002, - "precipitationProbability": 33.3 - } - }, - { - "startTime": "2021-03-21T10:00:00Z", - "values": { - "temperatureMin": 33.66, - "temperatureMax": 66.54, - "windSpeed": 15.93, - "windDirection": 82.57, - "weatherCode": 5001, - "precipitationIntensityAvg": 0.0004, - "precipitationProbability": 45 - } - } - ] - } -} \ No newline at end of file diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index 476f2ba3bee..9aa16b8a7c5 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -1,179 +1,27 @@ """Test the ClimaCell config flow.""" -from unittest.mock import patch - -from pyclimacell.exceptions import ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, -) - from homeassistant import data_entry_flow -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import ( CONF_TIMESTEP, - DEFAULT_NAME, DEFAULT_TIMESTEP, DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) from homeassistant.core import HomeAssistant -from .const import API_KEY, MIN_CONFIG +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry -async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: - """Test user config flow with minimum fields.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"][CONF_NAME] == DEFAULT_NAME - assert result["data"][CONF_API_KEY] == API_KEY - assert result["data"][CONF_API_VERSION] == 4 - assert result["data"][CONF_LATITUDE] == hass.config.latitude - assert result["data"][CONF_LONGITUDE] == hass.config.longitude - - -async def test_user_flow_v3(hass: HomeAssistant) -> None: - """Test user config flow with v3 API.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - data = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) - data[CONF_API_VERSION] = 3 - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=data, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"][CONF_NAME] == DEFAULT_NAME - assert result["data"][CONF_API_KEY] == API_KEY - assert result["data"][CONF_API_VERSION] == 3 - assert result["data"][CONF_LATITUDE] == hass.config.latitude - assert result["data"][CONF_LONGITUDE] == hass.config.longitude - - -async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: - """Test user config flow with the same unique ID as an existing entry.""" - user_input = _get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG) - MockConfigEntry( - domain=DOMAIN, - data=user_input, - source=SOURCE_USER, - unique_id=_get_unique_id(hass, user_input), - version=2, - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=user_input, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: - """Test user config flow when ClimaCell can't connect.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=CantConnectException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: - """Test user config flow when API key is invalid.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=InvalidAPIKeyException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} - - -async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: - """Test user config flow when API key is rate limited.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=RateLimitedException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_API_KEY: "rate_limited"} - - -async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: - """Test user config flow when unknown error occurs.""" - with patch( - "homeassistant.components.climacell.config_flow.ClimaCellV4.realtime", - side_effect=UnknownException, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=_get_config_schema(hass, MIN_CONFIG)(MIN_CONFIG), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - - -async def test_options_flow(hass: HomeAssistant) -> None: +async def test_options_flow( + hass: HomeAssistant, climacell_config_entry_update: None +) -> None: """Test options config flow for climacell.""" - user_config = _get_config_schema(hass)(MIN_CONFIG) entry = MockConfigEntry( domain=DOMAIN, - data=user_config, + data=API_V3_ENTRY_DATA, source=SOURCE_USER, - unique_id=_get_unique_id(hass, user_config), + unique_id="test", version=1, ) entry.add_to_hass(hass) diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py index 5ee50c6d0ec..baddd46c19d 100644 --- a/tests/components/climacell/test_init.py +++ b/tests/components/climacell/test_init.py @@ -1,16 +1,14 @@ """Tests for Climacell init.""" +from unittest.mock import patch + import pytest -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN from homeassistant.const import CONF_API_VERSION from homeassistant.core import HomeAssistant -from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry @@ -20,11 +18,10 @@ async def test_load_and_unload( climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading entry.""" - data = _get_config_schema(hass)(MIN_CONFIG) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data=API_V3_ENTRY_DATA, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -42,11 +39,10 @@ async def test_v3_load_and_unload( climacell_config_entry_update: pytest.fixture, ) -> None: """Test loading and unloading v3 entry.""" - data = _get_config_schema(hass)(API_V3_ENTRY_DATA) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data={k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION}, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -59,6 +55,29 @@ async def test_v3_load_and_unload( assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 +async def test_v4_load_and_unload( + hass: HomeAssistant, + climacell_config_entry_update: pytest.fixture, +) -> None: + """Test loading and unloading v3 entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_VERSION: 4, + **{k: v for k, v in API_V3_ENTRY_DATA.items() if k != CONF_API_VERSION}, + }, + unique_id="test", + version=1, + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.tomorrowio.async_setup_entry", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + + @pytest.mark.parametrize( "old_timestep, new_timestep", [(2, 1), (7, 5), (20, 15), (21, 30)] ) @@ -71,9 +90,9 @@ async def test_migrate_timestep( """Test migration to standardized timestep.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=V1_ENTRY_DATA, + data=API_V3_ENTRY_DATA, options={CONF_TIMESTEP: old_timestep}, - unique_id=_get_unique_id(hass, V1_ENTRY_DATA), + unique_id="test", version=1, ) config_entry.add_to_hass(hass) diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 8c075942cea..3412a5c35f0 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -7,10 +7,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION @@ -18,7 +14,7 @@ from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util -from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry @@ -105,11 +101,10 @@ async def _setup( "homeassistant.util.dt.utcnow", return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): - data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data=config, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -151,36 +146,3 @@ async def test_v3_sensor( check_sensor_state(hass, GRASS_POLLEN, "minimal_to_none") check_sensor_state(hass, WEED_POLLEN, "minimal_to_none") check_sensor_state(hass, TREE_POLLEN, "minimal_to_none") - - -async def test_v4_sensor( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test v4 sensor data.""" - await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA) - check_sensor_state(hass, O3, "46.53") - check_sensor_state(hass, CO, "0.63") - check_sensor_state(hass, NO2, "10.67") - check_sensor_state(hass, SO2, "1.65") - check_sensor_state(hass, PM25, "5.2972") - check_sensor_state(hass, PM10, "20.1294") - check_sensor_state(hass, MEP_AQI, "23") - check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") - check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") - check_sensor_state(hass, EPA_AQI, "24") - check_sensor_state(hass, EPA_HEALTH_CONCERN, "good") - check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") - check_sensor_state(hass, FIRE_INDEX, "10") - check_sensor_state(hass, GRASS_POLLEN, "none") - check_sensor_state(hass, WEED_POLLEN, "none") - check_sensor_state(hass, TREE_POLLEN, "none") - check_sensor_state(hass, FEELS_LIKE, "38.5") - check_sensor_state(hass, DEW_POINT, "22.6778") - check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.9688") - check_sensor_state(hass, GHI, "0.0") - check_sensor_state(hass, CLOUD_BASE, "1.1909") - check_sensor_state(hass, CLOUD_COVER, "100") - check_sensor_state(hass, CLOUD_CEILING, "1.1909") - check_sensor_state(hass, WIND_GUST, "5.6506") - check_sensor_state(hass, PRECIPITATION_TYPE, "rain") diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index e3e15889e44..3bac69dcbc5 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -7,10 +7,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.climacell.config_flow import ( - _get_config_schema, - _get_unique_id, -) from homeassistant.components.climacell.const import ( ATTR_CLOUD_COVER, ATTR_PRECIPITATION_TYPE, @@ -30,8 +26,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, @@ -46,7 +40,7 @@ from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util -from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA +from .const import API_V3_ENTRY_DATA from tests.common import MockConfigEntry @@ -69,11 +63,10 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: "homeassistant.util.dt.utcnow", return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): - data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( domain=DOMAIN, - data=data, - unique_id=_get_unique_id(hass, data), + data=config, + unique_id="test", version=1, ) config_entry.add_to_hass(hass) @@ -228,166 +221,3 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" - - -async def test_v4_weather( - hass: HomeAssistant, - climacell_config_entry_update: pytest.fixture, -) -> None: - """Test v4 weather data.""" - weather_state = await _setup(hass, API_V4_ENTRY_DATA) - assert weather_state.state == ATTR_CONDITION_SUNNY - assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION - assert weather_state.attributes[ATTR_FORECAST] == [ - { - ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 8, - ATTR_FORECAST_TEMP_LOW: -3, - ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 15.2727, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 10, - ATTR_FORECAST_TEMP_LOW: -3, - ATTR_FORECAST_WIND_BEARING: 262.82, - ATTR_FORECAST_WIND_SPEED: 11.6517, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 0, - ATTR_FORECAST_WIND_BEARING: 229.3, - ATTR_FORECAST_WIND_SPEED: 11.3459, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 18, - ATTR_FORECAST_TEMP_LOW: 3, - ATTR_FORECAST_WIND_BEARING: 149.91, - ATTR_FORECAST_WIND_SPEED: 17.1234, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 19, - ATTR_FORECAST_TEMP_LOW: 9, - ATTR_FORECAST_WIND_BEARING: 210.45, - ATTR_FORECAST_WIND_SPEED: 25.2506, - }, - { - ATTR_FORECAST_CONDITION: "rainy", - ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0.1219, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 20, - ATTR_FORECAST_TEMP_LOW: 12, - ATTR_FORECAST_WIND_BEARING: 217.98, - ATTR_FORECAST_WIND_SPEED: 19.7949, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 6, - ATTR_FORECAST_WIND_BEARING: 58.79, - ATTR_FORECAST_WIND_SPEED: 15.6428, - }, - { - ATTR_FORECAST_CONDITION: "snowy", - ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 23.9573, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: 1, - ATTR_FORECAST_WIND_BEARING: 70.25, - ATTR_FORECAST_WIND_SPEED: 26.1518, - }, - { - ATTR_FORECAST_CONDITION: "snowy", - ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.4630, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -1, - ATTR_FORECAST_WIND_BEARING: 84.47, - ATTR_FORECAST_WIND_SPEED: 25.5725, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 6, - ATTR_FORECAST_TEMP_LOW: -2, - ATTR_FORECAST_WIND_BEARING: 103.85, - ATTR_FORECAST_WIND_SPEED: 10.7987, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, - ATTR_FORECAST_TEMP: 11, - ATTR_FORECAST_TEMP_LOW: 1, - ATTR_FORECAST_WIND_BEARING: 145.41, - ATTR_FORECAST_WIND_SPEED: 11.6999, - }, - { - ATTR_FORECAST_CONDITION: "cloudy", - ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 0, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10, - ATTR_FORECAST_TEMP: 12, - ATTR_FORECAST_TEMP_LOW: 5, - ATTR_FORECAST_WIND_BEARING: 62.99, - ATTR_FORECAST_WIND_SPEED: 10.5895, - }, - { - ATTR_FORECAST_CONDITION: "rainy", - ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 2.9261, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, - ATTR_FORECAST_TEMP: 9, - ATTR_FORECAST_TEMP_LOW: 4, - ATTR_FORECAST_WIND_BEARING: 68.54, - ATTR_FORECAST_WIND_SPEED: 22.3860, - }, - { - ATTR_FORECAST_CONDITION: "snowy", - ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00", - ATTR_FORECAST_PRECIPITATION: 1.2192, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3, - ATTR_FORECAST_TEMP: 5, - ATTR_FORECAST_TEMP_LOW: 2, - ATTR_FORECAST_WIND_BEARING: 56.98, - ATTR_FORECAST_WIND_SPEED: 27.9221, - }, - ] - assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" - assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 - assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1027.7691 - assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.1162 - assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0152 - assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 - assert weather_state.attributes[ATTR_WIND_GUST] == 20.3421 - assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" diff --git a/tests/components/tomorrowio/__init__.py b/tests/components/tomorrowio/__init__.py new file mode 100644 index 00000000000..de0ec5d135a --- /dev/null +++ b/tests/components/tomorrowio/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tomorrow.io Weather API integration.""" diff --git a/tests/components/tomorrowio/conftest.py b/tests/components/tomorrowio/conftest.py new file mode 100644 index 00000000000..65c69209f0e --- /dev/null +++ b/tests/components/tomorrowio/conftest.py @@ -0,0 +1,46 @@ +"""Configure py.test.""" +import json +from unittest.mock import patch + +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(name="tomorrowio_config_flow_connect", autouse=True) +def tomorrowio_config_flow_connect(): + """Mock valid tomorrowio config flow setup.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + return_value={}, + ): + yield + + +@pytest.fixture(name="tomorrowio_config_entry_update", autouse=True) +def tomorrowio_config_entry_update_fixture(): + """Mock valid tomorrowio config entry setup.""" + with patch( + "homeassistant.components.tomorrowio.TomorrowioV4.realtime_and_all_forecasts", + return_value=json.loads(load_fixture("v4.json", "tomorrowio")), + ): + yield + + +@pytest.fixture(name="climacell_config_entry_update") +def climacell_config_entry_update_fixture(): + """Mock valid climacell config entry setup.""" + with patch( + "homeassistant.components.climacell.ClimaCellV3.realtime", + return_value={}, + ), patch( + "homeassistant.components.climacell.ClimaCellV3.forecast_hourly", + return_value={}, + ), patch( + "homeassistant.components.climacell.ClimaCellV3.forecast_daily", + return_value={}, + ), patch( + "homeassistant.components.climacell.ClimaCellV3.forecast_nowcast", + return_value={}, + ): + yield diff --git a/tests/components/tomorrowio/const.py b/tests/components/tomorrowio/const.py new file mode 100644 index 00000000000..8cb00c6707e --- /dev/null +++ b/tests/components/tomorrowio/const.py @@ -0,0 +1,21 @@ +"""Constants for tomorrowio tests.""" + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE + +API_KEY = "aa" + +MIN_CONFIG = { + CONF_API_KEY: API_KEY, +} + +V1_ENTRY_DATA = { + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, +} + +API_V4_ENTRY_DATA = { + CONF_API_KEY: API_KEY, + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, +} diff --git a/tests/components/tomorrowio/fixtures/v4.json b/tests/components/tomorrowio/fixtures/v4.json new file mode 100644 index 00000000000..5cd86b5b60e --- /dev/null +++ b/tests/components/tomorrowio/fixtures/v4.json @@ -0,0 +1,2384 @@ +{ + "current": { + "temperature": 44.13, + "humidity": 22.71, + "pressureSeaLevel": 30.35, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "visibility": 8.15, + "pollutantO3": 46.53, + "windGust": 12.64, + "cloudCover": 100, + "precipitationType": 1, + "particulateMatter25": 0.15, + "particulateMatter10": 0.57, + "pollutantNO2": 10.67, + "pollutantCO": 0.63, + "pollutantSO2": 1.65, + "epaIndex": 24, + "epaPrimaryPollutant": 0, + "epaHealthConcern": 0, + "mepIndex": 23, + "mepPrimaryPollutant": 1, + "mepHealthConcern": 0, + "treeIndex": 0, + "weedIndex": 0, + "grassIndex": 0, + "fireIndex": 10, + "temperatureApparent": 101.3, + "dewPoint": 72.82, + "pressureSurfaceLevel": 29.47, + "solarGHI": 0, + "cloudBase": 0.74, + "cloudCeiling": 0.74 + }, + "forecasts": { + "nowcast": [ + { + "startTime": "2021-03-07T17:48:00Z", + "values": { + "temperatureMin": 44.13, + "temperatureMax": 44.13, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T17:53:00Z", + "values": { + "temperatureMin": 43.9, + "temperatureMax": 43.9, + "windSpeed": 9.31, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T17:58:00Z", + "values": { + "temperatureMin": 43.68, + "temperatureMax": 43.68, + "windSpeed": 9.28, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:03:00Z", + "values": { + "temperatureMin": 43.66, + "temperatureMax": 43.66, + "windSpeed": 9.26, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:08:00Z", + "values": { + "temperatureMin": 43.79, + "temperatureMax": 43.79, + "windSpeed": 9.22, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:13:00Z", + "values": { + "temperatureMin": 43.92, + "temperatureMax": 43.92, + "windSpeed": 9.17, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:18:00Z", + "values": { + "temperatureMin": 44.04, + "temperatureMax": 44.04, + "windSpeed": 9.13, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:23:00Z", + "values": { + "temperatureMin": 44.17, + "temperatureMax": 44.17, + "windSpeed": 9.06, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:28:00Z", + "values": { + "temperatureMin": 44.31, + "temperatureMax": 44.31, + "windSpeed": 9.02, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:33:00Z", + "values": { + "temperatureMin": 44.44, + "temperatureMax": 44.44, + "windSpeed": 8.97, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:38:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.93, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:43:00Z", + "values": { + "temperatureMin": 44.69, + "temperatureMax": 44.69, + "windSpeed": 8.88, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:48:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.84, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:53:00Z", + "values": { + "temperatureMin": 44.94, + "temperatureMax": 44.94, + "windSpeed": 8.79, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:58:00Z", + "values": { + "temperatureMin": 45.07, + "temperatureMax": 45.07, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:03:00Z", + "values": { + "temperatureMin": 45.16, + "temperatureMax": 45.16, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:08:00Z", + "values": { + "temperatureMin": 45.23, + "temperatureMax": 45.23, + "windSpeed": 8.75, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:13:00Z", + "values": { + "temperatureMin": 45.28, + "temperatureMax": 45.28, + "windSpeed": 8.77, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:18:00Z", + "values": { + "temperatureMin": 45.36, + "temperatureMax": 45.36, + "windSpeed": 8.79, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:23:00Z", + "values": { + "temperatureMin": 45.43, + "temperatureMax": 45.43, + "windSpeed": 8.81, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:28:00Z", + "values": { + "temperatureMin": 45.5, + "temperatureMax": 45.5, + "windSpeed": 8.81, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:33:00Z", + "values": { + "temperatureMin": 45.55, + "temperatureMax": 45.55, + "windSpeed": 8.84, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:38:00Z", + "values": { + "temperatureMin": 45.63, + "temperatureMax": 45.63, + "windSpeed": 8.86, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:43:00Z", + "values": { + "temperatureMin": 45.7, + "temperatureMax": 45.7, + "windSpeed": 8.88, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:48:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:53:00Z", + "values": { + "temperatureMin": 45.82, + "temperatureMax": 45.82, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:58:00Z", + "values": { + "temperatureMin": 45.9, + "temperatureMax": 45.9, + "windSpeed": 8.93, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:03:00Z", + "values": { + "temperatureMin": 45.88, + "temperatureMax": 45.88, + "windSpeed": 8.97, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:08:00Z", + "values": { + "temperatureMin": 45.82, + "temperatureMax": 45.82, + "windSpeed": 9.02, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:13:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 9.06, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:18:00Z", + "values": { + "temperatureMin": 45.7, + "temperatureMax": 45.7, + "windSpeed": 9.1, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:23:00Z", + "values": { + "temperatureMin": 45.63, + "temperatureMax": 45.63, + "windSpeed": 9.15, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:28:00Z", + "values": { + "temperatureMin": 45.57, + "temperatureMax": 45.57, + "windSpeed": 9.19, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:33:00Z", + "values": { + "temperatureMin": 45.5, + "temperatureMax": 45.5, + "windSpeed": 9.24, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:38:00Z", + "values": { + "temperatureMin": 45.45, + "temperatureMax": 45.45, + "windSpeed": 9.28, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:43:00Z", + "values": { + "temperatureMin": 45.39, + "temperatureMax": 45.39, + "windSpeed": 9.33, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:48:00Z", + "values": { + "temperatureMin": 45.32, + "temperatureMax": 45.32, + "windSpeed": 9.37, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:53:00Z", + "values": { + "temperatureMin": 45.27, + "temperatureMax": 45.27, + "windSpeed": 9.42, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:58:00Z", + "values": { + "temperatureMin": 45.19, + "temperatureMax": 45.19, + "windSpeed": 9.46, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:03:00Z", + "values": { + "temperatureMin": 45.14, + "temperatureMax": 45.14, + "windSpeed": 9.4, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:08:00Z", + "values": { + "temperatureMin": 45.07, + "temperatureMax": 45.07, + "windSpeed": 9.24, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:13:00Z", + "values": { + "temperatureMin": 45.01, + "temperatureMax": 45.01, + "windSpeed": 9.08, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:18:00Z", + "values": { + "temperatureMin": 44.94, + "temperatureMax": 44.94, + "windSpeed": 8.95, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:23:00Z", + "values": { + "temperatureMin": 44.89, + "temperatureMax": 44.89, + "windSpeed": 8.79, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:28:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.63, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:33:00Z", + "values": { + "temperatureMin": 44.76, + "temperatureMax": 44.76, + "windSpeed": 8.5, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:38:00Z", + "values": { + "temperatureMin": 44.69, + "temperatureMax": 44.69, + "windSpeed": 8.34, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:43:00Z", + "values": { + "temperatureMin": 44.64, + "temperatureMax": 44.64, + "windSpeed": 8.19, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:48:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.05, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:53:00Z", + "values": { + "temperatureMin": 44.51, + "temperatureMax": 44.51, + "windSpeed": 7.9, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:58:00Z", + "values": { + "temperatureMin": 44.44, + "temperatureMax": 44.44, + "windSpeed": 7.74, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:03:00Z", + "values": { + "temperatureMin": 44.26, + "temperatureMax": 44.26, + "windSpeed": 7.47, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:08:00Z", + "values": { + "temperatureMin": 44.01, + "temperatureMax": 44.01, + "windSpeed": 7.14, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:13:00Z", + "values": { + "temperatureMin": 43.74, + "temperatureMax": 43.74, + "windSpeed": 6.78, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:18:00Z", + "values": { + "temperatureMin": 43.48, + "temperatureMax": 43.48, + "windSpeed": 6.44, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:23:00Z", + "values": { + "temperatureMin": 43.23, + "temperatureMax": 43.23, + "windSpeed": 6.08, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:28:00Z", + "values": { + "temperatureMin": 42.98, + "temperatureMax": 42.98, + "windSpeed": 5.75, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:33:00Z", + "values": { + "temperatureMin": 42.71, + "temperatureMax": 42.71, + "windSpeed": 5.39, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:38:00Z", + "values": { + "temperatureMin": 42.46, + "temperatureMax": 42.46, + "windSpeed": 5.06, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:43:00Z", + "values": { + "temperatureMin": 42.21, + "temperatureMax": 42.21, + "windSpeed": 4.7, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:48:00Z", + "values": { + "temperatureMin": 41.94, + "temperatureMax": 41.94, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:53:00Z", + "values": { + "temperatureMin": 41.68, + "temperatureMax": 41.68, + "windSpeed": 4, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:58:00Z", + "values": { + "temperatureMin": 41.43, + "temperatureMax": 41.43, + "windSpeed": 3.67, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:03:00Z", + "values": { + "temperatureMin": 41.16, + "temperatureMax": 41.16, + "windSpeed": 3.6, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:08:00Z", + "values": { + "temperatureMin": 40.91, + "temperatureMax": 40.91, + "windSpeed": 3.76, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:13:00Z", + "values": { + "temperatureMin": 40.66, + "temperatureMax": 40.66, + "windSpeed": 3.91, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:18:00Z", + "values": { + "temperatureMin": 40.41, + "temperatureMax": 40.41, + "windSpeed": 4.05, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:23:00Z", + "values": { + "temperatureMin": 40.14, + "temperatureMax": 40.14, + "windSpeed": 4.21, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:28:00Z", + "values": { + "temperatureMin": 39.88, + "temperatureMax": 39.88, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:33:00Z", + "values": { + "temperatureMin": 39.63, + "temperatureMax": 39.63, + "windSpeed": 4.5, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:38:00Z", + "values": { + "temperatureMin": 39.38, + "temperatureMax": 39.38, + "windSpeed": 4.65, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:43:00Z", + "values": { + "temperatureMin": 39.11, + "temperatureMax": 39.11, + "windSpeed": 4.79, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + } + ], + "hourly": [ + { + "startTime": "2021-03-07T17:48:00Z", + "values": { + "temperatureMin": 44.13, + "temperatureMax": 44.13, + "windSpeed": 9.33, + "windDirection": 315.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T18:48:00Z", + "values": { + "temperatureMin": 44.82, + "temperatureMax": 44.82, + "windSpeed": 8.84, + "windDirection": 321.71, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T19:48:00Z", + "values": { + "temperatureMin": 45.75, + "temperatureMax": 45.75, + "windSpeed": 8.9, + "windDirection": 323.38, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T20:48:00Z", + "values": { + "temperatureMin": 45.32, + "temperatureMax": 45.32, + "windSpeed": 9.37, + "windDirection": 318.43, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T21:48:00Z", + "values": { + "temperatureMin": 44.56, + "temperatureMax": 44.56, + "windSpeed": 8.05, + "windDirection": 320.9, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T22:48:00Z", + "values": { + "temperatureMin": 41.94, + "temperatureMax": 41.94, + "windSpeed": 4.36, + "windDirection": 322.11, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-07T23:48:00Z", + "values": { + "temperatureMin": 38.86, + "temperatureMax": 38.86, + "windSpeed": 4.94, + "windDirection": 295.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T00:48:00Z", + "values": { + "temperatureMin": 36.18, + "temperatureMax": 36.18, + "windSpeed": 5.59, + "windDirection": 11.94, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T01:48:00Z", + "values": { + "temperatureMin": 34.3, + "temperatureMax": 34.3, + "windSpeed": 5.57, + "windDirection": 13.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T02:48:00Z", + "values": { + "temperatureMin": 32.88, + "temperatureMax": 32.88, + "windSpeed": 5.41, + "windDirection": 14.93, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T03:48:00Z", + "values": { + "temperatureMin": 31.91, + "temperatureMax": 31.91, + "windSpeed": 4.61, + "windDirection": 26.07, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T04:48:00Z", + "values": { + "temperatureMin": 29.17, + "temperatureMax": 29.17, + "windSpeed": 2.59, + "windDirection": 51.27, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T05:48:00Z", + "values": { + "temperatureMin": 27.37, + "temperatureMax": 27.37, + "windSpeed": 3.31, + "windDirection": 343.25, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T06:48:00Z", + "values": { + "temperatureMin": 26.73, + "temperatureMax": 26.73, + "windSpeed": 4.27, + "windDirection": 341.46, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T07:48:00Z", + "values": { + "temperatureMin": 26.38, + "temperatureMax": 26.38, + "windSpeed": 3.53, + "windDirection": 322.34, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T08:48:00Z", + "values": { + "temperatureMin": 26.15, + "temperatureMax": 26.15, + "windSpeed": 3.65, + "windDirection": 294.69, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T09:48:00Z", + "values": { + "temperatureMin": 30.07, + "temperatureMax": 30.07, + "windSpeed": 3.2, + "windDirection": 325.32, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T10:48:00Z", + "values": { + "temperatureMin": 31.03, + "temperatureMax": 31.03, + "windSpeed": 2.84, + "windDirection": 322.27, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T11:48:00Z", + "values": { + "temperatureMin": 27.23, + "temperatureMax": 27.23, + "windSpeed": 5.59, + "windDirection": 310.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T12:48:00Z", + "values": { + "temperatureMin": 29.21, + "temperatureMax": 29.21, + "windSpeed": 7.05, + "windDirection": 324.8, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T13:48:00Z", + "values": { + "temperatureMin": 33.19, + "temperatureMax": 33.19, + "windSpeed": 6.46, + "windDirection": 335.16, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T14:48:00Z", + "values": { + "temperatureMin": 37.02, + "temperatureMax": 37.02, + "windSpeed": 5.88, + "windDirection": 324.49, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T15:48:00Z", + "values": { + "temperatureMin": 40.01, + "temperatureMax": 40.01, + "windSpeed": 5.55, + "windDirection": 310.68, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T16:48:00Z", + "values": { + "temperatureMin": 42.37, + "temperatureMax": 42.37, + "windSpeed": 5.46, + "windDirection": 304.18, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T17:48:00Z", + "values": { + "temperatureMin": 44.62, + "temperatureMax": 44.62, + "windSpeed": 4.99, + "windDirection": 301.19, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T18:48:00Z", + "values": { + "temperatureMin": 46.78, + "temperatureMax": 46.78, + "windSpeed": 4.72, + "windDirection": 295.05, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T19:48:00Z", + "values": { + "temperatureMin": 48.42, + "temperatureMax": 48.42, + "windSpeed": 4.81, + "windDirection": 287.4, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T20:48:00Z", + "values": { + "temperatureMin": 49.28, + "temperatureMax": 49.28, + "windSpeed": 4.74, + "windDirection": 282.48, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T21:48:00Z", + "values": { + "temperatureMin": 48.72, + "temperatureMax": 48.72, + "windSpeed": 2.51, + "windDirection": 268.74, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T22:48:00Z", + "values": { + "temperatureMin": 44.37, + "temperatureMax": 44.37, + "windSpeed": 3.56, + "windDirection": 180.04, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T23:48:00Z", + "values": { + "temperatureMin": 39.9, + "temperatureMax": 39.9, + "windSpeed": 4.68, + "windDirection": 177.89, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T00:48:00Z", + "values": { + "temperatureMin": 37.87, + "temperatureMax": 37.87, + "windSpeed": 5.21, + "windDirection": 197.47, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T01:48:00Z", + "values": { + "temperatureMin": 36.91, + "temperatureMax": 36.91, + "windSpeed": 5.46, + "windDirection": 209.77, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T02:48:00Z", + "values": { + "temperatureMin": 36.64, + "temperatureMax": 36.64, + "windSpeed": 6.11, + "windDirection": 210.14, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T03:48:00Z", + "values": { + "temperatureMin": 36.63, + "temperatureMax": 36.63, + "windSpeed": 6.4, + "windDirection": 216, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T04:48:00Z", + "values": { + "temperatureMin": 36.23, + "temperatureMax": 36.23, + "windSpeed": 6.22, + "windDirection": 223.92, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T05:48:00Z", + "values": { + "temperatureMin": 35.58, + "temperatureMax": 35.58, + "windSpeed": 5.75, + "windDirection": 229.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T06:48:00Z", + "values": { + "temperatureMin": 34.68, + "temperatureMax": 34.68, + "windSpeed": 5.21, + "windDirection": 235.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T07:48:00Z", + "values": { + "temperatureMin": 33.69, + "temperatureMax": 33.69, + "windSpeed": 4.81, + "windDirection": 237.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T08:48:00Z", + "values": { + "temperatureMin": 32.74, + "temperatureMax": 32.74, + "windSpeed": 4.52, + "windDirection": 239.35, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T09:48:00Z", + "values": { + "temperatureMin": 32.05, + "temperatureMax": 32.05, + "windSpeed": 4.32, + "windDirection": 245.68, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T10:48:00Z", + "values": { + "temperatureMin": 31.57, + "temperatureMax": 31.57, + "windSpeed": 4.14, + "windDirection": 248.11, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T11:48:00Z", + "values": { + "temperatureMin": 32.92, + "temperatureMax": 32.92, + "windSpeed": 4.32, + "windDirection": 249.54, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T12:48:00Z", + "values": { + "temperatureMin": 38.5, + "temperatureMax": 38.5, + "windSpeed": 4.7, + "windDirection": 253.3, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T13:48:00Z", + "values": { + "temperatureMin": 46.08, + "temperatureMax": 46.08, + "windSpeed": 4.41, + "windDirection": 258.49, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T14:48:00Z", + "values": { + "temperatureMin": 53.26, + "temperatureMax": 53.26, + "windSpeed": 4.9, + "windDirection": 260.49, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T15:48:00Z", + "values": { + "temperatureMin": 58.15, + "temperatureMax": 58.15, + "windSpeed": 5.55, + "windDirection": 261.29, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T16:48:00Z", + "values": { + "temperatureMin": 61.56, + "temperatureMax": 61.56, + "windSpeed": 6.35, + "windDirection": 264.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T17:48:00Z", + "values": { + "temperatureMin": 64, + "temperatureMax": 64, + "windSpeed": 6.6, + "windDirection": 257.54, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T18:48:00Z", + "values": { + "temperatureMin": 65.79, + "temperatureMax": 65.79, + "windSpeed": 6.96, + "windDirection": 253.12, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T19:48:00Z", + "values": { + "temperatureMin": 66.74, + "temperatureMax": 66.74, + "windSpeed": 6.8, + "windDirection": 259.46, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T20:48:00Z", + "values": { + "temperatureMin": 66.96, + "temperatureMax": 66.96, + "windSpeed": 6.33, + "windDirection": 294.25, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T21:48:00Z", + "values": { + "temperatureMin": 64.35, + "temperatureMax": 64.35, + "windSpeed": 3.91, + "windDirection": 279.37, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T22:48:00Z", + "values": { + "temperatureMin": 61.07, + "temperatureMax": 61.07, + "windSpeed": 3.65, + "windDirection": 218.19, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T23:48:00Z", + "values": { + "temperatureMin": 56.3, + "temperatureMax": 56.3, + "windSpeed": 4.09, + "windDirection": 208.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T00:48:00Z", + "values": { + "temperatureMin": 53.19, + "temperatureMax": 53.19, + "windSpeed": 4.21, + "windDirection": 216.42, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T01:48:00Z", + "values": { + "temperatureMin": 51.94, + "temperatureMax": 51.94, + "windSpeed": 3.38, + "windDirection": 257.19, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T02:48:00Z", + "values": { + "temperatureMin": 49.82, + "temperatureMax": 49.82, + "windSpeed": 2.71, + "windDirection": 288.85, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T03:48:00Z", + "values": { + "temperatureMin": 48.24, + "temperatureMax": 48.24, + "windSpeed": 2.8, + "windDirection": 334.41, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T04:48:00Z", + "values": { + "temperatureMin": 47.44, + "temperatureMax": 47.44, + "windSpeed": 2.26, + "windDirection": 342.01, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T05:48:00Z", + "values": { + "temperatureMin": 45.59, + "temperatureMax": 45.59, + "windSpeed": 2.35, + "windDirection": 2.43, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T06:48:00Z", + "values": { + "temperatureMin": 43.43, + "temperatureMax": 43.43, + "windSpeed": 2.3, + "windDirection": 336.56, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T07:48:00Z", + "values": { + "temperatureMin": 41.11, + "temperatureMax": 41.11, + "windSpeed": 2.71, + "windDirection": 4.41, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T08:48:00Z", + "values": { + "temperatureMin": 39.58, + "temperatureMax": 39.58, + "windSpeed": 3.4, + "windDirection": 21.26, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T09:48:00Z", + "values": { + "temperatureMin": 39.85, + "temperatureMax": 39.85, + "windSpeed": 3.31, + "windDirection": 22.76, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T10:48:00Z", + "values": { + "temperatureMin": 37.85, + "temperatureMax": 37.85, + "windSpeed": 4.03, + "windDirection": 29.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T11:48:00Z", + "values": { + "temperatureMin": 38.97, + "temperatureMax": 38.97, + "windSpeed": 3.15, + "windDirection": 21.82, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T12:48:00Z", + "values": { + "temperatureMin": 44.31, + "temperatureMax": 44.31, + "windSpeed": 3.53, + "windDirection": 14.25, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T13:48:00Z", + "values": { + "temperatureMin": 50.25, + "temperatureMax": 50.25, + "windSpeed": 2.82, + "windDirection": 42.41, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T14:48:00Z", + "values": { + "temperatureMin": 54.97, + "temperatureMax": 54.97, + "windSpeed": 2.53, + "windDirection": 87.81, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T15:48:00Z", + "values": { + "temperatureMin": 58.46, + "temperatureMax": 58.46, + "windSpeed": 3.09, + "windDirection": 125.82, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T16:48:00Z", + "values": { + "temperatureMin": 61.21, + "temperatureMax": 61.21, + "windSpeed": 4.03, + "windDirection": 157.54, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T17:48:00Z", + "values": { + "temperatureMin": 63.36, + "temperatureMax": 63.36, + "windSpeed": 5.21, + "windDirection": 166.66, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T18:48:00Z", + "values": { + "temperatureMin": 64.83, + "temperatureMax": 64.83, + "windSpeed": 6.93, + "windDirection": 189.24, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T19:48:00Z", + "values": { + "temperatureMin": 65.23, + "temperatureMax": 65.23, + "windSpeed": 8.95, + "windDirection": 194.58, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T20:48:00Z", + "values": { + "temperatureMin": 64.98, + "temperatureMax": 64.98, + "windSpeed": 9.4, + "windDirection": 193.22, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T21:48:00Z", + "values": { + "temperatureMin": 64.06, + "temperatureMax": 64.06, + "windSpeed": 8.55, + "windDirection": 186.39, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T22:48:00Z", + "values": { + "temperatureMin": 61.9, + "temperatureMax": 61.9, + "windSpeed": 7.49, + "windDirection": 171.81, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T23:48:00Z", + "values": { + "temperatureMin": 59.4, + "temperatureMax": 59.4, + "windSpeed": 7.54, + "windDirection": 165.51, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T00:48:00Z", + "values": { + "temperatureMin": 57.63, + "temperatureMax": 57.63, + "windSpeed": 8.12, + "windDirection": 171.94, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T01:48:00Z", + "values": { + "temperatureMin": 56.17, + "temperatureMax": 56.17, + "windSpeed": 8.7, + "windDirection": 176.84, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T02:48:00Z", + "values": { + "temperatureMin": 55.36, + "temperatureMax": 55.36, + "windSpeed": 9.42, + "windDirection": 184.14, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T03:48:00Z", + "values": { + "temperatureMin": 54.88, + "temperatureMax": 54.88, + "windSpeed": 10, + "windDirection": 195.54, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T04:48:00Z", + "values": { + "temperatureMin": 54.14, + "temperatureMax": 54.14, + "windSpeed": 10.4, + "windDirection": 200.56, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T05:48:00Z", + "values": { + "temperatureMin": 53.46, + "temperatureMax": 53.46, + "windSpeed": 10.04, + "windDirection": 198.08, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T06:48:00Z", + "values": { + "temperatureMin": 52.11, + "temperatureMax": 52.11, + "windSpeed": 10.02, + "windDirection": 199.54, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T07:48:00Z", + "values": { + "temperatureMin": 51.64, + "temperatureMax": 51.64, + "windSpeed": 10.51, + "windDirection": 202.73, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T08:48:00Z", + "values": { + "temperatureMin": 50.79, + "temperatureMax": 50.79, + "windSpeed": 10.38, + "windDirection": 203.35, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T09:48:00Z", + "values": { + "temperatureMin": 49.93, + "temperatureMax": 49.93, + "windSpeed": 9.51, + "windDirection": 210.36, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T10:48:00Z", + "values": { + "temperatureMin": 49.1, + "temperatureMax": 49.1, + "windSpeed": 8.61, + "windDirection": 210.6, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T11:48:00Z", + "values": { + "temperatureMin": 48.42, + "temperatureMax": 48.42, + "windSpeed": 9.15, + "windDirection": 211.29, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T12:48:00Z", + "values": { + "temperatureMin": 48.9, + "temperatureMax": 48.9, + "windSpeed": 10.25, + "windDirection": 215.59, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T13:48:00Z", + "values": { + "temperatureMin": 50.54, + "temperatureMax": 50.54, + "windSpeed": 10.18, + "windDirection": 215.48, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T14:48:00Z", + "values": { + "temperatureMin": 53.19, + "temperatureMax": 53.19, + "windSpeed": 9.4, + "windDirection": 208.76, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T15:48:00Z", + "values": { + "temperatureMin": 56.19, + "temperatureMax": 56.19, + "windSpeed": 9.73, + "windDirection": 197.59, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T16:48:00Z", + "values": { + "temperatureMin": 59.34, + "temperatureMax": 59.34, + "windSpeed": 10.69, + "windDirection": 204.29, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T17:48:00Z", + "values": { + "temperatureMin": 62.35, + "temperatureMax": 62.35, + "windSpeed": 11.81, + "windDirection": 204.56, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T18:48:00Z", + "values": { + "temperatureMin": 64.6, + "temperatureMax": 64.6, + "windSpeed": 13.09, + "windDirection": 206.85, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T19:48:00Z", + "values": { + "temperatureMin": 65.91, + "temperatureMax": 65.91, + "windSpeed": 13.82, + "windDirection": 204.82, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T20:48:00Z", + "values": { + "temperatureMin": 66.22, + "temperatureMax": 66.22, + "windSpeed": 14.54, + "windDirection": 208.43, + "weatherCode": 1100, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T21:48:00Z", + "values": { + "temperatureMin": 65.46, + "temperatureMax": 65.46, + "windSpeed": 13.2, + "windDirection": 208.3, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T22:48:00Z", + "values": { + "temperatureMin": 64.35, + "temperatureMax": 64.35, + "windSpeed": 12.35, + "windDirection": 208.58, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T23:48:00Z", + "values": { + "temperatureMin": 62.85, + "temperatureMax": 62.85, + "windSpeed": 12.86, + "windDirection": 205.39, + "weatherCode": 1101, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T00:48:00Z", + "values": { + "temperatureMin": 61.75, + "temperatureMax": 61.75, + "windSpeed": 14.7, + "windDirection": 209.51, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T01:48:00Z", + "values": { + "temperatureMin": 61.2, + "temperatureMax": 61.2, + "windSpeed": 15.57, + "windDirection": 211.47, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T02:48:00Z", + "values": { + "temperatureMin": 60.46, + "temperatureMax": 60.46, + "windSpeed": 14.94, + "windDirection": 211.57, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T03:48:00Z", + "values": { + "temperatureMin": 59.94, + "temperatureMax": 59.94, + "windSpeed": 14.29, + "windDirection": 208.93, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T04:48:00Z", + "values": { + "temperatureMin": 59.52, + "temperatureMax": 59.52, + "windSpeed": 14.36, + "windDirection": 217.91, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + } + ], + "daily": [ + { + "startTime": "2021-03-07T11:00:00Z", + "values": { + "temperatureMin": 26.11, + "temperatureMax": 45.93, + "windSpeed": 9.49, + "windDirection": 239.6, + "weatherCode": 1000, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-08T11:00:00Z", + "values": { + "temperatureMin": 26.28, + "temperatureMax": 49.42, + "windSpeed": 7.24, + "windDirection": 262.82, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-09T11:00:00Z", + "values": { + "temperatureMin": 31.48, + "temperatureMax": 66.98, + "windSpeed": 7.05, + "windDirection": 229.3, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-10T11:00:00Z", + "values": { + "temperatureMin": 37.32, + "temperatureMax": 65.28, + "windSpeed": 10.64, + "windDirection": 149.91, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-11T11:00:00Z", + "values": { + "temperatureMin": 48.29, + "temperatureMax": 66.25, + "windSpeed": 15.69, + "windDirection": 210.45, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-12T11:00:00Z", + "values": { + "temperatureMin": 53.83, + "temperatureMax": 67.91, + "windSpeed": 12.3, + "windDirection": 217.98, + "weatherCode": 4000, + "precipitationIntensityAvg": 0.0002, + "precipitationProbability": 25 + } + }, + { + "startTime": "2021-03-13T11:00:00Z", + "values": { + "temperatureMin": 42.91, + "temperatureMax": 54.48, + "windSpeed": 9.72, + "windDirection": 58.79, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 25 + } + }, + { + "startTime": "2021-03-14T10:00:00Z", + "values": { + "temperatureMin": 33.35, + "temperatureMax": 42.91, + "windSpeed": 16.25, + "windDirection": 70.25, + "weatherCode": 5101, + "precipitationIntensityAvg": 0.0393, + "precipitationProbability": 95 + } + }, + { + "startTime": "2021-03-15T10:00:00Z", + "values": { + "temperatureMin": 29.35, + "temperatureMax": 43.67, + "windSpeed": 15.89, + "windDirection": 84.47, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.0024, + "precipitationProbability": 55 + } + }, + { + "startTime": "2021-03-16T10:00:00Z", + "values": { + "temperatureMin": 29.1, + "temperatureMax": 43, + "windSpeed": 6.71, + "windDirection": 103.85, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-17T10:00:00Z", + "values": { + "temperatureMin": 34.32, + "temperatureMax": 52.4, + "windSpeed": 7.27, + "windDirection": 145.41, + "weatherCode": 1102, + "precipitationIntensityAvg": 0, + "precipitationProbability": 0 + } + }, + { + "startTime": "2021-03-18T10:00:00Z", + "values": { + "temperatureMin": 41.32, + "temperatureMax": 54.07, + "windSpeed": 6.58, + "windDirection": 62.99, + "weatherCode": 1001, + "precipitationIntensityAvg": 0, + "precipitationProbability": 10 + } + }, + { + "startTime": "2021-03-19T10:00:00Z", + "values": { + "temperatureMin": 39.4, + "temperatureMax": 48.94, + "windSpeed": 13.91, + "windDirection": 68.54, + "weatherCode": 4000, + "precipitationIntensityAvg": 0.0048, + "precipitationProbability": 55 + } + }, + { + "startTime": "2021-03-20T10:00:00Z", + "values": { + "temperatureMin": 35.06, + "temperatureMax": 40.12, + "windSpeed": 17.35, + "windDirection": 56.98, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.002, + "precipitationProbability": 33.3 + } + }, + { + "startTime": "2021-03-21T10:00:00Z", + "values": { + "temperatureMin": 33.66, + "temperatureMax": 66.54, + "windSpeed": 15.93, + "windDirection": 82.57, + "weatherCode": 5001, + "precipitationIntensityAvg": 0.0004, + "precipitationProbability": 45 + } + } + ] + } + } diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py new file mode 100644 index 00000000000..7e04e213bbb --- /dev/null +++ b/tests/components/tomorrowio/test_config_flow.py @@ -0,0 +1,279 @@ +"""Test the Tomorrow.io config flow.""" +from unittest.mock import patch + +from pytomorrowio.exceptions import ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, +) + +from homeassistant import data_entry_flow +from homeassistant.components.climacell.const import DOMAIN as CC_DOMAIN +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, ConfigEntryState +from homeassistant.const import ( + CONF_API_KEY, + CONF_API_VERSION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import API_KEY, MIN_CONFIG + +from tests.common import MockConfigEntry +from tests.components.climacell.const import API_V3_ENTRY_DATA + + +async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: + """Test user config flow with minimum fields.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_API_KEY] == API_KEY + assert result["data"][CONF_LATITUDE] == hass.config.latitude + assert result["data"][CONF_LONGITUDE] == hass.config.longitude + + +async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: + """Test user config flow with minimum fields.""" + assert await async_setup_component( + hass, + "zone", + { + "zone": { + CONF_NAME: "Home", + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + CONF_RADIUS: 100, + } + }, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{DEFAULT_NAME} - Home" + assert result["data"][CONF_NAME] == f"{DEFAULT_NAME} - Home" + assert result["data"][CONF_API_KEY] == API_KEY + assert result["data"][CONF_LATITUDE] == hass.config.latitude + assert result["data"][CONF_LONGITUDE] == hass.config.longitude + + +async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: + """Test user config flow with the same unique ID as an existing entry.""" + user_input = _get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG) + MockConfigEntry( + domain=DOMAIN, + data=user_input, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_input), + version=2, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: + """Test user config flow when Tomorrow.io can't connect.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=CantConnectException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: + """Test user config flow when API key is invalid.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=InvalidAPIKeyException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: + """Test user config flow when API key is rate limited.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=RateLimitedException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "rate_limited"} + + +async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: + """Test user config flow when unknown error occurs.""" + with patch( + "homeassistant.components.tomorrowio.config_flow.TomorrowioV4.realtime", + side_effect=UnknownException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test options config flow for tomorrowio.""" + user_config = _get_config_schema(hass, SOURCE_USER)(MIN_CONFIG) + entry = MockConfigEntry( + domain=DOMAIN, + data=user_config, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_config), + version=1, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.options[CONF_TIMESTEP] == DEFAULT_TIMESTEP + assert CONF_TIMESTEP not in entry.data + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_TIMESTEP: 1} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_TIMESTEP] == 1 + assert entry.options[CONF_TIMESTEP] == 1 + + +async def test_import_flow_v4(hass: HomeAssistant) -> None: + """Test import flow for climacell v4 config entry.""" + user_config = API_V3_ENTRY_DATA.copy() + user_config[CONF_API_VERSION] = 4 + old_entry = MockConfigEntry( + domain=CC_DOMAIN, + data=user_config, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_config), + version=1, + ) + old_entry.add_to_hass(hass) + await hass.config_entries.async_setup(old_entry.entry_id) + await hass.async_block_till_done() + assert old_entry.state != ConfigEntryState.LOADED + + assert len(hass.config_entries.async_entries(CC_DOMAIN)) == 0 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert "old_config_entry_id" not in entry.data + assert CONF_API_VERSION not in entry.data + + +async def test_import_flow_v3( + hass: HomeAssistant, climacell_config_entry_update +) -> None: + """Test import flow for climacell v3 config entry.""" + user_config = API_V3_ENTRY_DATA + old_entry = MockConfigEntry( + domain=CC_DOMAIN, + data=user_config, + source=SOURCE_USER, + unique_id=_get_unique_id(hass, user_config), + version=1, + ) + old_entry.add_to_hass(hass) + await hass.config_entries.async_setup(old_entry.entry_id) + assert old_entry.state == ConfigEntryState.LOADED + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT, "old_config_entry_id": old_entry.entry_id}, + data=old_entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_KEY: "this is a test"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_API_KEY: "this is a test", + CONF_LATITUDE: 80, + CONF_LONGITUDE: 80, + CONF_NAME: "ClimaCell", + "old_config_entry_id": old_entry.entry_id, + } + + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(CC_DOMAIN)) == 0 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert "old_config_entry_id" not in entry.data + assert CONF_API_VERSION not in entry.data diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py new file mode 100644 index 00000000000..f0c963d0dee --- /dev/null +++ b/tests/components/tomorrowio/test_init.py @@ -0,0 +1,151 @@ +"""Tests for Tomorrow.io init.""" +from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN as CC_DOMAIN +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import MIN_CONFIG + +from tests.common import MockConfigEntry +from tests.components.climacell.const import API_V3_ENTRY_DATA + +NEW_NAME = "New Name" + + +async def test_load_and_unload(hass: HomeAssistant) -> None: + """Test loading and unloading entry.""" + data = _get_config_schema(hass, SOURCE_USER)(MIN_CONFIG) + data[CONF_NAME] = "test" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: 1}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 1 + + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 + + +async def test_climacell_migration_logic( + hass: HomeAssistant, climacell_config_entry_update +) -> None: + """Test that climacell config entry is properly migrated.""" + old_data = API_V3_ENTRY_DATA.copy() + old_data[CONF_API_KEY] = "v3apikey" + old_config_entry = MockConfigEntry( + domain=CC_DOMAIN, + data=old_data, + unique_id=_get_unique_id(hass, old_data), + version=1, + ) + old_config_entry.add_to_hass(hass) + # Let's create a device and update its name + dev_reg = dr.async_get(hass) + old_device = dev_reg.async_get_or_create( + config_entry_id=old_config_entry.entry_id, + identifiers={(CC_DOMAIN, old_data[CONF_API_KEY])}, + manufacturer="ClimaCell", + sw_version="v4", + entry_type="service", + name="ClimaCell", + ) + dev_reg.async_update_device(old_device.id, name_by_user=NEW_NAME) + # Now let's create some entity and update some things to see if everything migrates + # over + ent_reg = er.async_get(hass) + old_entity_daily = ent_reg.async_get_or_create( + "weather", + CC_DOMAIN, + f"{_get_unique_id(hass, old_data)}_daily", + config_entry=old_config_entry, + original_name="ClimaCell - Daily", + suggested_object_id="climacell_daily", + device_id=old_device.id, + ) + old_entity_hourly = ent_reg.async_get_or_create( + "weather", + CC_DOMAIN, + f"{_get_unique_id(hass, old_data)}_hourly", + config_entry=old_config_entry, + original_name="ClimaCell - Hourly", + suggested_object_id="climacell_hourly", + device_id=old_device.id, + disabled_by=er.DISABLED_USER, + ) + old_entity_nowcast = ent_reg.async_get_or_create( + "weather", + CC_DOMAIN, + f"{_get_unique_id(hass, old_data)}_nowcast", + config_entry=old_config_entry, + original_name="ClimaCell - Nowcast", + suggested_object_id="climacell_nowcast", + device_id=old_device.id, + ) + ent_reg.async_update_entity(old_entity_daily.entity_id, name=NEW_NAME) + + # Now let's create a new tomorrowio config entry that is supposedly created from a + # climacell import and see what happens - we are also changing the API key to ensure + # that things work as expected + new_data = API_V3_ENTRY_DATA.copy() + new_data[CONF_API_VERSION] = 4 + new_data["old_config_entry_id"] = old_config_entry.entry_id + config_entry = MockConfigEntry( + domain=DOMAIN, + data=new_data, + unique_id=_get_unique_id(hass, new_data), + version=1, + source=SOURCE_IMPORT, + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check that the old device no longer exists + assert dev_reg.async_get(old_device.id) is None + + # Check that the new device was created and that it has the correct name + assert ( + dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)[ + 0 + ].name_by_user + == NEW_NAME + ) + + # Check that the new entities match the old ones (minus the default name) + new_entity_daily = ent_reg.async_get(old_entity_daily.entity_id) + assert new_entity_daily.platform == DOMAIN + assert new_entity_daily.name == NEW_NAME + assert new_entity_daily.original_name == "ClimaCell - Daily" + assert new_entity_daily.device_id != old_device.id + assert new_entity_daily.unique_id == f"{_get_unique_id(hass, new_data)}_daily" + assert new_entity_daily.disabled_by is None + + new_entity_hourly = ent_reg.async_get(old_entity_hourly.entity_id) + assert new_entity_hourly.platform == DOMAIN + assert new_entity_hourly.name is None + assert new_entity_hourly.original_name == "ClimaCell - Hourly" + assert new_entity_hourly.device_id != old_device.id + assert new_entity_hourly.unique_id == f"{_get_unique_id(hass, new_data)}_hourly" + assert new_entity_hourly.disabled_by == er.DISABLED_USER + + new_entity_nowcast = ent_reg.async_get(old_entity_nowcast.entity_id) + assert new_entity_nowcast.platform == DOMAIN + assert new_entity_nowcast.name is None + assert new_entity_nowcast.original_name == "ClimaCell - Nowcast" + assert new_entity_nowcast.device_id != old_device.id + assert new_entity_nowcast.unique_id == f"{_get_unique_id(hass, new_data)}_nowcast" + assert new_entity_nowcast.disabled_by is None diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py new file mode 100644 index 00000000000..09316ed2518 --- /dev/null +++ b/tests/components/tomorrowio/test_sensor.py @@ -0,0 +1,166 @@ +"""Tests for Tomorrow.io sensor entities.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any +from unittest.mock import patch + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt as dt_util + +from .const import API_V4_ENTRY_DATA + +from tests.common import MockConfigEntry + +CC_SENSOR_ENTITY_ID = "sensor.tomorrow_io_{}" + +O3 = "ozone" +CO = "carbon_monoxide" +NO2 = "nitrogen_dioxide" +SO2 = "sulphur_dioxide" +PM25 = "particulate_matter_2_5_mm" +PM10 = "particulate_matter_10_mm" +MEP_AQI = "china_mep_air_quality_index" +MEP_HEALTH_CONCERN = "china_mep_health_concern" +MEP_PRIMARY_POLLUTANT = "china_mep_primary_pollutant" +EPA_AQI = "us_epa_air_quality_index" +EPA_HEALTH_CONCERN = "us_epa_health_concern" +EPA_PRIMARY_POLLUTANT = "us_epa_primary_pollutant" +FIRE_INDEX = "fire_index" +GRASS_POLLEN = "grass_pollen_index" +WEED_POLLEN = "weed_pollen_index" +TREE_POLLEN = "tree_pollen_index" +FEELS_LIKE = "feels_like" +DEW_POINT = "dew_point" +PRESSURE_SURFACE_LEVEL = "pressure_surface_level" +SNOW_ACCUMULATION = "snow_accumulation" +ICE_ACCUMULATION = "ice_accumulation" +GHI = "global_horizontal_irradiance" +CLOUD_BASE = "cloud_base" +CLOUD_COVER = "cloud_cover" +CLOUD_CEILING = "cloud_ceiling" +WIND_GUST = "wind_gust" +PRECIPITATION_TYPE = "precipitation_type" + +V3_FIELDS = [ + O3, + CO, + NO2, + SO2, + PM25, + PM10, + MEP_AQI, + MEP_HEALTH_CONCERN, + MEP_PRIMARY_POLLUTANT, + EPA_AQI, + EPA_HEALTH_CONCERN, + EPA_PRIMARY_POLLUTANT, + FIRE_INDEX, + GRASS_POLLEN, + WEED_POLLEN, + TREE_POLLEN, +] + +V4_FIELDS = [ + *V3_FIELDS, + FEELS_LIKE, + DEW_POINT, + PRESSURE_SURFACE_LEVEL, + GHI, + CLOUD_BASE, + CLOUD_COVER, + CLOUD_CEILING, + WIND_GUST, + PRECIPITATION_TYPE, +] + + +@callback +def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: + """Enable disabled entity.""" + ent_reg = async_get(hass) + entry = ent_reg.async_get(entity_name) + updated_entry = ent_reg.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def _setup( + hass: HomeAssistant, sensors: list[str], config: dict[str, Any] +) -> State: + """Set up entry and return entity state.""" + with patch( + "homeassistant.util.dt.utcnow", + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), + ): + data = _get_config_schema(hass, SOURCE_USER)(config) + data[CONF_NAME] = DEFAULT_NAME + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + for entity_name in sensors: + _enable_entity(hass, CC_SENSOR_ENTITY_ID.format(entity_name)) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == len(sensors) + + +def check_sensor_state(hass: HomeAssistant, entity_name: str, value: str): + """Check the state of a Tomorrow.io sensor.""" + state = hass.states.get(CC_SENSOR_ENTITY_ID.format(entity_name)) + assert state + assert state.state == value + assert state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + + +async def test_v4_sensor(hass: HomeAssistant) -> None: + """Test v4 sensor data.""" + await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA) + check_sensor_state(hass, O3, "94.46") + check_sensor_state(hass, CO, "0.63") + check_sensor_state(hass, NO2, "20.81") + check_sensor_state(hass, SO2, "4.47") + check_sensor_state(hass, PM25, "5.3") + check_sensor_state(hass, PM10, "20.13") + check_sensor_state(hass, MEP_AQI, "23") + check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") + check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") + check_sensor_state(hass, EPA_AQI, "24") + check_sensor_state(hass, EPA_HEALTH_CONCERN, "good") + check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") + check_sensor_state(hass, FIRE_INDEX, "10") + check_sensor_state(hass, GRASS_POLLEN, "none") + check_sensor_state(hass, WEED_POLLEN, "none") + check_sensor_state(hass, TREE_POLLEN, "none") + check_sensor_state(hass, FEELS_LIKE, "38.5") + check_sensor_state(hass, DEW_POINT, "22.68") + check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "997.97") + check_sensor_state(hass, GHI, "0.0") + check_sensor_state(hass, CLOUD_BASE, "1.19") + check_sensor_state(hass, CLOUD_COVER, "100") + check_sensor_state(hass, CLOUD_CEILING, "1.19") + check_sensor_state(hass, WIND_GUST, "5.65") + check_sensor_state(hass, PRECIPITATION_TYPE, "rain") diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py new file mode 100644 index 00000000000..f47e8ed22d8 --- /dev/null +++ b/tests/components/tomorrowio/test_weather.py @@ -0,0 +1,245 @@ +"""Tests for Tomorrow.io weather entity.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any +from unittest.mock import patch + +from homeassistant.components.tomorrowio.config_flow import ( + _get_config_schema, + _get_unique_id, +) +from homeassistant.components.tomorrowio.const import ( + ATTRIBUTION, + CONF_TIMESTEP, + DEFAULT_NAME, + DEFAULT_TIMESTEP, + DOMAIN, +) +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_OZONE, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, CONF_NAME +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt as dt_util + +from .const import API_V4_ENTRY_DATA + +from tests.common import MockConfigEntry + + +@callback +def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: + """Enable disabled entity.""" + ent_reg = async_get(hass) + entry = ent_reg.async_get(entity_name) + updated_entry = ent_reg.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: + """Set up entry and return entity state.""" + with patch( + "homeassistant.util.dt.utcnow", + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), + ): + data = _get_config_schema(hass, SOURCE_USER)(config) + data[CONF_NAME] = DEFAULT_NAME + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: DEFAULT_TIMESTEP}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + for entity_name in ("hourly", "nowcast"): + _enable_entity(hass, f"weather.tomorrow_io_{entity_name}") + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 3 + + return hass.states.get("weather.tomorrow_io_daily") + + +async def test_v4_weather(hass: HomeAssistant) -> None: + """Test v4 weather data.""" + weather_state = await _setup(hass, API_V4_ENTRY_DATA) + assert weather_state.state == ATTR_CONDITION_SUNNY + assert weather_state.attributes[ATTR_ATTRIBUTION] == ATTRIBUTION + assert weather_state.attributes[ATTR_FORECAST] == [ + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, + ATTR_FORECAST_TIME: "2021-03-07T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 8, + ATTR_FORECAST_TEMP_LOW: -3, + ATTR_FORECAST_WIND_BEARING: 239.6, + ATTR_FORECAST_WIND_SPEED: 4.24, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-08T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 10, + ATTR_FORECAST_TEMP_LOW: -3, + ATTR_FORECAST_WIND_BEARING: 262.82, + ATTR_FORECAST_WIND_SPEED: 3.24, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-09T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 19, + ATTR_FORECAST_TEMP_LOW: 0, + ATTR_FORECAST_WIND_BEARING: 229.3, + ATTR_FORECAST_WIND_SPEED: 3.15, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-10T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 18, + ATTR_FORECAST_TEMP_LOW: 3, + ATTR_FORECAST_WIND_BEARING: 149.91, + ATTR_FORECAST_WIND_SPEED: 4.76, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-11T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 19, + ATTR_FORECAST_TEMP_LOW: 9, + ATTR_FORECAST_WIND_BEARING: 210.45, + ATTR_FORECAST_WIND_SPEED: 7.01, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, + ATTR_FORECAST_TIME: "2021-03-12T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0.12, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 20, + ATTR_FORECAST_TEMP_LOW: 12, + ATTR_FORECAST_WIND_BEARING: 217.98, + ATTR_FORECAST_WIND_SPEED: 5.5, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-13T11:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, + ATTR_FORECAST_TEMP: 12, + ATTR_FORECAST_TEMP_LOW: 6, + ATTR_FORECAST_WIND_BEARING: 58.79, + ATTR_FORECAST_WIND_SPEED: 4.35, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, + ATTR_FORECAST_TIME: "2021-03-14T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 23.96, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_WIND_BEARING: 70.25, + ATTR_FORECAST_WIND_SPEED: 7.26, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, + ATTR_FORECAST_TIME: "2021-03-15T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.46, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: -1, + ATTR_FORECAST_WIND_BEARING: 84.47, + ATTR_FORECAST_WIND_SPEED: 7.1, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-16T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 6, + ATTR_FORECAST_TEMP_LOW: -2, + ATTR_FORECAST_WIND_BEARING: 103.85, + ATTR_FORECAST_WIND_SPEED: 3.0, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-17T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, + ATTR_FORECAST_TEMP: 11, + ATTR_FORECAST_TEMP_LOW: 1, + ATTR_FORECAST_WIND_BEARING: 145.41, + ATTR_FORECAST_WIND_SPEED: 3.25, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, + ATTR_FORECAST_TIME: "2021-03-18T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 0, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 10, + ATTR_FORECAST_TEMP: 12, + ATTR_FORECAST_TEMP_LOW: 5, + ATTR_FORECAST_WIND_BEARING: 62.99, + ATTR_FORECAST_WIND_SPEED: 2.94, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, + ATTR_FORECAST_TIME: "2021-03-19T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 2.93, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, + ATTR_FORECAST_TEMP: 9, + ATTR_FORECAST_TEMP_LOW: 4, + ATTR_FORECAST_WIND_BEARING: 68.54, + ATTR_FORECAST_WIND_SPEED: 6.22, + }, + { + ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, + ATTR_FORECAST_TIME: "2021-03-20T10:00:00+00:00", + ATTR_FORECAST_PRECIPITATION: 1.22, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 33.3, + ATTR_FORECAST_TEMP: 5, + ATTR_FORECAST_TEMP_LOW: 2, + ATTR_FORECAST_WIND_BEARING: 56.98, + ATTR_FORECAST_WIND_SPEED: 7.76, + }, + ] + assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - Daily" + assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 + assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 102776.91 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 7 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 13.12 + assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 4.17 From b160931603a3b1a1442e1053e595ee72f06e3d3d Mon Sep 17 00:00:00 2001 From: Scott Bradshaw Date: Sat, 19 Mar 2022 05:45:18 -0400 Subject: [PATCH 0535/1054] Bump python-smarttub dependency to 0.0.30 (#68356) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 9bec5d4a72e..90f85b6c839 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.29"], + "requirements": ["python-smarttub==0.0.30"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/requirements_all.txt b/requirements_all.txt index 25510ad8f59..d21b52d2f99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1915,7 +1915,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.29 +python-smarttub==0.0.30 # homeassistant.components.sochain python-sochain-api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ec5de4531f..b2ef1a65420 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1230,7 +1230,7 @@ python-nest==4.2.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.29 +python-smarttub==0.0.30 # homeassistant.components.songpal python-songpal==0.14.1 From ce30b32add41a568b2279473cee3eb5f24ad8e28 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Mar 2022 12:14:05 +0100 Subject: [PATCH 0536/1054] Update pytest to 7.1.1 (#68366) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2c4016e9b12..a319d6c3005 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,7 +24,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==2.1.0 pytest-xdist==2.5.0 -pytest==7.1.0 +pytest==7.1.1 requests_mock==1.9.2 respx==0.19.0 stdlib-list==0.7.0 From 126320529e2153c11c2b3897689180a103e25712 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 19 Mar 2022 12:37:04 +0100 Subject: [PATCH 0537/1054] Add zha typing [core.discovery] (1) (#68359) * Add zha typing [core.discovery] (1) * Fix circular import --- .../components/zha/core/discovery.py | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 26323793e13..9f7523d41f0 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import Counter from collections.abc import Callable import logging +from typing import TYPE_CHECKING from homeassistant import const as ha_const from homeassistant.core import HomeAssistant, callback @@ -11,9 +12,11 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.typing import ConfigType -from . import const as zha_const, registries as zha_regs, typing as zha_typing +from . import const as zha_const, registries as zha_regs from .. import ( # noqa: F401 pylint: disable=unused-import, alarm_control_panel, binary_sensor, @@ -32,16 +35,23 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, ) from .channels import base +if TYPE_CHECKING: + from ..entity import ZhaEntity + from .channels import ChannelPool + from .device import ZHADevice + from .gateway import ZHAGateway + from .group import ZHAGroup + _LOGGER = logging.getLogger(__name__) @callback async def async_add_entities( - _async_add_entities: Callable, + _async_add_entities: AddEntitiesCallback, entities: list[ tuple[ - zha_typing.ZhaEntityType, - tuple[str, zha_typing.ZhaDeviceType, list[zha_typing.ChannelType]], + type[ZhaEntity], + tuple[str, ZHADevice, list[base.ZigbeeChannel]], ] ], update_before_add: bool = True, @@ -50,20 +60,20 @@ async def async_add_entities( if not entities: return to_add = [ent_cls.create_entity(*args) for ent_cls, args in entities] - to_add = [entity for entity in to_add if entity is not None] - _async_add_entities(to_add, update_before_add=update_before_add) + entities_to_add = [entity for entity in to_add if entity is not None] + _async_add_entities(entities_to_add, update_before_add=update_before_add) entities.clear() class ProbeEndpoint: """All discovered channels and entities of an endpoint.""" - def __init__(self): + def __init__(self) -> None: """Initialize instance.""" - self._device_configs = {} + self._device_configs: ConfigType = {} @callback - def discover_entities(self, channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_entities(self, channel_pool: ChannelPool) -> None: """Process an endpoint on a zigpy device.""" self.discover_by_device_type(channel_pool) self.discover_multi_entities(channel_pool) @@ -71,12 +81,14 @@ class ProbeEndpoint: zha_regs.ZHA_ENTITIES.clean_up() @callback - def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_by_device_type(self, channel_pool: ChannelPool) -> None: """Process an endpoint on a zigpy device.""" unique_id = channel_pool.unique_id - component = self._device_configs.get(unique_id, {}).get(ha_const.CONF_TYPE) + component: str | None = self._device_configs.get(unique_id, {}).get( + ha_const.CONF_TYPE + ) if component is None: ep_profile_id = channel_pool.endpoint.profile_id ep_device_type = channel_pool.endpoint.device_type @@ -93,7 +105,7 @@ class ProbeEndpoint: channel_pool.async_new_entity(component, entity_class, unique_id, claimed) @callback - def discover_by_cluster_id(self, channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_by_cluster_id(self, channel_pool: ChannelPool) -> None: """Process an endpoint on a zigpy device.""" items = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.items() @@ -125,8 +137,8 @@ class ProbeEndpoint: @staticmethod def probe_single_cluster( component: str, - channel: zha_typing.ChannelType, - ep_channels: zha_typing.ChannelPoolType, + channel: base.ZigbeeChannel, + ep_channels: ChannelPool, ) -> None: """Probe specified cluster for specific component.""" if component is None or component not in zha_const.PLATFORMS: @@ -142,9 +154,7 @@ class ProbeEndpoint: ep_channels.claim_channels(claimed) ep_channels.async_new_entity(component, entity_class, unique_id, claimed) - def handle_on_off_output_cluster_exception( - self, ep_channels: zha_typing.ChannelPoolType - ) -> None: + def handle_on_off_output_cluster_exception(self, ep_channels: ChannelPool) -> None: """Process output clusters of the endpoint.""" profile_id = ep_channels.endpoint.profile_id @@ -167,7 +177,7 @@ class ProbeEndpoint: @staticmethod @callback - def discover_multi_entities(channel_pool: zha_typing.ChannelPoolType) -> None: + def discover_multi_entities(channel_pool: ChannelPool) -> None: """Process an endpoint on and discover multiple entities.""" ep_profile_id = channel_pool.endpoint.profile_id @@ -209,7 +219,9 @@ class ProbeEndpoint: def initialize(self, hass: HomeAssistant) -> None: """Update device overrides config.""" - zha_config = hass.data[zha_const.DATA_ZHA].get(zha_const.DATA_ZHA_CONFIG, {}) + zha_config: ConfigType = hass.data[zha_const.DATA_ZHA].get( + zha_const.DATA_ZHA_CONFIG, {} + ) if overrides := zha_config.get(zha_const.CONF_DEVICE_CONFIG): self._device_configs.update(overrides) @@ -217,10 +229,11 @@ class ProbeEndpoint: class GroupProbe: """Determine the appropriate component for a group.""" - def __init__(self): + _hass: HomeAssistant + + def __init__(self) -> None: """Initialize instance.""" - self._hass = None - self._unsubs = [] + self._unsubs: list[Callable[[], None]] = [] def initialize(self, hass: HomeAssistant) -> None: """Initialize the group probe.""" @@ -231,7 +244,7 @@ class GroupProbe: ) ) - def cleanup(self): + def cleanup(self) -> None: """Clean up on when zha shuts down.""" for unsub in self._unsubs[:]: unsub() @@ -240,13 +253,15 @@ class GroupProbe: @callback def _reprobe_group(self, group_id: int) -> None: """Reprobe a group for entities after its members change.""" - zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = self._hass.data[zha_const.DATA_ZHA][ + zha_const.DATA_ZHA_GATEWAY + ] if (zha_group := zha_gateway.groups.get(group_id)) is None: return self.discover_group_entities(zha_group) @callback - def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None: + def discover_group_entities(self, group: ZHAGroup) -> None: """Process a group and create any entities that are needed.""" # only create a group entity if there are 2 or more members in a group if len(group.members) < 2: @@ -262,7 +277,9 @@ class GroupProbe: if not entity_domains: return - zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = self._hass.data[zha_const.DATA_ZHA][ + zha_const.DATA_ZHA_GATEWAY + ] for domain in entity_domains: entity_class = zha_regs.ZHA_ENTITIES.get_group_entity(domain) if entity_class is None: @@ -281,12 +298,12 @@ class GroupProbe: async_dispatcher_send(self._hass, zha_const.SIGNAL_ADD_ENTITIES) @staticmethod - def determine_entity_domains( - hass: HomeAssistant, group: zha_typing.ZhaGroupType - ) -> list[str]: + def determine_entity_domains(hass: HomeAssistant, group: ZHAGroup) -> list[str]: """Determine the entity domains for this group.""" entity_domains: list[str] = [] - zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[zha_const.DATA_ZHA][ + zha_const.DATA_ZHA_GATEWAY + ] all_domain_occurrences = [] for member in group.members: if member.device.is_coordinator: From cca0ecc5da56a24f47f7f629eafc12c88dc5590d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Mar 2022 15:18:35 +0100 Subject: [PATCH 0538/1054] Update sentry-sdk to 1.5.8 (#68367) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index ff8f2bb7ec8..a8af46270ae 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.5.7"], + "requirements": ["sentry-sdk==1.5.8"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index d21b52d2f99..f3385e0579a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ sendgrid==6.8.2 sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.7 +sentry-sdk==1.5.8 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2ef1a65420..01ec75a0e56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ securetar==2022.2.0 sense_energy==0.10.2 # homeassistant.components.sentry -sentry-sdk==1.5.7 +sentry-sdk==1.5.8 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 8220817d479a101f06fa029b221b2faca496260a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 19 Mar 2022 11:26:44 -0400 Subject: [PATCH 0539/1054] Switch zwave_js redact keys from tuple to set (#68375) --- homeassistant/components/zwave_js/diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index dd9860edeb0..c9e45f09685 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -15,7 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DATA_CLIENT, DOMAIN from .helpers import get_home_and_node_id_from_device_entry -TO_REDACT = ("homeId", "location") +TO_REDACT = {"homeId", "location"} async def async_get_config_entry_diagnostics( From 5027e1bcff99b5367f4d5a91b8f17570e9e2f6cc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 19 Mar 2022 10:24:32 -0700 Subject: [PATCH 0540/1054] Mark stream available on idle timeout (#68380) Mark stream as available on idle timeout so that the frontend can still interact with it. In particular, the Frontend won't interact with camera objects that are not available e.g. from picture glance card. Issue #67922 --- homeassistant/components/stream/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 9ec389fb4a8..abaf367486d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -371,17 +371,18 @@ class Stream: wait_timeout, redact_credentials(str(self.source)), ) - self._worker_finished() - - def _worker_finished(self) -> None: - """Schedule cleanup of all outputs.""" @callback - def remove_outputs() -> None: + def worker_finished() -> None: + # The worker is no checking availability of the stream and can no longer track + # availability so mark it as available, otherwise the frontend may not be able to + # interact with the stream. + if not self.available: + self._async_update_state(True) for provider in self.outputs().values(): self.remove_provider(provider) - self.hass.loop.call_soon_threadsafe(remove_outputs) + self.hass.loop.call_soon_threadsafe(worker_finished) def stop(self) -> None: """Remove outputs and access token.""" From 8187541d413eaef32c26585823d2559390f0793b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 19 Mar 2022 21:27:04 +0100 Subject: [PATCH 0541/1054] Hue integration: update errors that should be supressed (#68337) --- homeassistant/components/hue/v2/group.py | 8 ++++++++ homeassistant/components/hue/v2/light.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 31c5a502853..948609f4c13 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -42,6 +42,14 @@ ALLOWED_ERRORS = [ 'device (groupedLight) is "soft off", command (on) may not have effect', "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "device (grouped_light) has communication issues, command (.on) may not have effect", + 'device (grouped_light) is "soft off", command (.on) may not have effect' + "device (grouped_light) has communication issues, command (.on.on) may not have effect", + 'device (grouped_light) is "soft off", command (.on.on) may not have effect' + "device (light) has communication issues, command (.on) may not have effect", + 'device (light) is "soft off", command (.on) may not have effect', + "device (light) has communication issues, command (.on.on) may not have effect", + 'device (light) is "soft off", command (.on.on) may not have effect', ] diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index ee40222b083..5b4574c717c 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -39,6 +39,10 @@ from .helpers import ( ALLOWED_ERRORS = [ "device (light) has communication issues, command (on) may not have effect", 'device (light) is "soft off", command (on) may not have effect', + "device (light) has communication issues, command (.on) may not have effect", + 'device (light) is "soft off", command (.on) may not have effect', + "device (light) has communication issues, command (.on.on) may not have effect", + 'device (light) is "soft off", command (.on.on) may not have effect', ] From 0df88b80e78e0774c545d77ced7ea7960a6b51ee Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 19 Mar 2022 17:34:30 -0300 Subject: [PATCH 0542/1054] Bump broadlink to 0.18.1 (#68391) --- homeassistant/components/broadlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index db1601edd67..e3df0dc829d 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.18.0"], + "requirements": ["broadlink==0.18.1"], "codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index f3385e0579a..97f563bd9fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -421,7 +421,7 @@ boto3==1.20.24 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.18.0 +broadlink==0.18.1 # homeassistant.components.brother brother==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01ec75a0e56..b6fab08593e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -310,7 +310,7 @@ boschshcpy==0.2.30 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.18.0 +broadlink==0.18.1 # homeassistant.components.brother brother==1.1.0 From fed447a3f40b21c0411fc7b7258df33996e82d30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 10:40:00 -1000 Subject: [PATCH 0543/1054] Filter IPv6 addreses from enphase_envoy discovery (#68362) --- .../components/enphase_envoy/config_flow.py | 3 ++ .../enphase_envoy/translations/en.json | 3 +- .../enphase_envoy/test_config_flow.py | 41 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index fa43cb61ffe..88310579e72 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.util.network import is_ipv4_address from .const import DOMAIN @@ -86,6 +87,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" + if not is_ipv4_address(discovery_info.host): + return self.async_abort(reason="not_ipv4_address") serial = discovery_info.properties["serialnum"] await self.async_set_unique_id(serial) self.ip_address = discovery_info.host diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index 5d4617ed9fa..ff600fea454 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Device is already configured", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "not_ipv4_address": "Only IPv4 addresess are supported" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index 76179c02e22..caba2296927 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -6,6 +6,7 @@ import httpx from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.enphase_envoy.const import DOMAIN +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -312,8 +313,8 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="1.1.1.1", - addresses=["1.1.1.1"], + host="4.4.4.4", + addresses=["4.4.4.4"], hostname="mock_hostname", name="mock_name", port=None, @@ -324,6 +325,42 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: assert result["type"] == "abort" assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == "4.4.4.4" + + +async def test_zeroconf_serial_already_exists_ignores_ipv6(hass: HomeAssistant) -> None: + """Test serial number already exists from zeroconf but the discovery is ipv6.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.1", + "name": "Envoy", + "username": "test-username", + "password": "test-password", + }, + unique_id="1234", + title="Envoy", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={"serialnum": "1234"}, + type="mock_type", + ), + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_ipv4_address" + assert config_entry.data[CONF_HOST] == "1.1.1.1" async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: From 7ee647cc78f544373f1ee8d822f1fa6ed298dc6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 11:21:28 -1000 Subject: [PATCH 0544/1054] Fix FOREIGN KEY constraint failed when removing state_attributes (#68364) --- homeassistant/components/recorder/purge.py | 62 +++++++--------------- tests/components/recorder/test_purge.py | 4 +- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 0109d68f0f5..a15d22810f4 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -49,9 +49,6 @@ def purge_old_data( state_ids, attributes_ids = _select_state_and_attributes_ids_to_purge( session, purge_before, event_ids ) - attributes_ids = _remove_attributes_ids_used_by_newer_states( - session, purge_before, attributes_ids - ) statistics_runs = _select_statistics_runs_to_purge(session, purge_before) short_term_statistics = _select_short_term_statistics_to_purge( session, purge_before @@ -60,8 +57,10 @@ def purge_old_data( if state_ids: _purge_state_ids(instance, session, state_ids) - if attributes_ids: - _purge_attributes_ids(instance, session, attributes_ids) + if unused_attribute_ids_set := _select_unused_attributes_ids( + session, attributes_ids + ): + _purge_attributes_ids(instance, session, unused_attribute_ids_set) if event_ids: _purge_event_ids(session, event_ids) @@ -121,20 +120,18 @@ def _select_state_and_attributes_ids_to_purge( return state_ids, attributes_ids -def _remove_attributes_ids_used_by_newer_states( - session: Session, purge_before: datetime, attributes_ids: set[int] +def _select_unused_attributes_ids( + session: Session, attributes_ids: set[int] ) -> set[int]: - """Remove attributes ids that are still in use for states we are not purging yet.""" + """Return a set of attributes ids that are not used by any states in the database.""" if not attributes_ids: return set() - keep_attributes_ids = { - state.attributes_id - for state in session.query(States.attributes_id) - .filter(States.last_updated >= purge_before) + to_remove = attributes_ids - { + state[0] + for state in session.query(distinct(States.attributes_id)) .filter(States.attributes_id.in_(attributes_ids)) - .group_by(States.attributes_id) + .all() } - to_remove = attributes_ids - keep_attributes_ids _LOGGER.debug( "Selected %s shared attributes to remove", len(to_remove), @@ -187,9 +184,7 @@ def _purge_state_ids(instance: Recorder, session: Session, state_ids: set[int]) disconnected_rows = ( session.query(States) .filter(States.old_state_id.in_(state_ids)) - .update( - {"old_state_id": None, "attributes_id": None}, synchronize_session=False - ) + .update({"old_state_id": None}, synchronize_session=False) ) _LOGGER.debug("Updated %s states to remove old_state_id", disconnected_rows) @@ -332,27 +327,6 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: return True -def _remove_attributes_ids_used_by_other_entities( - session: Session, entities: list[str], attributes_ids: set[int] -) -> set[int]: - """Remove attributes ids that are still in use for entitiy_ids we are not purging yet.""" - if not attributes_ids: - return set() - keep_attributes_ids = { - state.attributes_id - for state in session.query(States.attributes_id) - .filter(States.entity_id.not_in(entities)) - .filter(States.attributes_id.in_(attributes_ids)) - .group_by(States.attributes_id) - } - to_remove = attributes_ids - keep_attributes_ids - _LOGGER.debug( - "Selected %s shared attributes to remove", - len(to_remove), - ) - return to_remove - - def _purge_filtered_states( instance: Recorder, session: Session, excluded_entity_ids: list[str] ) -> None: @@ -369,15 +343,15 @@ def _purge_filtered_states( ) ) event_ids = [id_ for id_ in event_ids if id_ is not None] - attributes_ids_set = _remove_attributes_ids_used_by_other_entities( - session, excluded_entity_ids, {id_ for id_ in attributes_ids if id_ is not None} - ) _LOGGER.debug( "Selected %s state_ids to remove that should be filtered", len(state_ids) ) _purge_state_ids(instance, session, set(state_ids)) _purge_event_ids(session, event_ids) # type: ignore[arg-type] # type of event_ids already narrowed to 'list[int]' - _purge_attributes_ids(instance, session, attributes_ids_set) + unused_attribute_ids_set = _select_unused_attributes_ids( + session, {id_ for id_ in attributes_ids if id_ is not None} + ) + _purge_attributes_ids(instance, session, unused_attribute_ids_set) def _purge_filtered_events( @@ -390,7 +364,9 @@ def _purge_filtered_events( .limit(MAX_ROWS_TO_PURGE) .all() ) - event_ids: list[int] = [event.event_id for event in events] + event_ids: list[int] = [ + event.event_id for event in events if event.event_id is not None + ] _LOGGER.debug( "Selected %s event_ids to remove that should be filtered", len(event_ids) ) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1bdb391541e..441cbb87a06 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -710,9 +710,9 @@ async def test_purge_filtered_states( assert states_sensor_excluded.count() == 0 assert session.query(States).get(72).old_state_id is None - assert session.query(States).get(72).attributes_id is None + assert session.query(States).get(72).attributes_id == 71 assert session.query(States).get(73).old_state_id is None - assert session.query(States).get(73).attributes_id is None + assert session.query(States).get(73).attributes_id == 71 final_keep_state = session.query(States).get(74) assert final_keep_state.old_state_id == 62 # should have been kept From ead81edcec7746c115649c744380893098dc953b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Mar 2022 14:28:16 -0700 Subject: [PATCH 0545/1054] Handle Hue discovery errors (#68392) --- homeassistant/components/hue/config_flow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0901d9a1e2c..265777814a8 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -6,6 +6,7 @@ import logging from typing import Any from urllib.parse import urlparse +import aiohttp from aiohue import LinkButtonNotPressed, create_app_key from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp from aiohue.util import normalize_bridge_id @@ -70,9 +71,12 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, host: str, bridge_id: str | None = None ) -> DiscoveredHueBridge: """Return a DiscoveredHueBridge object.""" - bridge = await discover_bridge( - host, websession=aiohttp_client.async_get_clientsession(self.hass) - ) + try: + bridge = await discover_bridge( + host, websession=aiohttp_client.async_get_clientsession(self.hass) + ) + except aiohttp.ClientError: + return None if bridge_id is not None: bridge_id = normalize_bridge_id(bridge_id) assert bridge_id == bridge.id From f1f48475f24b5caa98b35557bd044c133c54872d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 19 Mar 2022 22:34:52 +0100 Subject: [PATCH 0546/1054] Add Airzone coordinator tests (#68384) --- tests/components/airzone/test_coordinator.py | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/components/airzone/test_coordinator.py diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py new file mode 100644 index 00000000000..00ef0616b3e --- /dev/null +++ b/tests/components/airzone/test_coordinator.py @@ -0,0 +1,39 @@ +"""Define tests for the Airzone coordinator.""" + +from unittest.mock import MagicMock, patch + +from aiohttp import ClientConnectorError + +from homeassistant.components.airzone.const import DOMAIN +from homeassistant.components.airzone.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .util import CONFIG, HVAC_MOCK + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator_client_connector_error(hass: HomeAssistant): + """Test ClientConnectorError on coordinator update.""" + + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ) as mock_hvac: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + mock_hvac.assert_called_once() + mock_hvac.reset_mock() + + mock_hvac.side_effect = ClientConnectorError(MagicMock(), MagicMock()) + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + mock_hvac.assert_called_once() + + state = hass.states.get("sensor.despacho_temperature") + assert state.state == STATE_UNAVAILABLE From a91888a7f81e1bf467395891764eab25913a98d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Mar 2022 14:39:32 -0700 Subject: [PATCH 0547/1054] Don't use hass.helpers (#68393) --- homeassistant/helpers/entity_component.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a1dba0d6962..1cef123b292 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -201,8 +201,8 @@ class EntityComponent: async def handle_service(call: ServiceCall) -> None: """Handle the service.""" - await self.hass.helpers.service.entity_service_call( - self._platforms.values(), func, call, required_features + await service.entity_service_call( + self.hass, self._platforms.values(), func, call, required_features ) self.hass.services.async_register(self.domain, name, handle_service, schema) From 0c0df07c529e1ddfee751a5d83cf4903fc6cd912 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 20:33:37 -1000 Subject: [PATCH 0548/1054] Avoid hashing attributes when they are already in the cache (#68395) --- homeassistant/components/recorder/__init__.py | 36 +++++++----- homeassistant/components/recorder/const.py | 8 +++ homeassistant/components/recorder/models.py | 56 ++++++++++--------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 2ee6896d032..c188dd6c4b6 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -985,7 +985,7 @@ class Recorder(threading.Thread): if event.event_type == EVENT_STATE_CHANGED: try: dbstate = States.from_event(event) - dbstate_attributes = StateAttributes.from_event(event) + shared_attrs = StateAttributes.shared_attrs_from_event(event) except (TypeError, ValueError) as ex: _LOGGER.warning( "State is not JSON serializable: %s: %s", @@ -995,27 +995,33 @@ class Recorder(threading.Thread): return dbstate.attributes = None - shared_attrs = dbstate_attributes.shared_attrs # Matching attributes found in the pending commit if pending_attributes := self._pending_state_attributes.get(shared_attrs): dbstate.state_attributes = pending_attributes # Matching attributes id found in the cache elif attributes_id := self._state_attributes_ids.get(shared_attrs): dbstate.attributes_id = attributes_id - # Matching attributes found in the database - elif ( - attributes := self.event_session.query(StateAttributes.attributes_id) - .filter(StateAttributes.hash == dbstate_attributes.hash) - .filter(StateAttributes.shared_attrs == shared_attrs) - .first() - ): - dbstate.attributes_id = attributes[0] - self._state_attributes_ids[shared_attrs] = attributes[0] - # No matching attributes found, save them in the DB else: - dbstate.state_attributes = dbstate_attributes - self._pending_state_attributes[shared_attrs] = dbstate_attributes - self.event_session.add(dbstate_attributes) + attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) + # Matching attributes found in the database + if ( + attributes := self.event_session.query( + StateAttributes.attributes_id + ) + .filter(StateAttributes.hash == attr_hash) + .filter(StateAttributes.shared_attrs == shared_attrs) + .first() + ): + dbstate.attributes_id = attributes[0] + self._state_attributes_ids[shared_attrs] = attributes[0] + # No matching attributes found, save them in the DB + else: + dbstate_attributes = StateAttributes( + shared_attrs=shared_attrs, hash=attr_hash + ) + dbstate.state_attributes = dbstate_attributes + self._pending_state_attributes[shared_attrs] = dbstate_attributes + self.event_session.add(dbstate_attributes) if old_state := self._old_states.pop(dbstate.entity_id, None): if old_state.state_id: diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index ae0b37e211a..13f833627a4 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,5 +1,11 @@ """Recorder constants.""" +from functools import partial +import json +from typing import Final + +from homeassistant.helpers.json import JSONEncoder + DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" DOMAIN = "recorder" @@ -17,3 +23,5 @@ MAX_QUEUE_BACKLOG = 30000 MAX_ROWS_TO_PURGE = 998 DB_WORKER_PREFIX = "DbWorker" + +JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":")) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ab874081055..004a98d6e43 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta import json import logging -from typing import TypedDict, overload +from typing import Any, TypedDict, overload from fnvhash import fnv1a_32 from sqlalchemy import ( @@ -35,9 +35,10 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util +from .const import JSON_DUMP + # SQLAlchemy Schema # pylint: disable=invalid-name Base = declarative_base() @@ -116,8 +117,7 @@ class Events(Base): # type: ignore[misc,valid-type] """Create an event database object from a native event.""" return Events( event_type=event.event_type, - event_data=event_data - or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")), + event_data=event_data or JSON_DUMP(event.data), origin=str(event.origin.value), time_fired=event.time_fired, context_id=event.context.id, @@ -186,15 +186,13 @@ class States(Base): # type: ignore[misc,valid-type] ) @staticmethod - def from_event(event): + def from_event(event) -> States: """Create object from a state_changed event.""" entity_id = event.data["entity_id"] - state = event.data.get("new_state") + state: State | None = event.data.get("new_state") + dbstate = States(entity_id=entity_id, attributes=None) - dbstate = States(entity_id=entity_id) - dbstate.attributes = None - - # State got deleted + # None state means the state was removed from the state machine if state is None: dbstate.state = "" dbstate.domain = split_entity_id(entity_id)[0] @@ -208,7 +206,7 @@ class States(Base): # type: ignore[misc,valid-type] return dbstate - def to_native(self, validate_entity_id=True): + def to_native(self, validate_entity_id: bool = True) -> State | None: """Convert to an HA state object.""" try: return State( @@ -221,7 +219,7 @@ class States(Base): # type: ignore[misc,valid-type] process_timestamp(self.last_updated), # Join the events table on event_id to get the context instead # as it will always be there for state_changed events - context=Context(id=None), + context=Context(id=None), # type: ignore[arg-type] validate_entity_id=validate_entity_id, ) except ValueError: @@ -251,23 +249,29 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] ) @staticmethod - def from_event(event): + def from_event(event: Event) -> StateAttributes: """Create object from a state_changed event.""" - state = event.data.get("new_state") - dbstate = StateAttributes() - # State got deleted - if state is None: - dbstate.shared_attrs = "{}" - else: - dbstate.shared_attrs = json.dumps( - dict(state.attributes), - cls=JSONEncoder, - separators=(",", ":"), - ) - dbstate.hash = fnv1a_32(dbstate.shared_attrs.encode("utf-8")) + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) return dbstate - def to_native(self): + @staticmethod + def shared_attrs_from_event(event: Event) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + return "{}" if state is None else JSON_DUMP(state.attributes) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return fnv1a_32(shared_attrs.encode("utf-8")) + + def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: return json.loads(self.shared_attrs) From dbeec1f7da66a0e34be84a28130a03f2488dc0e9 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Sun, 20 Mar 2022 16:35:34 +1000 Subject: [PATCH 0549/1054] Update pyaussiebb to 0.0.14 (#68293) --- homeassistant/components/aussie_broadband/__init__.py | 3 ++- homeassistant/components/aussie_broadband/config_flow.py | 3 ++- homeassistant/components/aussie_broadband/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index f3a07616d93..6136ff6f8f3 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp import ClientError from aussiebb.asyncio import AussieBB +from aussiebb.const import FETCH_TYPES from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType from homeassistant.config_entries import ConfigEntry @@ -31,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await client.login() - services = await client.get_services() + services = await client.get_services(drop_types=FETCH_TYPES) except AuthenticationException as exc: raise ConfigEntryAuthFailed() from exc except ClientError as exc: diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py index 5eaf39853b5..6e101250386 100644 --- a/homeassistant/components/aussie_broadband/config_flow.py +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -5,6 +5,7 @@ from typing import Any from aiohttp import ClientError from aussiebb.asyncio import AussieBB, AuthenticationException +from aussiebb.const import FETCH_TYPES import voluptuous as vol from homeassistant import config_entries @@ -54,7 +55,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data = user_input - self.services = await self.client.get_services() # type: ignore[union-attr] + self.services = await self.client.get_services(drop_types=FETCH_TYPES) # type: ignore[union-attr] if not self.services: return self.async_abort(reason="no_services_found") diff --git a/homeassistant/components/aussie_broadband/manifest.json b/homeassistant/components/aussie_broadband/manifest.json index 5476371f755..0823000956a 100644 --- a/homeassistant/components/aussie_broadband/manifest.json +++ b/homeassistant/components/aussie_broadband/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aussie_broadband", "requirements": [ - "pyaussiebb==0.0.11" + "pyaussiebb==0.0.14" ], "codeowners": [ "@nickw444", @@ -14,4 +14,4 @@ "loggers": [ "aussiebb" ] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 97f563bd9fd..2ce5287b8b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1353,7 +1353,7 @@ pyatome==0.1.1 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.11 +pyaussiebb==0.0.14 # homeassistant.components.balboa pybalboa==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6fab08593e..4f165fe65f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -893,7 +893,7 @@ pyatmo==6.2.4 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.11 +pyaussiebb==0.0.14 # homeassistant.components.balboa pybalboa==0.13 From cf4033b1bc853fc70828c6128ac91cdfb1d5bdaf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 20 Mar 2022 10:25:15 +0100 Subject: [PATCH 0550/1054] Simplify time zone setting in tests (#68330) * Simplify timezone setting in tests * Fix typo * Adjust caldav tests * Adjust input_datetime tests * Adjust time_date tests * Adjust tod tests * Adjust helper tests * Adjust recorder tests * Adjust risco tests * Adjust aemet tests * Adjust flux tests * Adjust forecast_solar tests * Revert unnecessary change in forecast_solar test * Adjust climacell tests * Adjust google tests * Adjust sensor tests * Adjust sonarr tests * Adjust template tests * Adjust zodiac tests Co-authored-by: Martin Hjelmare --- tests/common.py | 2 +- tests/components/aemet/test_sensor.py | 1 + tests/components/aemet/test_weather.py | 1 + tests/components/caldav/test_calendar.py | 63 +++--- tests/components/climacell/test_weather.py | 30 +-- tests/components/config/test_core.py | 2 - tests/components/flux/test_switch.py | 6 + tests/components/forecast_solar/conftest.py | 7 +- .../components/forecast_solar/test_sensor.py | 4 +- tests/components/google/conftest.py | 8 +- tests/components/google/test_init.py | 4 +- tests/components/input_datetime/test_init.py | 190 +++++++++--------- tests/components/jewish_calendar/__init__.py | 7 - .../jewish_calendar/test_binary_sensor.py | 4 +- .../components/jewish_calendar/test_sensor.py | 4 +- .../pvpc_hourly_pricing/test_config_flow.py | 3 +- .../pvpc_hourly_pricing/test_sensor.py | 3 +- tests/components/recorder/test_statistics.py | 10 + tests/components/risco/test_sensor.py | 1 + tests/components/sensor/test_recorder.py | 2 + tests/components/sonarr/test_sensor.py | 4 +- tests/components/sun/test_trigger.py | 11 - tests/components/template/test_sensor.py | 6 +- tests/components/time_date/test_sensor.py | 35 +--- tests/components/tod/test_binary_sensor.py | 71 +++---- tests/components/vallox/test_sensor.py | 29 +-- tests/components/zodiac/test_sensor.py | 1 + tests/conftest.py | 8 +- tests/helpers/test_condition.py | 22 +- tests/helpers/test_event.py | 22 +- tests/helpers/test_template.py | 9 +- tests/test_bootstrap.py | 2 - tests/test_config.py | 4 - 33 files changed, 256 insertions(+), 320 deletions(-) diff --git a/tests/common.py b/tests/common.py index bdebc7217a7..55878f76da7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -282,7 +282,7 @@ async def async_test_home_assistant(loop, load_registries=True): hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 hass.config.elevation = 0 - hass.config.time_zone = "US/Pacific" + hass.config.set_time_zone("US/Pacific") hass.config.units = METRIC_SYSTEM hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True diff --git a/tests/components/aemet/test_sensor.py b/tests/components/aemet/test_sensor.py index 7887139a386..b5e7679dbe6 100644 --- a/tests/components/aemet/test_sensor.py +++ b/tests/components/aemet/test_sensor.py @@ -15,6 +15,7 @@ from .util import async_init_integration async def test_aemet_forecast_create_sensors(hass): """Test creation of forecast sensors.""" + hass.config.set_time_zone("UTC") now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 43acf4c1c87..d1f1889c807 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -30,6 +30,7 @@ from .util import async_init_integration async def test_aemet_weather(hass): """Test states of the weather.""" + hass.config.set_time_zone("UTC") now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( "homeassistant.util.dt.utcnow", return_value=now diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 6993aa97081..7bfb8b620f6 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -222,15 +222,6 @@ CALDAV_CONFIG = { "custom_calendars": [], } -ORIG_TZ = dt.DEFAULT_TIME_ZONE - - -@pytest.fixture(autouse=True) -def reset_tz(): - """Restore the default TZ after test runs.""" - yield - dt.DEFAULT_TIME_ZONE = ORIG_TZ - @pytest.fixture def set_tz(request): @@ -239,21 +230,21 @@ def set_tz(request): @pytest.fixture -def utc(): +def utc(hass): """Set the default TZ to UTC.""" - dt.set_default_time_zone(dt.get_time_zone("UTC")) + hass.config.set_time_zone("UTC") @pytest.fixture -def new_york(): +def new_york(hass): """Set the default TZ to America/New_York.""" - dt.set_default_time_zone(dt.get_time_zone("America/New_York")) + hass.config.set_time_zone("America/New_York") @pytest.fixture -def baghdad(): +def baghdad(hass): """Set the default TZ to Asia/Baghdad.""" - dt.set_default_time_zone(dt.get_time_zone("Asia/Baghdad")) + hass.config.set_time_zone("Asia/Baghdad") @pytest.fixture(autouse=True) @@ -364,8 +355,9 @@ async def test_setup_component_with_one_custom_calendar(hass, mock_dav_client): assert state.name == "HomeOffice" +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 45)) -async def test_ongoing_event(mock_now, hass, calendar): +async def test_ongoing_event(mock_now, hass, calendar, set_tz): """Test that the ongoing event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -385,8 +377,9 @@ async def test_ongoing_event(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 30)) -async def test_just_ended_event(mock_now, hass, calendar): +async def test_just_ended_event(mock_now, hass, calendar, set_tz): """Test that the next ongoing event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -406,8 +399,9 @@ async def test_just_ended_event(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(17, 00)) -async def test_ongoing_event_different_tz(mock_now, hass, calendar): +async def test_ongoing_event_different_tz(mock_now, hass, calendar, set_tz): """Test that the ongoing event with another timezone is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -427,8 +421,9 @@ async def test_ongoing_event_different_tz(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(19, 10)) -async def test_ongoing_floating_event_returned(mock_now, hass, calendar): +async def test_ongoing_floating_event_returned(mock_now, hass, calendar, set_tz): """Test that floating events without timezones work.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -448,8 +443,9 @@ async def test_ongoing_floating_event_returned(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(8, 30)) -async def test_ongoing_event_with_offset(mock_now, hass, calendar): +async def test_ongoing_event_with_offset(mock_now, hass, calendar, set_tz): """Test that the offset is taken into account.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -469,8 +465,9 @@ async def test_ongoing_event_with_offset(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(12, 00)) -async def test_matching_filter(mock_now, hass, calendar): +async def test_matching_filter(mock_now, hass, calendar, set_tz): """Test that the matching event is returned.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ @@ -495,8 +492,9 @@ async def test_matching_filter(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(12, 00)) -async def test_matching_filter_real_regexp(mock_now, hass, calendar): +async def test_matching_filter_real_regexp(mock_now, hass, calendar, set_tz): """Test that the event matching the regexp is returned.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ @@ -625,8 +623,9 @@ async def test_all_day_event_returned_late(hass, calendar, set_tz): ) +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(21, 45)) -async def test_event_rrule(mock_now, hass, calendar): +async def test_event_rrule(mock_now, hass, calendar, set_tz): """Test that the future recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -646,8 +645,9 @@ async def test_event_rrule(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(22, 15)) -async def test_event_rrule_ongoing(mock_now, hass, calendar): +async def test_event_rrule_ongoing(mock_now, hass, calendar, set_tz): """Test that the current recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -667,8 +667,9 @@ async def test_event_rrule_ongoing(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(22, 45)) -async def test_event_rrule_duration(mock_now, hass, calendar): +async def test_event_rrule_duration(mock_now, hass, calendar, set_tz): """Test that the future recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -688,8 +689,9 @@ async def test_event_rrule_duration(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(23, 15)) -async def test_event_rrule_duration_ongoing(mock_now, hass, calendar): +async def test_event_rrule_duration_ongoing(mock_now, hass, calendar, set_tz): """Test that the ongoing recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -709,8 +711,9 @@ async def test_event_rrule_duration_ongoing(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch("homeassistant.util.dt.now", return_value=_local_datetime(23, 37)) -async def test_event_rrule_endless(mock_now, hass, calendar): +async def test_event_rrule_endless(mock_now, hass, calendar, set_tz): """Test that the endless recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -798,11 +801,12 @@ async def test_event_rrule_all_day_late(hass, calendar, set_tz): ) +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch( "homeassistant.util.dt.now", return_value=dt.as_local(datetime.datetime(2015, 11, 27, 0, 15)), ) -async def test_event_rrule_hourly_on_first(mock_now, hass, calendar): +async def test_event_rrule_hourly_on_first(mock_now, hass, calendar, set_tz): """Test that the endless recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() @@ -822,11 +826,12 @@ async def test_event_rrule_hourly_on_first(mock_now, hass, calendar): } +@pytest.mark.parametrize("set_tz", ["utc"], indirect=True) @patch( "homeassistant.util.dt.now", return_value=dt.as_local(datetime.datetime(2015, 11, 27, 11, 15)), ) -async def test_event_rrule_hourly_on_last(mock_now, hass, calendar): +async def test_event_rrule_hourly_on_last(mock_now, hass, calendar, set_tz): """Test that the endless recurring event is returned.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 3bac69dcbc5..d5385b6cfd5 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -91,7 +91,7 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_FORECAST] == [ { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SUNNY, - ATTR_FORECAST_TIME: "2021-03-07T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-07T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 7, @@ -99,7 +99,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-08T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-08T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 10, @@ -107,7 +107,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-09T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-09T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 19, @@ -115,7 +115,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-10T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-10T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 18, @@ -123,7 +123,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-11T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-11T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, ATTR_FORECAST_TEMP: 20, @@ -131,7 +131,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-12T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0.0457, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, ATTR_FORECAST_TEMP: 20, @@ -139,7 +139,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-13T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-13T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, ATTR_FORECAST_TEMP: 16, @@ -147,7 +147,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-14T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", ATTR_FORECAST_PRECIPITATION: 1.0744, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, ATTR_FORECAST_TEMP: 6, @@ -155,7 +155,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, - ATTR_FORECAST_TIME: "2021-03-15T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts ATTR_FORECAST_PRECIPITATION: 7.3050, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, ATTR_FORECAST_TEMP: 1, @@ -163,7 +163,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-16T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.0051, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, ATTR_FORECAST_TEMP: 6, @@ -171,7 +171,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-17T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-17T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 0, ATTR_FORECAST_TEMP: 11, @@ -179,7 +179,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-18T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-18T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, ATTR_FORECAST_TEMP: 12, @@ -187,7 +187,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-19T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.1778, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, ATTR_FORECAST_TEMP: 9, @@ -195,7 +195,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, - ATTR_FORECAST_TIME: "2021-03-20T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 1.2319, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, ATTR_FORECAST_TEMP: 5, @@ -203,7 +203,7 @@ async def test_v3_weather( }, { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, - ATTR_FORECAST_TIME: "2021-03-21T00:00:00+00:00", + ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", ATTR_FORECAST_PRECIPITATION: 0.0432, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, ATTR_FORECAST_TEMP: 7, diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index b78ed50cdf2..33309f6b6c6 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -10,8 +10,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location -ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture async def client(hass, hass_ws_client): diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 275c10c5592..19d5c064e82 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -23,6 +23,12 @@ from tests.common import ( ) +@pytest.fixture(autouse=True) +def set_utc(hass): + """Set timezone to UTC.""" + hass.config.set_time_zone("UTC") + + async def test_valid_config(hass): """Test configuration.""" assert await async_setup_component( diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 6e1adc14b7f..408c5423861 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -43,8 +43,11 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_forecast_solar() -> Generator[None, MagicMock, None]: - """Return a mocked Forecast.Solar client.""" +def mock_forecast_solar(hass) -> Generator[None, MagicMock, None]: + """Return a mocked Forecast.Solar client. + + hass fixture included because it sets the time zone. + """ with patch( "homeassistant.components.forecast_solar.ForecastSolar", autospec=True ) as forecast_solar_mock: diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index ee8afe5794b..a05acb5bc16 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -68,7 +68,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" - assert state.state == "2021-06-27T13:00:00+00:00" + assert state.state == "2021-06-27T20:00:00+00:00" # Timestamp sensor is UTC assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP @@ -80,7 +80,7 @@ async def test_sensors( assert entry assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" - assert state.state == "2021-06-27T14:00:00+00:00" + assert state.state == "2021-06-27T21:00:00+00:00" # Timestamp sensor is UTC assert ( state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" ) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index c3b8ae4e172..102b8a1ccc6 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -15,13 +15,10 @@ from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry -ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE - ApiResult = Callable[[dict[str, Any]], None] ComponentSetup = Callable[[], Awaitable[bool]] _T = TypeVar("_T") @@ -252,10 +249,7 @@ def set_time_zone(hass): """Set the time zone for the tests.""" # Set our timezone to CST/Regina so we can check calculations # This keeps UTC-6 all year round - hass.config.time_zone = "CST" - dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina")) - yield - dt_util.set_default_time_zone(ORIG_TIMEZONE) + hass.config.set_time_zone("America/Regina") @pytest.fixture diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 6fc575ba03d..49e10d137b6 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -417,11 +417,11 @@ async def test_add_event_date_time( "description": "Description", "start": { "dateTime": start_datetime.isoformat(timespec="seconds"), - "timeZone": "CST", + "timeZone": "America/Regina", }, "end": { "dateTime": end_datetime.isoformat(timespec="seconds"), - "timeZone": "CST", + "timeZone": "America/Regina", }, }, ) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 0a968caf67f..28ca2ab02bd 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -38,8 +38,6 @@ INITIAL_DATE = "2020-01-10" INITIAL_TIME = "23:45:56" INITIAL_DATETIME = f"{INITIAL_DATE} {INITIAL_TIME}" -ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture def storage_setup(hass, hass_storage): @@ -131,7 +129,9 @@ async def test_set_datetime(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) + dt_obj = datetime.datetime( + 2017, 9, 7, 19, 46, 30, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) await async_set_date_and_time(hass, entity_id, dt_obj) @@ -157,7 +157,9 @@ async def test_set_datetime_2(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) + dt_obj = datetime.datetime( + 2017, 9, 7, 19, 46, 30, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) await async_set_datetime(hass, entity_id, dt_obj) @@ -183,7 +185,9 @@ async def test_set_datetime_3(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) + dt_obj = datetime.datetime( + 2017, 9, 7, 19, 46, 30, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) @@ -649,101 +653,97 @@ async def test_setup_no_config(hass, hass_admin_user): async def test_timestamp(hass): """Test timestamp.""" - try: - dt_util.set_default_time_zone(dt_util.get_time_zone("America/Los_Angeles")) + hass.config.set_time_zone("America/Los_Angeles") - assert await async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "test_datetime_initial_with_tz": { - "has_time": True, - "has_date": True, - "initial": "2020-12-13 10:00:00+01:00", - }, - "test_datetime_initial_without_tz": { - "has_time": True, - "has_date": True, - "initial": "2020-12-13 10:00:00", - }, - "test_time_initial": { - "has_time": True, - "has_date": False, - "initial": "10:00:00", - }, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_datetime_initial_with_tz": { + "has_time": True, + "has_date": True, + "initial": "2020-12-13 10:00:00+01:00", + }, + "test_datetime_initial_without_tz": { + "has_time": True, + "has_date": True, + "initial": "2020-12-13 10:00:00", + }, + "test_time_initial": { + "has_time": True, + "has_date": False, + "initial": "10:00:00", + }, + } + }, + ) - # initial has been converted to the set timezone - state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz") - assert state_with_tz is not None - # Timezone LA is UTC-8 => timestamp carries +01:00 => delta is -9 => 10:00 - 09:00 => 01:00 - assert state_with_tz.state == "2020-12-13 01:00:00" - assert ( - dt_util.as_local( - dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP]) - ).strftime(FMT_DATETIME) - == "2020-12-13 01:00:00" - ) + # initial has been converted to the set timezone + state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz") + assert state_with_tz is not None + # Timezone LA is UTC-8 => timestamp carries +01:00 => delta is -9 => 10:00 - 09:00 => 01:00 + assert state_with_tz.state == "2020-12-13 01:00:00" + assert ( + dt_util.as_local( + dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP]) + ).strftime(FMT_DATETIME) + == "2020-12-13 01:00:00" + ) - # initial has been interpreted as being part of set timezone - state_without_tz = hass.states.get( - "input_datetime.test_datetime_initial_without_tz" - ) - assert state_without_tz is not None - assert state_without_tz.state == "2020-12-13 10:00:00" - # Timezone LA is UTC-8 => timestamp has no zone (= assumed local) => delta to UTC is +8 => 10:00 + 08:00 => 18:00 - assert ( - dt_util.utc_from_timestamp( - state_without_tz.attributes[ATTR_TIMESTAMP] - ).strftime(FMT_DATETIME) - == "2020-12-13 18:00:00" - ) - assert ( - dt_util.as_local( - dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) - ).strftime(FMT_DATETIME) - == "2020-12-13 10:00:00" - ) - # Use datetime.datetime.fromtimestamp - assert ( - dt_util.as_local( - datetime.datetime.fromtimestamp( - state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc - ) - ).strftime(FMT_DATETIME) - == "2020-12-13 10:00:00" - ) + # initial has been interpreted as being part of set timezone + state_without_tz = hass.states.get( + "input_datetime.test_datetime_initial_without_tz" + ) + assert state_without_tz is not None + assert state_without_tz.state == "2020-12-13 10:00:00" + # Timezone LA is UTC-8 => timestamp has no zone (= assumed local) => delta to UTC is +8 => 10:00 + 08:00 => 18:00 + assert ( + dt_util.utc_from_timestamp( + state_without_tz.attributes[ATTR_TIMESTAMP] + ).strftime(FMT_DATETIME) + == "2020-12-13 18:00:00" + ) + assert ( + dt_util.as_local( + dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) + # Use datetime.datetime.fromtimestamp + assert ( + dt_util.as_local( + datetime.datetime.fromtimestamp( + state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc + ) + ).strftime(FMT_DATETIME) + == "2020-12-13 10:00:00" + ) - # Test initial time sets timestamp correctly. - state_time = hass.states.get("input_datetime.test_time_initial") - assert state_time is not None - assert state_time.state == "10:00:00" - assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 + # Test initial time sets timestamp correctly. + state_time = hass.states.get("input_datetime.test_time_initial") + assert state_time is not None + assert state_time.state == "10:00:00" + assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60 - # Test that setting the timestamp of an entity works. - await hass.services.async_call( - DOMAIN, - "set_datetime", - { - ATTR_ENTITY_ID: "input_datetime.test_datetime_initial_with_tz", - ATTR_TIMESTAMP: state_without_tz.attributes[ATTR_TIMESTAMP], - }, - blocking=True, - ) - state_with_tz_updated = hass.states.get( - "input_datetime.test_datetime_initial_with_tz" - ) - assert state_with_tz_updated.state == "2020-12-13 10:00:00" - assert ( - state_with_tz_updated.attributes[ATTR_TIMESTAMP] - == state_without_tz.attributes[ATTR_TIMESTAMP] - ) - - finally: - dt_util.set_default_time_zone(ORIG_TIMEZONE) + # Test that setting the timestamp of an entity works. + await hass.services.async_call( + DOMAIN, + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.test_datetime_initial_with_tz", + ATTR_TIMESTAMP: state_without_tz.attributes[ATTR_TIMESTAMP], + }, + blocking=True, + ) + state_with_tz_updated = hass.states.get( + "input_datetime.test_datetime_initial_with_tz" + ) + assert state_with_tz_updated.state == "2020-12-13 10:00:00" + assert ( + state_with_tz_updated.attributes[ATTR_TIMESTAMP] + == state_without_tz.attributes[ATTR_TIMESTAMP] + ) @pytest.mark.parametrize( diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index b0279fd2748..fbcd6c5325f 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -13,13 +13,6 @@ HDATE_DEFAULT_ALTITUDE = 754 NYC_LATLNG = _LatLng(40.7128, -74.0060) JERUSALEM_LATLNG = _LatLng(31.778, 35.235) -ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - - -def teardown_module(): - """Reset time zone.""" - dt_util.set_default_time_zone(ORIG_TIME_ZONE) - def make_nyc_test_params(dtime, results, havdalah_offset=0): """Make test params for NYC.""" diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index b34dfdb28e4..15052243baa 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -181,7 +181,7 @@ async def test_issur_melacha_sensor( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude @@ -272,7 +272,7 @@ async def test_issur_melacha_sensor_update( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 879b5edb120..e2d24bcef04 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -165,7 +165,7 @@ async def test_jewish_calendar_sensor( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude @@ -510,7 +510,7 @@ async def test_shabbat_times_sensor( time_zone = dt_util.get_time_zone(tzname) test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = tzname + hass.config.set_time_zone(tzname) hass.config.latitude = latitude hass.config.longitude = longitude diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index ba0dc802007..4a9530768a6 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components.pvpc_hourly_pricing import ( ) from homeassistant.const import CONF_NAME from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from .conftest import check_valid_state @@ -32,7 +31,7 @@ async def test_config_flow( - Check removal and add again to check state restoration - Configure options to change power and tariff to "2.0TD" """ - hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") + hass.config.set_time_zone("Europe/Madrid") tst_config = { CONF_NAME: "test", ATTR_TARIFF: TARIFFS[1], diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 727a144e75d..bbea27477cf 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -11,7 +11,6 @@ from homeassistant.components.pvpc_hourly_pricing import ( TARIFFS, ) from homeassistant.const import CONF_NAME -from homeassistant.util import dt as dt_util from .conftest import check_valid_state @@ -29,7 +28,7 @@ async def test_multi_sensor_migration( ): """Test tariff migration when there are >1 old sensors.""" entity_reg = mock_registry(hass) - hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") + hass.config.set_time_zone("Europe/Madrid") uid_1 = "discrimination" uid_2 = "normal" old_conf_1 = {CONF_NAME: "test_pvpc_1", ATTR_TARIFF: uid_1} diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index c96465a671f..fe05dbc25ab 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -37,6 +37,8 @@ import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant, mock_registry from tests.components.recorder.common import wait_recording_done +ORIG_TZ = dt_util.DEFAULT_TIME_ZONE + def test_compile_hourly_statistics(hass_recorder): """Test compiling hourly statistics.""" @@ -841,6 +843,7 @@ def test_delete_duplicates(caplog, tmpdir): session.add(recorder.models.Statistics.from_stats(3, stat)) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() @@ -849,6 +852,7 @@ def test_delete_duplicates(caplog, tmpdir): wait_recording_done(hass) wait_recording_done(hass) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ assert "Deleted 2 duplicated statistics rows" in caplog.text assert "Found non identical" not in caplog.text @@ -1014,6 +1018,7 @@ def test_delete_duplicates_many(caplog, tmpdir): session.add(recorder.models.Statistics.from_stats(3, stat)) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() @@ -1022,6 +1027,7 @@ def test_delete_duplicates_many(caplog, tmpdir): wait_recording_done(hass) wait_recording_done(hass) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ assert "Deleted 3002 duplicated statistics rows" in caplog.text assert "Found non identical" not in caplog.text @@ -1149,6 +1155,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): session.add(recorder.models.Statistics.from_stats(2, stat)) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() @@ -1158,6 +1165,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): wait_recording_done(hass) wait_recording_done(hass) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ assert "Deleted 2 duplicated statistics rows" in caplog.text assert "Deleted 1 non identical" in caplog.text @@ -1249,6 +1257,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): ) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() @@ -1258,6 +1267,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): wait_recording_done(hass) wait_recording_done(hass) hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ assert "duplicated statistics rows" not in caplog.text assert "Found non identical" not in caplog.text diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 4286a7d09c9..24efbabf087 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -168,6 +168,7 @@ def _check_state(hass, category, entity_id): async def test_setup(hass, two_zone_alarm): # noqa: F811 """Test entity setup.""" + hass.config.set_time_zone("UTC") registry = er.async_get(hass) for id in ENTITY_IDS.values(): diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 155060222c8..af1494b381e 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2176,6 +2176,8 @@ def test_compile_statistics_hourly_daily_monthly_summary( "homeassistant.components.recorder.models.dt_util.utcnow", return_value=zero ): hass = hass_recorder() + # Remove this after dropping the use of the hass_recorder fixture + hass.config.set_time_zone("America/Regina") recorder = hass.data[DATA_INSTANCE] recorder._db_supports_row_number = db_supports_row_number setup_component(hass, "sensor", {}) diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index c499dc0112f..1b17fc2726e 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -96,10 +96,10 @@ async def test_sensors( assert state assert state.attributes.get(ATTR_ICON) == "mdi:television" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" - assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-27T01:30:00+00:00" + assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-26T17:30:00-08:00" assert ( state.attributes.get("The Andy Griffith Show S01E01") - == "1960-10-03T01:00:00+00:00" + == "1960-10-02T17:00:00-08:00" ) assert state.state == "2" diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 55c31da6c89..3b5f3af76ec 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -20,8 +20,6 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture def calls(hass): @@ -33,20 +31,11 @@ def calls(hass): def setup_comp(hass): """Initialize components.""" mock_component(hass, "group") - hass.config.set_time_zone(hass.config.time_zone) hass.loop.run_until_complete( async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) ) -@pytest.fixture(autouse=True) -def teardown(): - """Restore.""" - yield - - dt_util.set_default_time_zone(ORIG_TIME_ZONE) - - async def test_sunset_trigger(hass, calls, legacy_patchable_time): """Test the sunset trigger.""" now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 0352080bed8..b9b1a0cd93b 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1124,7 +1124,8 @@ async def test_trigger_entity_device_class_parsing_works(hass): await hass.async_block_till_done() - now = dt_util.now() + # State of timestamp sensors are always in UTC + now = dt_util.utcnow() with patch("homeassistant.util.dt.now", return_value=now): hass.bus.async_fire("test_event") @@ -1184,7 +1185,8 @@ async def test_trigger_entity_device_class_errors_works(hass): async def test_entity_device_class_parsing_works(hass): """Test entity device class parsing works.""" - now = dt_util.now() + # State of timestamp sensors are always in UTC + now = dt_util.utcnow() with patch("homeassistant.util.dt.now", return_value=now): assert await async_setup_component( diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index a9b5ea83c05..56f58221529 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,20 +1,9 @@ """The tests for time_date sensor platform.""" from unittest.mock import patch -import pytest - import homeassistant.components.time_date.sensor as time_date import homeassistant.util.dt as dt_util -ORIG_TZ = dt_util.DEFAULT_TIME_ZONE - - -@pytest.fixture(autouse=True) -def restore_ts(): - """Restore default TZ.""" - yield - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - # pylint: disable=protected-access async def test_intervals(hass): @@ -45,6 +34,8 @@ async def test_intervals(hass): async def test_states(hass): """Test states of sensors.""" + hass.config.set_time_zone("UTC") + now = dt_util.utc_from_timestamp(1495068856) device = time_date.TimeDateSensor(hass, "time") device._update_internal_state(now) @@ -79,9 +70,7 @@ async def test_states(hass): async def test_states_non_default_timezone(hass): """Test states of sensors in a timezone other than UTC.""" - new_tz = dt_util.get_time_zone("America/New_York") - assert new_tz is not None - dt_util.set_default_time_zone(new_tz) + hass.config.set_time_zone("America/New_York") now = dt_util.utc_from_timestamp(1495068856) device = time_date.TimeDateSensor(hass, "time") @@ -116,9 +105,7 @@ async def test_states_non_default_timezone(hass): # pylint: disable=no-member async def test_timezone_intervals(hass): """Test date sensor behavior in a timezone besides UTC.""" - new_tz = dt_util.get_time_zone("America/New_York") - assert new_tz is not None - dt_util.set_default_time_zone(new_tz) + hass.config.set_time_zone("America/New_York") device = time_date.TimeDateSensor(hass, "date") now = dt_util.utc_from_timestamp(50000) @@ -128,9 +115,7 @@ async def test_timezone_intervals(hass): # so the second day was 18000 + 86400 assert next_time.timestamp() == 104400 - new_tz = dt_util.get_time_zone("America/Edmonton") - assert new_tz is not None - dt_util.set_default_time_zone(new_tz) + hass.config.set_time_zone("America/Edmonton") now = dt_util.parse_datetime("2017-11-13 19:47:19-07:00") device = time_date.TimeDateSensor(hass, "date") with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -138,9 +123,7 @@ async def test_timezone_intervals(hass): assert next_time.timestamp() == dt_util.as_timestamp("2017-11-14 00:00:00-07:00") # Entering DST - new_tz = dt_util.get_time_zone("Europe/Prague") - assert new_tz is not None - dt_util.set_default_time_zone(new_tz) + hass.config.set_time_zone("Europe/Prague") now = dt_util.parse_datetime("2020-03-29 00:00+01:00") with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -168,11 +151,9 @@ async def test_timezone_intervals(hass): "homeassistant.util.dt.utcnow", return_value=dt_util.parse_datetime("2017-11-14 02:47:19-00:00"), ) -async def test_timezone_intervals_empty_parameter(hass): +async def test_timezone_intervals_empty_parameter(utcnow_mock, hass): """Test get_interval() without parameters.""" - new_tz = dt_util.get_time_zone("America/Edmonton") - assert new_tz is not None - dt_util.set_default_time_zone(new_tz) + hass.config.set_time_zone("America/Edmonton") device = time_date.TimeDateSensor(hass, "date") next_time = device.get_next_interval() assert next_time.timestamp() == dt_util.as_timestamp("2017-11-14 00:00:00-07:00") diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 06f29436d6e..ecdabdb910d 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -12,8 +12,6 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component -ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture(autouse=True) def mock_legacy_time(legacy_patchable_time): @@ -28,13 +26,6 @@ def setup_fixture(hass): hass.config.longitude = 18.98583 -@pytest.fixture(autouse=True) -def restore_timezone(hass): - """Make sure we change timezone.""" - yield - dt_util.set_default_time_zone(ORIG_TIMEZONE) - - async def test_setup(hass): """Test the setup.""" config = { @@ -69,7 +60,9 @@ async def test_setup_no_sensors(hass): async def test_in_period_on_start(hass): """Test simple setting.""" - test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) + test_time = datetime( + 2019, 1, 10, 18, 43, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) config = { "binary_sensor": [ { @@ -93,7 +86,9 @@ async def test_in_period_on_start(hass): async def test_midnight_turnover_before_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime(2019, 1, 10, 22, 30, 0, tzinfo=dt_util.UTC) + test_time = datetime( + 2019, 1, 10, 22, 30, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -112,7 +107,9 @@ async def test_midnight_turnover_before_midnight_inside_period(hass): async def test_midnight_turnover_after_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime(2019, 1, 10, 21, 0, 0, tzinfo=dt_util.UTC) + test_time = datetime( + 2019, 1, 10, 21, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -184,7 +181,9 @@ async def test_after_happens_tomorrow(hass): async def test_midnight_turnover_after_midnight_outside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime(2019, 1, 10, 20, 0, 0, tzinfo=dt_util.UTC) + test_time = datetime( + 2019, 1, 10, 20, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) config = { "binary_sensor": [ @@ -201,7 +200,9 @@ async def test_midnight_turnover_after_midnight_outside_period(hass): state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF - switchover_time = datetime(2019, 1, 11, 4, 59, 0, tzinfo=dt_util.UTC) + switchover_time = datetime( + 2019, 1, 11, 4, 59, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) with patch( "homeassistant.components.tod.binary_sensor.dt_util.utcnow", return_value=switchover_time, @@ -420,13 +421,13 @@ async def test_from_sunset_to_sunrise(hass): async def test_offset(hass): """Test offset.""" - after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=dt_util.UTC) + timedelta( - hours=1, minutes=34 - ) + after = datetime( + 2019, 1, 10, 18, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) + timedelta(hours=1, minutes=34) - before = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + timedelta( - hours=1, minutes=45 - ) + before = datetime( + 2019, 1, 10, 22, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) + timedelta(hours=1, minutes=45) entity_id = "binary_sensor.evening" config = { @@ -499,9 +500,9 @@ async def test_offset(hass): async def test_offset_overnight(hass): """Test offset overnight.""" - after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=dt_util.UTC) + timedelta( - hours=1, minutes=34 - ) + after = datetime( + 2019, 1, 10, 18, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) + ) + timedelta(hours=1, minutes=34) entity_id = "binary_sensor.evening" config = { "binary_sensor": [ @@ -890,8 +891,7 @@ async def test_sun_offset(hass): async def test_dst(hass): """Test sun event with offset.""" - hass.config.time_zone = "CET" - dt_util.set_default_time_zone(dt_util.get_time_zone("CET")) + hass.config.set_time_zone("CET") test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -919,8 +919,7 @@ async def test_dst(hass): async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): """Test simple before after.""" - hass.config.time_zone = "UTC" - dt_util.set_default_time_zone(dt_util.UTC) + hass.config.set_time_zone("UTC") test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -948,8 +947,7 @@ async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): async def test_simple_before_after_does_not_loop_utc_in_range(hass): """Test simple before after.""" - hass.config.time_zone = "UTC" - dt_util.set_default_time_zone(dt_util.UTC) + hass.config.set_time_zone("UTC") test_time = datetime(2019, 1, 10, 22, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -977,8 +975,7 @@ async def test_simple_before_after_does_not_loop_utc_in_range(hass): async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): """Test simple before after.""" - hass.config.time_zone = "UTC" - dt_util.set_default_time_zone(dt_util.UTC) + hass.config.set_time_zone("UTC") test_time = datetime(2019, 1, 11, 6, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -1006,8 +1003,7 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): """Test simple before after.""" - hass.config.time_zone = "UTC" - dt_util.set_default_time_zone(dt_util.UTC) + hass.config.set_time_zone("UTC") test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -1035,8 +1031,7 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): """Test simple before after.""" - hass.config.time_zone = "UTC" - dt_util.set_default_time_zone(dt_util.UTC) + hass.config.set_time_zone("UTC") test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -1064,8 +1059,7 @@ async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): """Test simple before after.""" - hass.config.time_zone = "Europe/Berlin" - dt_util.set_default_time_zone(dt_util.get_time_zone("Europe/Berlin")) + hass.config.set_time_zone("Europe/Berlin") test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -1093,8 +1087,7 @@ async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): async def test_simple_before_after_does_not_loop_berlin_in_range(hass): """Test simple before after.""" - hass.config.time_zone = "Europe/Berlin" - dt_util.set_default_time_zone(dt_util.get_time_zone("Europe/Berlin")) + hass.config.set_time_zone("Europe/Berlin") test_time = datetime(2019, 1, 10, 23, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ diff --git a/tests/components/vallox/test_sensor.py b/tests/components/vallox/test_sensor.py index bd8ecbea905..7649799f56b 100644 --- a/tests/components/vallox/test_sensor.py +++ b/tests/components/vallox/test_sensor.py @@ -12,44 +12,29 @@ from .conftest import patch_metrics from tests.common import MockConfigEntry -ORIG_TZ = dt.DEFAULT_TIME_ZONE - - -@pytest.fixture(autouse=True) -def reset_tz(): - """Restore the default TZ after test runs.""" - yield - dt.DEFAULT_TIME_ZONE = ORIG_TZ - @pytest.fixture def set_tz(request): """Set the default TZ to the one requested.""" - return request.getfixturevalue(request.param) + request.getfixturevalue(request.param) @pytest.fixture -def utc() -> tzinfo: +def utc(hass: HomeAssistant) -> None: """Set the default TZ to UTC.""" - tz = dt.get_time_zone("UTC") - dt.set_default_time_zone(tz) - return tz + hass.config.set_time_zone("UTC") @pytest.fixture -def helsinki() -> tzinfo: +def helsinki(hass: HomeAssistant) -> None: """Set the default TZ to Europe/Helsinki.""" - tz = dt.get_time_zone("Europe/Helsinki") - dt.set_default_time_zone(tz) - return tz + hass.config.set_time_zone("Europe/Helsinki") @pytest.fixture -def new_york() -> tzinfo: +def new_york(hass: HomeAssistant) -> None: """Set the default TZ to America/New_York.""" - tz = dt.get_time_zone("America/New_York") - dt.set_default_time_zone(tz) - return tz + hass.config.set_time_zone("America/New_York") def _sensor_to_datetime(sensor): diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index 6c784d3998f..90b19e73b04 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -35,6 +35,7 @@ DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) ) async def test_zodiac_day(hass, now, sign, element, modality): """Test the zodiac sensor.""" + hass.config.set_time_zone("UTC") config = {DOMAIN: {}} with patch("homeassistant.components.zodiac.sensor.utcnow", return_value=now): diff --git a/tests/conftest.py b/tests/conftest.py index 902fc55eac4..a7dcef31591 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,7 +29,7 @@ from homeassistant.components.websocket_api.http import URL from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED, HASSIO_USER_NAME from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component -from homeassistant.util import location +from homeassistant.util import dt as dt_util, location from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS @@ -249,6 +249,8 @@ def load_registries(): def hass(loop, load_registries, hass_storage, request): """Fixture to provide a test instance of Home Assistant.""" + orig_tz = dt_util.DEFAULT_TIME_ZONE + def exc_handle(loop, context): """Handle exceptions by rethrowing them, which will fail the test.""" # Most of these contexts will contain an exception, but not all. @@ -273,6 +275,10 @@ def hass(loop, load_registries, hass_storage, request): yield hass loop.run_until_complete(hass.async_stop(force=True)) + + # Restore timezone, it is set when creating the hass object + dt_util.DEFAULT_TIME_ZONE = orig_tz + for ex in exceptions: if ( request.module.__name__, diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index ffbc2130f3b..2b5c35f06ad 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -28,8 +28,6 @@ import homeassistant.util.dt as dt_util from tests.common import async_mock_service -ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - @pytest.fixture def calls(hass): @@ -46,14 +44,6 @@ def setup_comp(hass): ) -@pytest.fixture(autouse=True) -def teardown(): - """Restore.""" - yield - - dt_util.set_default_time_zone(ORIG_TIME_ZONE) - - def assert_element(trace_element, expected_element, path): """Assert a trace element is as expected. @@ -2659,8 +2649,7 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(hass, hass_ws_client, at 7 AM and sunset at 3AM during summer After sunrise is true from sunrise until midnight, local time. """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) + hass.config.set_time_zone("America/Anchorage") hass.config.latitude = 66.5 hass.config.longitude = 162.4 await async_setup_component( @@ -2736,8 +2725,7 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(hass, hass_ws_client, at 7 AM and sunset at 3AM during summer Before sunrise is true from midnight until sunrise, local time. """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) + hass.config.set_time_zone("America/Anchorage") hass.config.latitude = 66.5 hass.config.longitude = 162.4 await async_setup_component( @@ -2813,8 +2801,7 @@ async def test_if_action_before_sunset_no_offset_kotzebue(hass, hass_ws_client, at 7 AM and sunset at 3AM during summer Before sunset is true from midnight until sunset, local time. """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) + hass.config.set_time_zone("America/Anchorage") hass.config.latitude = 66.5 hass.config.longitude = 162.4 await async_setup_component( @@ -2890,8 +2877,7 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, hass_ws_client, c at 7 AM and sunset at 3AM during summer After sunset is true from sunset until midnight, local time. """ - tz = dt_util.get_time_zone("America/Anchorage") - dt_util.set_default_time_zone(tz) + hass.config.set_time_zone("America/Anchorage") hass.config.latitude = 66.5 hass.config.longitude = 162.4 await async_setup_component( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index bd17aec92e6..67fc679bb8b 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -46,14 +46,6 @@ from tests.common import async_fire_time_changed DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE -@pytest.fixture(autouse=True) -def teardown(): - """Stop everything that was started.""" - yield - - dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) - - async def test_track_point_in_time(hass): """Test track point in time.""" before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) @@ -3751,8 +3743,7 @@ async def test_periodic_task_duplicate_time(hass): @pytest.mark.freeze_time("2021-03-28 01:28:00+01:00") async def test_periodic_task_entering_dst(hass, freezer): """Test periodic task behavior when entering dst.""" - timezone = dt_util.get_time_zone("Europe/Vienna") - dt_util.set_default_time_zone(timezone) + hass.config.set_time_zone("Europe/Vienna") specific_runs = [] today = date.today().isoformat() @@ -3801,8 +3792,7 @@ async def test_periodic_task_entering_dst_2(hass, freezer): This tests a task firing every second in the range 0..58 (not *:*:59) """ - timezone = dt_util.get_time_zone("Europe/Vienna") - dt_util.set_default_time_zone(timezone) + hass.config.set_time_zone("Europe/Vienna") specific_runs = [] today = date.today().isoformat() @@ -3850,8 +3840,7 @@ async def test_periodic_task_entering_dst_2(hass, freezer): @pytest.mark.freeze_time("2021-10-31 02:28:00+02:00") async def test_periodic_task_leaving_dst(hass, freezer): """Test periodic task behavior when leaving dst.""" - timezone = dt_util.get_time_zone("Europe/Vienna") - dt_util.set_default_time_zone(timezone) + hass.config.set_time_zone("Europe/Vienna") specific_runs = [] today = date.today().isoformat() @@ -3925,8 +3914,7 @@ async def test_periodic_task_leaving_dst(hass, freezer): @pytest.mark.freeze_time("2021-10-31 02:28:00+02:00") async def test_periodic_task_leaving_dst_2(hass, freezer): """Test periodic task behavior when leaving dst.""" - timezone = dt_util.get_time_zone("Europe/Vienna") - dt_util.set_default_time_zone(timezone) + hass.config.set_time_zone("Europe/Vienna") specific_runs = [] today = date.today().isoformat() @@ -4188,8 +4176,8 @@ async def test_async_track_point_in_time_cancel(hass): """Test cancel of async track point in time.""" times = [] + hass.config.set_time_zone("US/Hawaii") hst_tz = dt_util.get_time_zone("US/Hawaii") - dt_util.set_default_time_zone(hst_tz) @ha.callback def run_callback(local_time): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d70837bd088..bbb5e11be7f 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -674,6 +674,7 @@ def test_strptime(hass): def test_timestamp_custom(hass): """Test the timestamps to custom filter.""" + hass.config.set_time_zone("UTC") now = dt_util.utcnow() tests = [ (None, None, None, None), @@ -700,6 +701,7 @@ def test_timestamp_custom(hass): def test_timestamp_local(hass): """Test the timestamps to local filter.""" + hass.config.set_time_zone("UTC") tests = {None: None, 1469119144: "2016-07-21T16:39:04+00:00"} for inp, out in tests.items(): @@ -1290,10 +1292,7 @@ def test_today_at(mock_is_safe, hass, now, expected, expected_midnight, timezone freezer = freeze_time(now) freezer.start() - original_tz = dt_util.DEFAULT_TIME_ZONE - - timezone = dt_util.get_time_zone(timezone_str) - dt_util.set_default_time_zone(timezone) + hass.config.set_time_zone(timezone_str) result = template.Template( "{{ today_at('10:00').isoformat() }}", @@ -1323,7 +1322,6 @@ def test_today_at(mock_is_safe, hass, now, expected, expected_midnight, timezone template.Template("{{ today_at('bad') }}", hass).async_render() freezer.stop() - dt_util.set_default_time_zone(original_tz) @patch( @@ -1332,6 +1330,7 @@ def test_today_at(mock_is_safe, hass, now, expected, expected_midnight, timezone ) def test_relative_time(mock_is_safe, hass): """Test relative_time method.""" + hass.config.set_time_zone("UTC") now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") with patch("homeassistant.util.dt.now", return_value=now): result = template.Template( diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 87d93c1a1ac..c6c507a0f73 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -12,7 +12,6 @@ import homeassistant.config as config_util from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.dt as dt_util from tests.common import ( MockModule, @@ -23,7 +22,6 @@ from tests.common import ( mock_integration, ) -ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) diff --git a/tests/test_config.py b/tests/test_config.py index 4e761bc3f47..552139fa0ef 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -32,7 +32,6 @@ from homeassistant.helpers import config_validation as cv import homeassistant.helpers.check_config as check_config from homeassistant.helpers.entity import Entity from homeassistant.loader import async_get_integration -from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML from tests.common import get_test_config_dir, patch_yaml_files @@ -44,7 +43,6 @@ VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE) AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) SCENES_PATH = os.path.join(CONFIG_DIR, config_util.SCENE_CONFIG_PATH) -ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE def create_file(path): @@ -58,8 +56,6 @@ def teardown(): """Clean up.""" yield - dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE - if os.path.isfile(YAML_PATH): os.remove(YAML_PATH) From e09d0b7106419bdd02bf44e563770456e63e3049 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 20 Mar 2022 10:32:10 +0100 Subject: [PATCH 0551/1054] Fix CI file changed filter (#68351) --- .core_files.yaml | 188 +++++++++++++++++++------------------- .github/workflows/ci.yaml | 2 +- 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/.core_files.yaml b/.core_files.yaml index 6f7b57c7869..85152a80aef 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -3,103 +3,103 @@ core: &core - homeassistant/*.py - homeassistant/auth/** - - homeassistant/helpers/* + - homeassistant/helpers/** - homeassistant/package_constraints.txt - - homeassistant/util/* + - homeassistant/util/** - pyproject.toml - requirements.txt - setup.cfg # Our base platforms, that are used by other integrations base_platforms: &base_platforms - - homeassistant/components/air_quality/* - - homeassistant/components/alarm_control_panel/* - - homeassistant/components/binary_sensor/* - - homeassistant/components/button/* - - homeassistant/components/calendar/* - - homeassistant/components/camera/* - - homeassistant/components/climate/* - - homeassistant/components/cover/* - - homeassistant/components/device_tracker/* - - homeassistant/components/diagnostics/* - - homeassistant/components/fan/* - - homeassistant/components/geo_location/* - - homeassistant/components/humidifier/* - - homeassistant/components/image_processing/* - - homeassistant/components/light/* - - homeassistant/components/lock/* - - homeassistant/components/media_player/* - - homeassistant/components/notify/* - - homeassistant/components/number/* - - homeassistant/components/remote/* - - homeassistant/components/scene/* - - homeassistant/components/select/* - - homeassistant/components/sensor/* - - homeassistant/components/siren/* - - homeassistant/components/stt/* - - homeassistant/components/switch/* - - homeassistant/components/tts/* - - homeassistant/components/vacuum/* - - homeassistant/components/water_heater/* - - homeassistant/components/weather/* + - homeassistant/components/air_quality/** + - homeassistant/components/alarm_control_panel/** + - homeassistant/components/binary_sensor/** + - homeassistant/components/button/** + - homeassistant/components/calendar/** + - homeassistant/components/camera/** + - homeassistant/components/climate/** + - homeassistant/components/cover/** + - homeassistant/components/device_tracker/** + - homeassistant/components/diagnostics/** + - homeassistant/components/fan/** + - homeassistant/components/geo_location/** + - homeassistant/components/humidifier/** + - homeassistant/components/image_processing/** + - homeassistant/components/light/** + - homeassistant/components/lock/** + - homeassistant/components/media_player/** + - homeassistant/components/notify/** + - homeassistant/components/number/** + - homeassistant/components/remote/** + - homeassistant/components/scene/** + - homeassistant/components/select/** + - homeassistant/components/sensor/** + - homeassistant/components/siren/** + - homeassistant/components/stt/** + - homeassistant/components/switch/** + - homeassistant/components/tts/** + - homeassistant/components/vacuum/** + - homeassistant/components/water_heater/** + - homeassistant/components/weather/** # Extra components that trigger the full suite components: &components - - homeassistant/components/alert/* - - homeassistant/components/alexa/* - - homeassistant/components/auth/* - - homeassistant/components/automation/* - - homeassistant/components/backup/* - - homeassistant/components/cloud/* - - homeassistant/components/config/* - - homeassistant/components/configurator/* - - homeassistant/components/conversation/* - - homeassistant/components/demo/* - - homeassistant/components/device_automation/* - - homeassistant/components/dhcp/* - - homeassistant/components/discovery/* - - homeassistant/components/energy/* - - homeassistant/components/ffmpeg/* - - homeassistant/components/frontend/* - - homeassistant/components/google_assistant/* - - homeassistant/components/group/* - - homeassistant/components/hassio/* + - homeassistant/components/alert/** + - homeassistant/components/alexa/** + - homeassistant/components/auth/** + - homeassistant/components/automation/** + - homeassistant/components/backup/** + - homeassistant/components/cloud/** + - homeassistant/components/config/** + - homeassistant/components/configurator/** + - homeassistant/components/conversation/** + - homeassistant/components/demo/** + - homeassistant/components/device_automation/** + - homeassistant/components/dhcp/** + - homeassistant/components/discovery/** + - homeassistant/components/energy/** + - homeassistant/components/ffmpeg/** + - homeassistant/components/frontend/** + - homeassistant/components/google_assistant/** + - homeassistant/components/group/** + - homeassistant/components/hassio/** - homeassistant/components/homeassistant/** - homeassistant/components/http/** - - homeassistant/components/image/* - - homeassistant/components/input_boolean/* - - homeassistant/components/input_button/* - - homeassistant/components/input_datetime/* - - homeassistant/components/input_number/* - - homeassistant/components/input_select/* - - homeassistant/components/input_text/* - - homeassistant/components/logbook/* - - homeassistant/components/logger/* - - homeassistant/components/lovelace/* - - homeassistant/components/media_source/* - - homeassistant/components/mjpeg/* - - homeassistant/components/mqtt/* - - homeassistant/components/network/* - - homeassistant/components/onboarding/* - - homeassistant/components/otp/* - - homeassistant/components/persistent_notification/* - - homeassistant/components/person/* - - homeassistant/components/recorder/* - - homeassistant/components/safe_mode/* - - homeassistant/components/script/* - - homeassistant/components/shopping_list/* - - homeassistant/components/ssdp/* - - homeassistant/components/stream/* - - homeassistant/components/sun/* - - homeassistant/components/system_health/* - - homeassistant/components/tag/* - - homeassistant/components/template/* - - homeassistant/components/timer/* - - homeassistant/components/usb/* - - homeassistant/components/webhook/* - - homeassistant/components/websocket_api/* - - homeassistant/components/zeroconf/* - - homeassistant/components/zone/* + - homeassistant/components/image/** + - homeassistant/components/input_boolean/** + - homeassistant/components/input_button/** + - homeassistant/components/input_datetime/** + - homeassistant/components/input_number/** + - homeassistant/components/input_select/** + - homeassistant/components/input_text/** + - homeassistant/components/logbook/** + - homeassistant/components/logger/** + - homeassistant/components/lovelace/** + - homeassistant/components/media_source/** + - homeassistant/components/mjpeg/** + - homeassistant/components/mqtt/** + - homeassistant/components/network/** + - homeassistant/components/onboarding/** + - homeassistant/components/otp/** + - homeassistant/components/persistent_notification/** + - homeassistant/components/person/** + - homeassistant/components/recorder/** + - homeassistant/components/safe_mode/** + - homeassistant/components/script/** + - homeassistant/components/shopping_list/** + - homeassistant/components/ssdp/** + - homeassistant/components/stream/** + - homeassistant/components/sun/** + - homeassistant/components/system_health/** + - homeassistant/components/tag/** + - homeassistant/components/template/** + - homeassistant/components/timer/** + - homeassistant/components/usb/** + - homeassistant/components/webhook/** + - homeassistant/components/websocket_api/** + - homeassistant/components/zeroconf/** + - homeassistant/components/zone/** # Testing related files that affect the whole test/linting suite tests: &tests @@ -108,25 +108,25 @@ tests: &tests - requirements_test_pre_commit.txt - requirements_test.txt - tests/auth/** - - tests/backports/* + - tests/backports/** - tests/common.py - tests/conftest.py - - tests/hassfest/* - - tests/helpers/* + - tests/hassfest/** + - tests/helpers/** - tests/ignore_uncaught_exceptions.py - - tests/mock/* - - tests/pylint/* - - tests/scripts/* - - tests/test_util/* + - tests/mock/** + - tests/pylint/** + - tests/scripts/** + - tests/test_util/** - tests/testing_config/** - tests/util/** other: &other - - .github/workflows/* + - .github/workflows/** - homeassistant/scripts/** requirements: &requirements - - .github/workflows/* + - .github/workflows/** - homeassistant/package_constraints.txt - requirements*.txt - setup.cfg diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 259277b1666..35d0bd06287 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,7 +62,7 @@ jobs: integrations=$(ls -Ad ./homeassistant/components/[!_]* | xargs -n 1 basename) touch .integration_paths.yaml for integration in $integrations; do - echo "${integration}: [homeassistant/components/${integration}/*, tests/components/${integration}/*]" \ + echo "${integration}: [homeassistant/components/${integration}/**, tests/components/${integration}/**]" \ >> .integration_paths.yaml; done echo "Result:" From a0a96dab05b3ade3d76d21d26f99dda541d6dbc9 Mon Sep 17 00:00:00 2001 From: Garrett <7310260+G-Two@users.noreply.github.com> Date: Sun, 20 Mar 2022 05:41:53 -0400 Subject: [PATCH 0552/1054] Add door locks to Subaru integration (#52852) Co-authored-by: J. Nick Koston --- homeassistant/components/subaru/__init__.py | 11 +++ homeassistant/components/subaru/const.py | 18 ++++ homeassistant/components/subaru/lock.py | 91 +++++++++++++++++++ .../components/subaru/remote_service.py | 33 +++++++ homeassistant/components/subaru/services.yaml | 19 ++++ tests/components/subaru/test_lock.py | 86 ++++++++++++++++++ 6 files changed, 258 insertions(+) create mode 100644 homeassistant/components/subaru/lock.py create mode 100644 homeassistant/components/subaru/remote_service.py create mode 100644 homeassistant/components/subaru/services.yaml create mode 100644 tests/components/subaru/test_lock.py diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index a252e61b690..6e05586706f 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_US from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -21,6 +22,7 @@ from .const import ( ENTRY_COORDINATOR, ENTRY_VEHICLES, FETCH_INTERVAL, + MANUFACTURER, PLATFORMS, UPDATE_INTERVAL, VEHICLE_API_GEN, @@ -154,3 +156,12 @@ def get_vehicle_info(controller, vin): VEHICLE_LAST_UPDATE: 0, } return info + + +def get_device_info(vehicle_info): + """Return DeviceInfo object based on vehicle info.""" + return DeviceInfo( + identifiers={(DOMAIN, vehicle_info[VEHICLE_VIN])}, + manufacturer=MANUFACTURER, + name=vehicle_info[VEHICLE_NAME], + ) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index 596923cbc06..3ad7dd58af5 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -1,4 +1,6 @@ """Constants for the Subaru integration.""" +from subarulink.const import ALL_DOORS, DRIVERS_DOOR, TAILGATE_DOOR + from homeassistant.const import Platform DOMAIN = "subaru" @@ -32,9 +34,25 @@ API_GEN_2 = "g2" MANUFACTURER = "Subaru Corp." PLATFORMS = [ + Platform.LOCK, Platform.SENSOR, ] +SERVICE_LOCK = "lock" +SERVICE_UNLOCK = "unlock" +SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door" + +ATTR_DOOR = "door" + +UNLOCK_DOOR_ALL = "all" +UNLOCK_DOOR_DRIVERS = "driver" +UNLOCK_DOOR_TAILGATE = "tailgate" +UNLOCK_VALID_DOORS = { + UNLOCK_DOOR_ALL: ALL_DOORS, + UNLOCK_DOOR_DRIVERS: DRIVERS_DOOR, + UNLOCK_DOOR_TAILGATE: TAILGATE_DOOR, +} + ICONS = { "Avg Fuel Consumption": "mdi:leaf", "EV Range": "mdi:ev-station", diff --git a/homeassistant/components/subaru/lock.py b/homeassistant/components/subaru/lock.py new file mode 100644 index 00000000000..fb460c6279a --- /dev/null +++ b/homeassistant/components/subaru/lock.py @@ -0,0 +1,91 @@ +"""Support for Subaru door locks.""" +import logging + +import voluptuous as vol + +from homeassistant.components.lock import LockEntity +from homeassistant.const import SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.helpers import entity_platform + +from . import DOMAIN, get_device_info +from .const import ( + ATTR_DOOR, + ENTRY_CONTROLLER, + ENTRY_VEHICLES, + SERVICE_UNLOCK_SPECIFIC_DOOR, + UNLOCK_DOOR_ALL, + UNLOCK_VALID_DOORS, + VEHICLE_HAS_REMOTE_SERVICE, + VEHICLE_NAME, + VEHICLE_VIN, +) +from .remote_service import async_call_remote_service + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Subaru locks by config_entry.""" + entry = hass.data[DOMAIN][config_entry.entry_id] + controller = entry[ENTRY_CONTROLLER] + vehicle_info = entry[ENTRY_VEHICLES] + async_add_entities( + SubaruLock(vehicle, controller) + for vehicle in vehicle_info.values() + if vehicle[VEHICLE_HAS_REMOTE_SERVICE] + ) + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_UNLOCK_SPECIFIC_DOOR, + {vol.Required(ATTR_DOOR): vol.In(UNLOCK_VALID_DOORS)}, + "async_unlock_specific_door", + ) + + +class SubaruLock(LockEntity): + """ + Representation of a Subaru door lock. + + Note that the Subaru API currently does not support returning the status of the locks. Lock status is always unknown. + """ + + def __init__(self, vehicle_info, controller): + """Initialize the locks for the vehicle.""" + self.controller = controller + self.vehicle_info = vehicle_info + vin = vehicle_info[VEHICLE_VIN] + self.car_name = vehicle_info[VEHICLE_NAME] + self._attr_name = f"{self.car_name} Door Locks" + self._attr_unique_id = f"{vin}_door_locks" + self._attr_device_info = get_device_info(vehicle_info) + + async def async_lock(self, **kwargs): + """Send the lock command.""" + _LOGGER.debug("Locking doors for: %s", self.car_name) + await async_call_remote_service( + self.controller, + SERVICE_LOCK, + self.vehicle_info, + ) + + async def async_unlock(self, **kwargs): + """Send the unlock command.""" + _LOGGER.debug("Unlocking doors for: %s", self.car_name) + await async_call_remote_service( + self.controller, + SERVICE_UNLOCK, + self.vehicle_info, + UNLOCK_VALID_DOORS[UNLOCK_DOOR_ALL], + ) + + async def async_unlock_specific_door(self, door): + """Send the unlock command for a specified door.""" + _LOGGER.debug("Unlocking %s door for: %s", door, self.car_name) + await async_call_remote_service( + self.controller, + SERVICE_UNLOCK, + self.vehicle_info, + UNLOCK_VALID_DOORS[door], + ) diff --git a/homeassistant/components/subaru/remote_service.py b/homeassistant/components/subaru/remote_service.py new file mode 100644 index 00000000000..04c87b6b8d2 --- /dev/null +++ b/homeassistant/components/subaru/remote_service.py @@ -0,0 +1,33 @@ +"""Remote vehicle services for Subaru integration.""" +import logging + +from subarulink.exceptions import SubaruException + +from homeassistant.exceptions import HomeAssistantError + +from .const import SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_call_remote_service(controller, cmd, vehicle_info, arg=None): + """Execute subarulink remote command.""" + car_name = vehicle_info[VEHICLE_NAME] + vin = vehicle_info[VEHICLE_VIN] + + _LOGGER.debug("Sending %s command command to %s", cmd, car_name) + success = False + err_msg = "" + try: + if cmd == SERVICE_UNLOCK: + success = await getattr(controller, cmd)(vin, arg) + else: + success = await getattr(controller, cmd)(vin) + except SubaruException as err: + err_msg = err.message + + if success: + _LOGGER.debug("%s command successfully completed for %s", cmd, car_name) + return + + raise HomeAssistantError(f"Service {cmd} failed for {car_name}: {err_msg}") diff --git a/homeassistant/components/subaru/services.yaml b/homeassistant/components/subaru/services.yaml new file mode 100644 index 00000000000..58be48f9d18 --- /dev/null +++ b/homeassistant/components/subaru/services.yaml @@ -0,0 +1,19 @@ +unlock_specific_door: + name: Unlock Specific Door + description: Unlocks specific door(s) + target: + entity: + domain: lock + integration: subaru + fields: + door: + name: Door + description: "One of the following: 'all', 'driver', 'tailgate'" + example: driver + required: true + selector: + select: + options: + - "all" + - "driver" + - "tailgate" diff --git a/tests/components/subaru/test_lock.py b/tests/components/subaru/test_lock.py new file mode 100644 index 00000000000..19918ba205c --- /dev/null +++ b/tests/components/subaru/test_lock.py @@ -0,0 +1,86 @@ +"""Test Subaru locks.""" +from unittest.mock import patch + +from pytest import raises +from voluptuous.error import MultipleInvalid + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.subaru.const import ( + ATTR_DOOR, + DOMAIN as SUBARU_DOMAIN, + SERVICE_UNLOCK_SPECIFIC_DOOR, + UNLOCK_DOOR_DRIVERS, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.exceptions import HomeAssistantError + +from .conftest import MOCK_API + +MOCK_API_LOCK = f"{MOCK_API}lock" +MOCK_API_UNLOCK = f"{MOCK_API}unlock" +DEVICE_ID = "lock.test_vehicle_2_door_locks" + + +async def test_device_exists(hass, ev_entry): + """Test subaru lock entity exists.""" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(DEVICE_ID) + assert entry + + +async def test_lock_cmd(hass, ev_entry): + """Test subaru lock function.""" + with patch(MOCK_API_LOCK) as mock_lock: + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_lock.assert_called_once() + + +async def test_unlock_cmd(hass, ev_entry): + """Test subaru unlock function.""" + with patch(MOCK_API_UNLOCK) as mock_unlock: + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_unlock.assert_called_once() + + +async def test_lock_cmd_fails(hass, ev_entry): + """Test subaru lock request that initiates but fails.""" + with patch(MOCK_API_LOCK, return_value=False) as mock_lock, raises( + HomeAssistantError + ): + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_lock.assert_called_once() + + +async def test_unlock_specific_door(hass, ev_entry): + """Test subaru unlock specific door function.""" + with patch(MOCK_API_UNLOCK) as mock_unlock: + await hass.services.async_call( + SUBARU_DOMAIN, + SERVICE_UNLOCK_SPECIFIC_DOOR, + {ATTR_ENTITY_ID: DEVICE_ID, ATTR_DOOR: UNLOCK_DOOR_DRIVERS}, + blocking=True, + ) + await hass.async_block_till_done() + mock_unlock.assert_called_once() + + +async def test_unlock_specific_door_invalid(hass, ev_entry): + """Test subaru unlock specific door function.""" + with patch(MOCK_API_UNLOCK) as mock_unlock, raises(MultipleInvalid): + await hass.services.async_call( + SUBARU_DOMAIN, + SERVICE_UNLOCK_SPECIFIC_DOOR, + {ATTR_ENTITY_ID: DEVICE_ID, ATTR_DOOR: "bad_value"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_unlock.assert_not_called() From 816695cc96c19110ccda10431d92160ea6064d32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 19 Mar 2022 23:47:22 -1000 Subject: [PATCH 0553/1054] Avoid selecting attributes in the history api when `no_attributes` is passed (#68352) --- homeassistant/components/history/__init__.py | 4 + homeassistant/components/recorder/history.py | 136 +++++++++++++------ tests/components/history/test_init.py | 36 ++++- tests/components/recorder/test_history.py | 76 +++++++++-- 4 files changed, 201 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 5d870ffa8ee..5d4b2dc4675 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -224,6 +224,7 @@ class HistoryPeriodView(HomeAssistantView): ) minimal_response = "minimal_response" in request.query + no_attributes = "no_attributes" in request.query hass = request.app["hass"] @@ -245,6 +246,7 @@ class HistoryPeriodView(HomeAssistantView): include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ), ) @@ -257,6 +259,7 @@ class HistoryPeriodView(HomeAssistantView): include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ): """Fetch significant stats from the database as json.""" timer_start = time.perf_counter() @@ -272,6 +275,7 @@ class HistoryPeriodView(HomeAssistantView): include_start_time_state, significant_changes_only, minimal_response, + no_attributes, ) result = list(result.values()) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index c4d15863a5b..da128d69dc9 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -2,15 +2,17 @@ from __future__ import annotations from collections import defaultdict +from datetime import datetime from itertools import groupby import logging import time -from sqlalchemy import and_, bindparam, func +from sqlalchemy import Text, and_, bindparam, func from sqlalchemy.ext import baked +from sqlalchemy.sql.expression import literal from homeassistant.components import recorder -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util from .models import ( @@ -44,13 +46,21 @@ NEED_ATTRIBUTE_DOMAINS = { "water_heater", } -QUERY_STATES = [ +BASE_STATES = [ States.domain, States.entity_id, States.state, - States.attributes, States.last_changed, States.last_updated, +] +QUERY_STATE_NO_ATTR = [ + *BASE_STATES, + literal(value=None, type_=Text).label("attributes"), + literal(value=None, type_=Text).label("shared_attrs"), +] +QUERY_STATES = [ + *BASE_STATES, + States.attributes, StateAttributes.shared_attrs, ] @@ -78,6 +88,7 @@ def get_significant_states_with_session( include_start_time_state=True, significant_changes_only=True, minimal_response=False, + no_attributes=False, ): """ Return states changes during UTC period start_time - end_time. @@ -92,10 +103,8 @@ def get_significant_states_with_session( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES + baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) if significant_changes_only: baked_query += lambda q: q.filter( @@ -120,9 +129,10 @@ def get_significant_states_with_session( if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) + if not no_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) states = execute( @@ -144,14 +154,25 @@ def get_significant_states_with_session( filters, include_start_time_state, minimal_response, + no_attributes, ) -def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): +def state_changes_during_period( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + entity_id: str | None = None, + no_attributes: bool = False, + descending: bool = False, + limit: int | None = None, + include_start_time_state: bool = True, +) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) + lambda session: session.query(*query_keys) ) baked_query += lambda q: q.filter( @@ -168,10 +189,16 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + if not no_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) + + last_updated = States.last_updated.desc() if descending else States.last_updated + baked_query += lambda q: q.order_by(States.entity_id, last_updated) + + if limit: + baked_query += lambda q: q.limit(limit) states = execute( baked_query(session).params( @@ -181,7 +208,14 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) entity_ids = [entity_id] if entity_id is not None else None - return _sorted_states_to_dict(hass, session, states, start_time, entity_ids) + return _sorted_states_to_dict( + hass, + session, + states, + start_time, + entity_ids, + include_start_time_state=include_start_time_state, + ) def get_last_state_changes(hass, number_of_states, entity_id): @@ -225,7 +259,14 @@ def get_last_state_changes(hass, number_of_states, entity_id): ) -def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): +def get_states( + hass, + utc_point_in_time, + entity_ids=None, + run=None, + filters=None, + no_attributes=False, +): """Return the states at a specific point in time.""" if run is None: run = recorder.run_information_from_instance(hass, utc_point_in_time) @@ -236,17 +277,23 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) with session_scope(hass=hass) as session: return _get_states_with_session( - hass, session, utc_point_in_time, entity_ids, run, filters + hass, session, utc_point_in_time, entity_ids, run, filters, no_attributes ) def _get_states_with_session( - hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None + hass, + session, + utc_point_in_time, + entity_ids=None, + run=None, + filters=None, + no_attributes=False, ): """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: return _get_single_entity_states_with_session( - hass, session, utc_point_in_time, entity_ids[0] + hass, session, utc_point_in_time, entity_ids[0], no_attributes ) if run is None: @@ -258,7 +305,8 @@ def _get_states_with_session( # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. - query = session.query(*QUERY_STATES) + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES + query = session.query(*query_keys) if entity_ids: # We got an include-list of entities, accelerate the query by filtering already @@ -278,9 +326,11 @@ def _get_states_with_session( query = query.join( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, - ).outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) + if not no_attributes: + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) else: # We did not get an include-list of entities, query all states in the inner # query, then filter out unwanted domains as well as applying the custom filter. @@ -318,27 +368,30 @@ def _get_states_with_session( query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) if filters: query = filters.apply(query) - query = query.outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) + if not no_attributes: + query = query.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) attr_cache = {} return [LazyState(row, attr_cache) for row in execute(query)] -def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): +def _get_single_entity_states_with_session( + hass, session, utc_point_in_time, entity_id, no_attributes=False +): # Use an entirely different (and extremely fast) query if we only # have a single entity id - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) + query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES + baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) baked_query += lambda q: q.filter( States.last_updated < bindparam("utc_point_in_time"), States.entity_id == bindparam("entity_id"), ) - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) + if not no_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by(States.last_updated.desc()) baked_query += lambda q: q.limit(1) @@ -358,6 +411,7 @@ def _sorted_states_to_dict( filters=None, include_start_time_state=True, minimal_response=False, + no_attributes=False, ): """Convert SQL results into JSON friendly data structure. @@ -381,7 +435,13 @@ def _sorted_states_to_dict( if include_start_time_state: run = recorder.run_information_from_instance(hass, start_time) for state in _get_states_with_session( - hass, session, start_time, entity_ids, run=run, filters=filters + hass, + session, + start_time, + entity_ids, + run=run, + filters=filters, + no_attributes=no_attributes, ): state.last_changed = start_time state.last_updated = start_time @@ -440,7 +500,7 @@ def _sorted_states_to_dict( return {key: val for key, val in result.items() if val} -def get_state(hass, utc_point_in_time, entity_id, run=None): +def get_state(hass, utc_point_in_time, entity_id, run=None, no_attributes=False): """Return a state at a specific point in time.""" - states = get_states(hass, utc_point_in_time, (entity_id,), run) + states = get_states(hass, utc_point_in_time, (entity_id,), run, None, no_attributes) return states[0] if states else None diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 18fa3dc7625..1590dcf1ed0 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -17,8 +17,12 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import init_recorder_component -from tests.components.recorder.common import trigger_db_commit, wait_recording_done +from tests.common import async_init_recorder_component, init_recorder_component +from tests.components.recorder.common import ( + async_wait_recording_done_without_instance, + trigger_db_commit, + wait_recording_done, +) @pytest.mark.usefixtures("hass_history") @@ -604,14 +608,36 @@ async def test_fetch_period_api_with_use_include_order(hass, hass_client): async def test_fetch_period_api_with_minimal_response(hass, hass_client): """Test the fetch period view for history with minimal_response.""" - await hass.async_add_executor_job(init_recorder_component, hass) + await async_init_recorder_component(hass) + now = dt_util.utcnow() await async_setup_component(hass, "history", {}) - await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + hass.states.async_set("sensor.power", 0, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) + hass.states.async_set("sensor.power", 50, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) + hass.states.async_set("sensor.power", 23, {"attr": "any"}) + await async_wait_recording_done_without_instance(hass) client = await hass_client() response = await client.get( - f"/api/history/period/{dt_util.utcnow().isoformat()}?minimal_response" + f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response&no_attributes" ) assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json[0]) == 3 + state_list = response_json[0] + + assert state_list[0]["entity_id"] == "sensor.power" + assert state_list[0]["attributes"] == {} + assert state_list[0]["state"] == "0" + + assert "attributes" not in state_list[1] + assert "entity_id" not in state_list[1] + assert state_list[1]["state"] == "50" + + assert state_list[2]["entity_id"] == "sensor.power" + assert state_list[2]["attributes"] == {} + assert state_list[2]["state"] == "23" async def test_fetch_period_api_with_no_timestamp(hass, hass_client): diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 67a666c934f..5d1d72ca650 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -5,6 +5,8 @@ from datetime import timedelta import json from unittest.mock import patch, sentinel +import pytest + from homeassistant.components.recorder import history from homeassistant.components.recorder.models import process_timestamp import homeassistant.core as ha @@ -15,11 +17,9 @@ from tests.common import mock_state_change_event from tests.components.recorder.common import wait_recording_done -def test_get_states(hass_recorder): - """Test getting states at a specific point in time.""" - hass = hass_recorder() +def _setup_get_states(hass): + """Set up for testing get_states.""" states = [] - now = dt_util.utcnow() with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): for i in range(5): @@ -48,6 +48,13 @@ def test_get_states(hass_recorder): wait_recording_done(hass) + return now, future, states + + +def test_get_states(hass_recorder): + """Test getting states at a specific point in time.""" + hass = hass_recorder() + now, future, states = _setup_get_states(hass) # Get states returns everything before POINT for all entities for state1, state2 in zip( states, @@ -75,14 +82,65 @@ def test_get_states(hass_recorder): assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None -def test_state_changes_during_period(hass_recorder): +def test_get_states_no_attributes(hass_recorder): + """Test getting states without attributes at a specific point in time.""" + hass = hass_recorder() + now, future, states = _setup_get_states(hass) + for state in states: + state.attributes = {} + + # Get states returns everything before POINT for all entities + for state1, state2 in zip( + states, + sorted( + history.get_states(hass, future, no_attributes=True), + key=lambda state: state.entity_id, + ), + ): + assert state1 == state2 + + # Get states returns everything before POINT for tested entities + entities = [f"test.point_in_time_{i % 5}" for i in range(5)] + for state1, state2 in zip( + states, + sorted( + history.get_states(hass, future, entities, no_attributes=True), + key=lambda state: state.entity_id, + ), + ): + assert state1 == state2 + + # Test get_state here because we have a DB setup + assert states[0] == history.get_state( + hass, future, states[0].entity_id, no_attributes=True + ) + + time_before_recorder_ran = now - timedelta(days=1000) + assert history.get_states(hass, time_before_recorder_ran, no_attributes=True) == [] + + assert ( + history.get_state(hass, time_before_recorder_ran, "demo.id", no_attributes=True) + is None + ) + + +@pytest.mark.parametrize( + "attributes, no_attributes, limit", + [ + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), + ], +) +def test_state_changes_during_period(hass_recorder, attributes, no_attributes, limit): """Test state change during period.""" hass = hass_recorder() entity_id = "media_player.test" def set_state(state): """Set the state.""" - hass.states.set(entity_id, state) + hass.states.set(entity_id, state, attributes) wait_recording_done(hass) return hass.states.get(entity_id) @@ -106,9 +164,11 @@ def test_state_changes_during_period(hass_recorder): set_state("Netflix") set_state("Plex") - hist = history.state_changes_during_period(hass, start, end, entity_id) + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, limit=limit + ) - assert states == hist[entity_id] + assert states[:limit] == hist[entity_id] def test_get_last_state_changes(hass_recorder): From 3150915cb72df2ca94a67fa29ebc9164d8deabf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Mar 2022 01:28:17 -1000 Subject: [PATCH 0554/1054] Convert unindexed domain queries to entity_id queries (#68404) --- homeassistant/components/history/__init__.py | 18 +++- homeassistant/components/logbook/__init__.py | 105 ++++++++++++++----- homeassistant/components/recorder/history.py | 48 ++++++--- homeassistant/components/recorder/models.py | 8 +- tests/components/recorder/test_models.py | 1 - tests/components/recorder/test_purge.py | 12 --- 6 files changed, 127 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 5d4b2dc4675..10b50f40fd3 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -363,7 +363,14 @@ class Filters: """Generate the entity filter query.""" includes = [] if self.included_domains: - includes.append(history_models.States.domain.in_(self.included_domains)) + includes.append( + or_( + *[ + history_models.States.entity_id.like(f"{domain}.%") + for domain in self.included_domains + ] + ).self_group() + ) if self.included_entities: includes.append(history_models.States.entity_id.in_(self.included_entities)) for glob in self.included_entity_globs: @@ -371,7 +378,14 @@ class Filters: excludes = [] if self.excluded_domains: - excludes.append(history_models.States.domain.in_(self.excluded_domains)) + excludes.append( + or_( + *[ + history_models.States.entity_id.like(f"{domain}.%") + for domain in self.excluded_domains + ] + ).self_group() + ) if self.excluded_entities: excludes.append(history_models.States.entity_id.in_(self.excluded_entities)) for glob in self.excluded_entity_globs: diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 8860daaec3c..8b00311a9e7 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,6 +1,9 @@ """Event parser and human readable log generator.""" +from __future__ import annotations + +from collections.abc import Iterable from contextlib import suppress -from datetime import timedelta +from datetime import datetime as dt, timedelta from http import HTTPStatus from itertools import groupby import json @@ -9,6 +12,8 @@ from typing import Any import sqlalchemy from sqlalchemy.orm import aliased +from sqlalchemy.orm.query import Query +from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal import voluptuous as vol @@ -59,13 +64,14 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -ENTITY_ID_JSON_TEMPLATE = '"entity_id":"{}"' +ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') ATTR_MESSAGE = "message" -CONTINUOUS_DOMAINS = ["proximity", "sensor"] +CONTINUOUS_DOMAINS = {"proximity", "sensor"} +CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] DOMAIN = "logbook" @@ -73,7 +79,7 @@ GROUP_BY_MINUTES = 15 EMPTY_JSON_OBJECT = "{}" UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' - +UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}._" CONFIG_SCHEMA = vol.Schema( @@ -489,35 +495,39 @@ def _get_events( ) -def _generate_events_query(session): +def _generate_events_query(session: Session) -> Query: return session.query( *EVENT_COLUMNS, States.state, States.entity_id, - States.domain, States.attributes, StateAttributes.shared_attrs, ) -def _generate_events_query_without_states(session): +def _generate_events_query_without_states(session: Session) -> Query: return session.query( *EVENT_COLUMNS, literal(value=None, type_=sqlalchemy.String).label("state"), literal(value=None, type_=sqlalchemy.String).label("entity_id"), - literal(value=None, type_=sqlalchemy.String).label("domain"), literal(value=None, type_=sqlalchemy.Text).label("attributes"), literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), ) -def _generate_states_query(session, start_day, end_day, old_state, entity_ids): +def _generate_states_query( + session: Session, + start_day: dt, + end_day: dt, + old_state: States, + entity_ids: Iterable[str], +) -> Query: return ( _generate_events_query(session) .outerjoin(Events, (States.event_id == Events.event_id)) .outerjoin(old_state, (States.old_state_id == old_state.state_id)) .filter(_missing_state_matcher(old_state)) - .filter(_continuous_entity_matcher()) + .filter(_not_continuous_entity_matcher()) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) .filter( (States.last_updated == States.last_changed) @@ -529,7 +539,9 @@ def _generate_states_query(session, start_day, end_day, old_state, entity_ids): ) -def _apply_events_types_and_states_filter(hass, query, old_state): +def _apply_events_types_and_states_filter( + hass: HomeAssistant, query: Query, old_state: States +) -> Query: events_query = ( query.outerjoin(States, (Events.event_id == States.event_id)) .outerjoin(old_state, (States.old_state_id == old_state.state_id)) @@ -538,7 +550,8 @@ def _apply_events_types_and_states_filter(hass, query, old_state): | _missing_state_matcher(old_state) ) .filter( - (Events.event_type != EVENT_STATE_CHANGED) | _continuous_entity_matcher() + (Events.event_type != EVENT_STATE_CHANGED) + | _not_continuous_entity_matcher() ) ) return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES).outerjoin( @@ -546,7 +559,7 @@ def _apply_events_types_and_states_filter(hass, query, old_state): ) -def _missing_state_matcher(old_state): +def _missing_state_matcher(old_state: States) -> Any: # The below removes state change events that do not have # and old_state or the old_state is missing (newly added entities) # or the new_state is missing (removed entities) @@ -557,37 +570,64 @@ def _missing_state_matcher(old_state): ) -def _continuous_entity_matcher(): - # - # Prefilter out continuous domains that have - # ATTR_UNIT_OF_MEASUREMENT as its much faster in sql. - # +def _not_continuous_entity_matcher() -> Any: + """Match non continuous entities.""" return sqlalchemy.or_( - sqlalchemy.not_(States.domain.in_(CONTINUOUS_DOMAINS)), - sqlalchemy.not_(States.attributes.contains(UNIT_OF_MEASUREMENT_JSON)), - sqlalchemy.not_( - StateAttributes.shared_attrs.contains(UNIT_OF_MEASUREMENT_JSON) - ), + _not_continuous_domain_matcher(), + sqlalchemy.and_( + _continuous_domain_matcher, _not_uom_attributes_matcher() + ).self_group(), ) -def _apply_event_time_filter(events_query, start_day, end_day): +def _not_continuous_domain_matcher() -> Any: + """Match not continuous domains.""" + return sqlalchemy.and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _continuous_domain_matcher() -> Any: + """Match continuous domains.""" + return sqlalchemy.or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _not_uom_attributes_matcher() -> Any: + """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" + return ~StateAttributes.shared_attrs.like( + UNIT_OF_MEASUREMENT_JSON_LIKE + ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) + + +def _apply_event_time_filter(events_query: Query, start_day: dt, end_day: dt) -> Query: return events_query.filter( (Events.time_fired > start_day) & (Events.time_fired < end_day) ) -def _apply_event_types_filter(hass, query, event_types): +def _apply_event_types_filter( + hass: HomeAssistant, query: Query, event_types: list[str] +) -> Query: return query.filter( Events.event_type.in_(event_types + list(hass.data.get(DOMAIN, {}))) ) -def _apply_event_entity_id_matchers(events_query, entity_ids): +def _apply_event_entity_id_matchers( + events_query: Query, entity_ids: Iterable[str] +) -> Query: return events_query.filter( sqlalchemy.or_( *( - Events.event_data.contains(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) + Events.event_data.like(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) for entity_id in entity_ids ) ) @@ -694,7 +734,7 @@ class LazyEventPartialState: "event_type", "entity_id", "state", - "domain", + "_domain", "context_id", "context_user_id", "context_parent_id", @@ -707,15 +747,22 @@ class LazyEventPartialState: self._event_data = None self._time_fired_isoformat = None self._attributes = None + self._domain = None self.event_type = self._row.event_type self.entity_id = self._row.entity_id self.state = self._row.state - self.domain = self._row.domain self.context_id = self._row.context_id self.context_user_id = self._row.context_user_id self.context_parent_id = self._row.context_parent_id self.time_fired_minute = self._row.time_fired.minute + @property + def domain(self): + """Return the domain for the state.""" + if self._domain is None: + self._domain = split_entity_id(self.entity_id)[0] + return self._domain + @property def attributes_icon(self): """Extract the icon from the decoded attributes or json.""" diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index da128d69dc9..6f5597ac268 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -7,7 +7,7 @@ from itertools import groupby import logging import time -from sqlalchemy import Text, and_, bindparam, func +from sqlalchemy import Text, and_, bindparam, func, or_ from sqlalchemy.ext import baked from sqlalchemy.sql.expression import literal @@ -30,14 +30,16 @@ _LOGGER = logging.getLogger(__name__) STATE_KEY = "state" LAST_CHANGED_KEY = "last_changed" -SIGNIFICANT_DOMAINS = ( +SIGNIFICANT_DOMAINS = { "climate", "device_tracker", "humidifier", "thermostat", "water_heater", -) -IGNORE_DOMAINS = ("zone", "scene") +} +SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in SIGNIFICANT_DOMAINS] +IGNORE_DOMAINS = {"zone", "scene"} +IGNORE_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in IGNORE_DOMAINS] NEED_ATTRIBUTE_DOMAINS = { "climate", "humidifier", @@ -47,7 +49,6 @@ NEED_ATTRIBUTE_DOMAINS = { } BASE_STATES = [ - States.domain, States.entity_id, States.state, States.last_changed, @@ -106,26 +107,42 @@ def get_significant_states_with_session( query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) - if significant_changes_only: - baked_query += lambda q: q.filter( - ( - States.domain.in_(SIGNIFICANT_DOMAINS) - | (States.last_changed == States.last_updated) + if entity_ids is not None and len(entity_ids) == 1: + if ( + significant_changes_only + and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS + ): + baked_query += lambda q: q.filter( + States.last_changed == States.last_updated + ) + elif significant_changes_only: + baked_query += lambda q: q.filter( + or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE + ], + (States.last_changed == States.last_updated), ) - & (States.last_updated > bindparam("start_time")) ) - else: - baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) if entity_ids is not None: baked_query += lambda q: q.filter( States.entity_id.in_(bindparam("entity_ids", expanding=True)) ) else: - baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS)) + baked_query += lambda q: q.filter( + and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE + ] + ) + ) if filters: filters.bake(baked_query) + baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) @@ -365,7 +382,8 @@ def _get_states_with_session( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, ) - query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) + for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE: + query = query.filter(~States.entity_id.like(entity_domain)) if filters: query = filters.apply(query) if not no_attributes: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 004a98d6e43..292abf87fd7 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -30,11 +30,10 @@ from homeassistant.const import ( MAX_LENGTH_EVENT_CONTEXT_ID, MAX_LENGTH_EVENT_EVENT_TYPE, MAX_LENGTH_EVENT_ORIGIN, - MAX_LENGTH_STATE_DOMAIN, MAX_LENGTH_STATE_ENTITY_ID, MAX_LENGTH_STATE_STATE, ) -from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.core import Context, Event, EventOrigin, State import homeassistant.util.dt as dt_util from .const import JSON_DUMP @@ -157,7 +156,6 @@ class States(Base): # type: ignore[misc,valid-type] ) __tablename__ = TABLE_STATES state_id = Column(Integer, Identity(), primary_key=True) - domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) state = Column(String(MAX_LENGTH_STATE_STATE)) attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) @@ -178,7 +176,7 @@ class States(Base): # type: ignore[misc,valid-type] """Return string representation of instance for debugging.""" return ( f" Date: Sun, 20 Mar 2022 01:28:44 -1000 Subject: [PATCH 0555/1054] Add shutdown guard to Recorder pool in case there is no connection (#68407) --- homeassistant/components/recorder/pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 76b8aceb30f..02985f9d0c3 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -37,7 +37,7 @@ class RecorderPool(SingletonThreadPool, NullPool): def shutdown(self): """Close the connection.""" - if self.recorder_or_dbworker and (conn := self._conn.current()): + if self.recorder_or_dbworker and self._conn and (conn := self._conn.current()): conn.close() def dispose(self): From 994ea04c857914c0917376a4a4816237a3dc7a96 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Mar 2022 01:14:07 +1300 Subject: [PATCH 0556/1054] Add device_id into ESPHome event data (#68408) --- homeassistant/components/esphome/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0154e2eba28..fd9b5dfd6d2 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -31,6 +31,7 @@ from homeassistant import const from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_HOST, CONF_MODE, CONF_PASSWORD, @@ -192,7 +193,13 @@ async def async_setup_entry( # noqa: C901 hass.async_create_task(tag.async_scan_tag(tag_id, device_id)) return - hass.bus.async_fire(service.service, service_data) + hass.bus.async_fire( + service.service, + { + ATTR_DEVICE_ID: device_id, + **service_data, + }, + ) else: hass.async_create_task( hass.services.async_call( From 972afc5ceae2057aa85f81e924ebb18a593fce46 Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Sun, 20 Mar 2022 16:50:16 +0300 Subject: [PATCH 0557/1054] Add Cover to Z-Wave.Me integration (#68233) * Cover integration * isort fix * Update homeassistant/components/zwave_me/cover.py Co-authored-by: Martin Hjelmare * Update cover.py * Update cover.py * Apply suggestions from code review Co-authored-by: Martin Hjelmare * coveragerc for cover * Fix position range * Clean up Co-authored-by: Dmitry Vlasov Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/zwave_me/const.py | 2 + homeassistant/components/zwave_me/cover.py | 72 +++++++++++++++++++ .../components/zwave_me/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/zwave_me/cover.py diff --git a/.coveragerc b/.coveragerc index 47400426202..e5619ac4d67 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1469,6 +1469,7 @@ omit = homeassistant/components/zwave_me/__init__.py homeassistant/components/zwave_me/binary_sensor.py homeassistant/components/zwave_me/button.py + homeassistant/components/zwave_me/cover.py homeassistant/components/zwave_me/climate.py homeassistant/components/zwave_me/helpers.py homeassistant/components/zwave_me/light.py diff --git a/homeassistant/components/zwave_me/const.py b/homeassistant/components/zwave_me/const.py index 87d740f1ece..cbb096c91f3 100644 --- a/homeassistant/components/zwave_me/const.py +++ b/homeassistant/components/zwave_me/const.py @@ -12,6 +12,7 @@ class ZWaveMePlatform(StrEnum): BINARY_SENSOR = "sensorBinary" BUTTON = "toggleButton" CLIMATE = "thermostat" + COVER = "motor" LOCK = "doorlock" NUMBER = "switchMultilevel" SWITCH = "switchBinary" @@ -25,6 +26,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, + Platform.COVER, Platform.LIGHT, Platform.LOCK, Platform.NUMBER, diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py new file mode 100644 index 00000000000..0425a99b568 --- /dev/null +++ b/homeassistant/components/zwave_me/cover.py @@ -0,0 +1,72 @@ +"""Representation of a cover.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ZWaveMeEntity +from .const import DOMAIN, ZWaveMePlatform + +DEVICE_NAME = ZWaveMePlatform.COVER + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the cover platform.""" + + @callback + def add_new_device(new_device): + controller = hass.data[DOMAIN][config_entry.entry_id] + cover = ZWaveMeCover(controller, new_device) + + async_add_entities( + [ + cover, + ] + ) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device + ) + ) + + +class ZWaveMeCover(ZWaveMeEntity, CoverEntity): + """Representation of a ZWaveMe Multilevel Cover.""" + + def close_cover(self, **kwargs): + """Close cover.""" + self.controller.zwave_api.send_command(self.device.id, "exact?level=0") + + def open_cover(self, **kwargs): + """Open cover.""" + self.controller.zwave_api.send_command(self.device.id, "exact?level=99") + + def set_cover_position(self, **kwargs: Any) -> None: + """Update the current value.""" + value = kwargs[ATTR_POSITION] + self.controller.zwave_api.send_command( + self.device.id, f"exact?level={str(min(value, 99))}" + ) + + @property + def current_cover_position(self) -> int | None: + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self.device.level + + @property + def supported_features(self) -> int: + """Return the supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 2e6efd9576b..ed994594ff0 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", "requirements": [ - "zwave_me_ws==0.2.2", + "zwave_me_ws==0.2.3", "url-normalize==1.4.1" ], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 2ce5287b8b8..7334c6ba64c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2497,4 +2497,4 @@ zm-py==0.5.2 zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me -zwave_me_ws==0.2.2 +zwave_me_ws==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f165fe65f7..243e201638f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1599,4 +1599,4 @@ zigpy==0.43.0 zwave-js-server-python==0.35.2 # homeassistant.components.zwave_me -zwave_me_ws==0.2.2 +zwave_me_ws==0.2.3 From 3b798ee14abad655fc09858653bccdb688d67dd9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 20 Mar 2022 16:29:50 +0100 Subject: [PATCH 0558/1054] Fix pip_check (#68421) --- .core_files.yaml | 1 + .github/workflows/ci.yaml | 2 +- script/pip_check | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.core_files.yaml b/.core_files.yaml index 85152a80aef..f154af62560 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -128,6 +128,7 @@ other: &other requirements: &requirements - .github/workflows/** - homeassistant/package_constraints.txt + - script/pip_check - requirements*.txt - setup.cfg diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 35d0bd06287..8bf509a3787 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -686,7 +686,7 @@ jobs: pip-check: runs-on: ubuntu-latest - if: needs.changes.outputs.requirements == 'true' + if: needs.changes.outputs.requirements == 'true' || github.event.inputs.full == 'true' needs: - changes - prepare-tests diff --git a/script/pip_check b/script/pip_check index 9d5ec6c87ec..6f48ce15bb1 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=4 +DEPENDENCY_CONFLICTS=7 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) @@ -14,6 +14,7 @@ then echo "------" echo "Requirements change added another dependency conflict." echo "Make sure to check the 'pip check' output above!" + echo "Expected $DEPENDENCY_CONFLICTS conflicts, got $LINE_COUNT." exit 1 elif [[ $((LINE_COUNT)) -lt $DEPENDENCY_CONFLICTS ]] then From 1013f77013032aeddffb11063909c442651988cd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 20 Mar 2022 11:19:32 -0600 Subject: [PATCH 0559/1054] Bump simplisafe-python to 2022.03.0 (#68424) --- 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 deb9577d576..7c8a9673851 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==2022.02.1"], + "requirements": ["simplisafe-python==2022.03.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7334c6ba64c..ce8a66e7496 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2136,7 +2136,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.02.1 +simplisafe-python==2022.03.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 243e201638f..1891d670169 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1361,7 +1361,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.02.1 +simplisafe-python==2022.03.0 # homeassistant.components.slack slackclient==2.5.0 From 89cfb4e86f10a3374db15188e21a45731886c533 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 20 Mar 2022 18:50:37 +0100 Subject: [PATCH 0560/1054] Add yale_smart_alarm to strict typing (#68422) * Add yale_smart_alarm to strict typing * Type as Any --- .strict-typing | 1 + .../yale_smart_alarm/alarm_control_panel.py | 6 +++--- .../components/yale_smart_alarm/binary_sensor.py | 4 ++-- homeassistant/components/yale_smart_alarm/lock.py | 12 +++++++----- mypy.ini | 11 +++++++++++ 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index 498f760bc93..c9f3a978047 100644 --- a/.strict-typing +++ b/.strict-typing @@ -224,6 +224,7 @@ homeassistant.components.wemo.* homeassistant.components.whois.* homeassistant.components.wiz.* homeassistant.components.worldclock.* +homeassistant.components.yale_smart_alarm.* homeassistant.components.zodiac.* homeassistant.components.zeroconf.* homeassistant.components.zone.* diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index ad38f30991b..27d0ed8b631 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -92,15 +92,15 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): self._attr_name = coordinator.entry.data[CONF_NAME] self._attr_unique_id = coordinator.entry.entry_id - async def async_alarm_disarm(self, code=None) -> None: + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" return await self.async_set_alarm(YALE_STATE_DISARM, code) - async def async_alarm_arm_home(self, code=None) -> None: + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" return await self.async_set_alarm(YALE_STATE_ARM_PARTIAL, code) - async def async_alarm_arm_away(self, code=None) -> None: + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" return await self.async_set_alarm(YALE_STATE_ARM_FULL, code) diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index 42f8b446abc..566dbed8c33 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -69,7 +69,7 @@ class YaleDoorSensor(YaleEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.coordinator.data["sensor_map"][self._attr_unique_id] == "open" + return bool(self.coordinator.data["sensor_map"][self._attr_unique_id] == "open") class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): @@ -93,7 +93,7 @@ class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return ( + return bool( self.coordinator.data["status"][self.entity_description.key] != "main.normal" ) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index 0ac16d23276..a97a98a2afb 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -1,7 +1,7 @@ """Lock for Yale Alarm.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -47,12 +47,14 @@ class YaleDoorlock(YaleEntity, LockEntity): super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" - code = kwargs.get(ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE)) + code: str | None = kwargs.get( + ATTR_CODE, self.coordinator.entry.options.get(CONF_CODE) + ) return await self.async_set_lock("unlocked", code) - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Send lock command.""" return await self.async_set_lock("locked", None) @@ -88,4 +90,4 @@ class YaleDoorlock(YaleEntity, LockEntity): @property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" - return self.coordinator.data["lock_map"][self._attr_unique_id] == "locked" + return bool(self.coordinator.data["lock_map"][self._attr_unique_id] == "locked") diff --git a/mypy.ini b/mypy.ini index 0cde5eda9a3..1a87e77298f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2266,6 +2266,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.yale_smart_alarm.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.zodiac.*] check_untyped_defs = true disallow_incomplete_defs = true From a136cf7086334fc53e47ebce42f8a8e4b5ca8a50 Mon Sep 17 00:00:00 2001 From: Baptiste Candellier Date: Sun, 20 Mar 2022 19:00:16 +0100 Subject: [PATCH 0561/1054] Remove SmartHab integration (#67874) --- .coveragerc | 3 - CODEOWNERS | 2 - homeassistant/components/smarthab/__init__.py | 85 ------------ .../components/smarthab/config_flow.py | 78 ----------- homeassistant/components/smarthab/cover.py | 100 -------------- homeassistant/components/smarthab/light.py | 74 ---------- .../components/smarthab/manifest.json | 10 -- .../components/smarthab/strings.json | 19 --- .../components/smarthab/translations/bg.json | 12 -- .../components/smarthab/translations/ca.json | 19 --- .../components/smarthab/translations/cs.json | 17 --- .../components/smarthab/translations/de.json | 19 --- .../components/smarthab/translations/el.json | 19 --- .../components/smarthab/translations/en.json | 19 --- .../components/smarthab/translations/es.json | 19 --- .../components/smarthab/translations/et.json | 19 --- .../components/smarthab/translations/fr.json | 19 --- .../components/smarthab/translations/he.json | 16 --- .../components/smarthab/translations/hu.json | 19 --- .../components/smarthab/translations/id.json | 19 --- .../components/smarthab/translations/it.json | 19 --- .../components/smarthab/translations/ja.json | 19 --- .../components/smarthab/translations/ko.json | 19 --- .../components/smarthab/translations/lb.json | 19 --- .../components/smarthab/translations/nl.json | 19 --- .../components/smarthab/translations/no.json | 19 --- .../components/smarthab/translations/pl.json | 19 --- .../smarthab/translations/pt-BR.json | 19 --- .../components/smarthab/translations/pt.json | 16 --- .../components/smarthab/translations/ru.json | 19 --- .../components/smarthab/translations/sk.json | 14 -- .../components/smarthab/translations/tr.json | 19 --- .../components/smarthab/translations/uk.json | 19 --- .../smarthab/translations/zh-Hans.json | 7 - .../smarthab/translations/zh-Hant.json | 19 --- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/smarthab/__init__.py | 1 - tests/components/smarthab/test_config_flow.py | 126 ------------------ 40 files changed, 986 deletions(-) delete mode 100644 homeassistant/components/smarthab/__init__.py delete mode 100644 homeassistant/components/smarthab/config_flow.py delete mode 100644 homeassistant/components/smarthab/cover.py delete mode 100644 homeassistant/components/smarthab/light.py delete mode 100644 homeassistant/components/smarthab/manifest.json delete mode 100644 homeassistant/components/smarthab/strings.json delete mode 100644 homeassistant/components/smarthab/translations/bg.json delete mode 100644 homeassistant/components/smarthab/translations/ca.json delete mode 100644 homeassistant/components/smarthab/translations/cs.json delete mode 100644 homeassistant/components/smarthab/translations/de.json delete mode 100644 homeassistant/components/smarthab/translations/el.json delete mode 100644 homeassistant/components/smarthab/translations/en.json delete mode 100644 homeassistant/components/smarthab/translations/es.json delete mode 100644 homeassistant/components/smarthab/translations/et.json delete mode 100644 homeassistant/components/smarthab/translations/fr.json delete mode 100644 homeassistant/components/smarthab/translations/he.json delete mode 100644 homeassistant/components/smarthab/translations/hu.json delete mode 100644 homeassistant/components/smarthab/translations/id.json delete mode 100644 homeassistant/components/smarthab/translations/it.json delete mode 100644 homeassistant/components/smarthab/translations/ja.json delete mode 100644 homeassistant/components/smarthab/translations/ko.json delete mode 100644 homeassistant/components/smarthab/translations/lb.json delete mode 100644 homeassistant/components/smarthab/translations/nl.json delete mode 100644 homeassistant/components/smarthab/translations/no.json delete mode 100644 homeassistant/components/smarthab/translations/pl.json delete mode 100644 homeassistant/components/smarthab/translations/pt-BR.json delete mode 100644 homeassistant/components/smarthab/translations/pt.json delete mode 100644 homeassistant/components/smarthab/translations/ru.json delete mode 100644 homeassistant/components/smarthab/translations/sk.json delete mode 100644 homeassistant/components/smarthab/translations/tr.json delete mode 100644 homeassistant/components/smarthab/translations/uk.json delete mode 100644 homeassistant/components/smarthab/translations/zh-Hans.json delete mode 100644 homeassistant/components/smarthab/translations/zh-Hant.json delete mode 100644 tests/components/smarthab/__init__.py delete mode 100644 tests/components/smarthab/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index e5619ac4d67..47b07724421 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1061,9 +1061,6 @@ omit = homeassistant/components/smappee/sensor.py homeassistant/components/smappee/switch.py homeassistant/components/smarty/* - homeassistant/components/smarthab/__init__.py - homeassistant/components/smarthab/cover.py - homeassistant/components/smarthab/light.py homeassistant/components/sms/__init__.py homeassistant/components/sms/const.py homeassistant/components/sms/gateway.py diff --git a/CODEOWNERS b/CODEOWNERS index 1a7c960730b..d013572790f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -908,8 +908,6 @@ homeassistant/components/smappee/* @bsmappee tests/components/smappee/* @bsmappee homeassistant/components/smart_meter_texas/* @grahamwetzler tests/components/smart_meter_texas/* @grahamwetzler -homeassistant/components/smarthab/* @outadoc -tests/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre tests/components/smartthings/* @andrewsayre homeassistant/components/smarttub/* @mdz diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py deleted file mode 100644 index a4dc1a43f31..00000000000 --- a/homeassistant/components/smarthab/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Support for SmartHab device integration.""" -import logging - -import pysmarthab -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "smarthab" -DATA_HUB = "hub" -PLATFORMS = [Platform.LIGHT, Platform.COVER] - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SmartHab platform.""" - - hass.data.setdefault(DOMAIN, {}) - - if DOMAIN not in config: - return True - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up config entry for SmartHab integration.""" - - # Assign configuration variables - username = entry.data[CONF_EMAIL] - password = entry.data[CONF_PASSWORD] - - # Setup connection with SmartHab API - hub = pysmarthab.SmartHab() - - try: - await hub.async_login(username, password) - except pysmarthab.RequestFailedException as err: - _LOGGER.exception("Error while trying to reach SmartHab API") - raise ConfigEntryNotReady from err - - # Pass hub object to child platforms - hass.data[DOMAIN][entry.entry_id] = {DATA_HUB: hub} - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload config entry from SmartHab integration.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/smarthab/config_flow.py b/homeassistant/components/smarthab/config_flow.py deleted file mode 100644 index 826454ab4d8..00000000000 --- a/homeassistant/components/smarthab/config_flow.py +++ /dev/null @@ -1,78 +0,0 @@ -"""SmartHab configuration flow.""" -import logging - -import pysmarthab -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD - -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -class SmartHabConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """SmartHab config flow.""" - - VERSION = 1 - - def _show_setup_form(self, user_input=None, errors=None): - """Show the setup form to the user.""" - - if user_input is None: - user_input = {} - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required( - CONF_EMAIL, default=user_input.get(CONF_EMAIL, "") - ): str, - vol.Required(CONF_PASSWORD): str, - } - ), - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None): - """Handle a flow initiated by the user.""" - errors = {} - - if user_input is None: - return self._show_setup_form(user_input, None) - - username = user_input[CONF_EMAIL] - password = user_input[CONF_PASSWORD] - - # Check if already configured - if self.unique_id is None: - await self.async_set_unique_id(username) - self._abort_if_unique_id_configured() - - # Setup connection with SmartHab API - hub = pysmarthab.SmartHab() - - try: - await hub.async_login(username, password) - - # Verify that passed in configuration works - if hub.is_logged_in(): - return self.async_create_entry( - title=username, data={CONF_EMAIL: username, CONF_PASSWORD: password} - ) - - errors["base"] = "invalid_auth" - except pysmarthab.RequestFailedException: - _LOGGER.exception("Error while trying to reach SmartHab API") - errors["base"] = "service" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected error during login") - errors["base"] = "unknown" - - return self._show_setup_form(user_input, errors) - - async def async_step_import(self, import_info): - """Handle import from legacy config.""" - return await self.async_step_user(import_info) diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py deleted file mode 100644 index 3c581774b03..00000000000 --- a/homeassistant/components/smarthab/cover.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Support for SmartHab device integration.""" -from datetime import timedelta -import logging - -import pysmarthab -from requests.exceptions import Timeout - -from homeassistant.components.cover import ( - ATTR_POSITION, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_SET_POSITION, - CoverDeviceClass, - CoverEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import DATA_HUB, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up SmartHab covers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id][DATA_HUB] - - entities = ( - SmartHabCover(cover) - for cover in await hub.async_get_device_list() - if isinstance(cover, pysmarthab.Shutter) - ) - - async_add_entities(entities, True) - - -class SmartHabCover(CoverEntity): - """Representation a cover.""" - - _attr_device_class = CoverDeviceClass.WINDOW - - def __init__(self, cover): - """Initialize a SmartHabCover.""" - self._cover = cover - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._cover.device_id - - @property - def name(self) -> str: - """Return the display name of this cover.""" - return self._cover.label - - @property - def current_cover_position(self) -> int: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - return self._cover.state - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - - @property - def is_closed(self) -> bool: - """Return if the cover is closed or not.""" - return self._cover.state == 0 - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - await self._cover.async_open() - - async def async_close_cover(self, **kwargs): - """Close cover.""" - await self._cover.async_close() - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - await self._cover.async_set_state(kwargs[ATTR_POSITION]) - - async def async_update(self): - """Fetch new state data for this cover.""" - try: - await self._cover.async_update() - except Timeout: - _LOGGER.error( - "Reached timeout while updating cover %s from API", self.entity_id - ) diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py deleted file mode 100644 index 9fcc952b24a..00000000000 --- a/homeassistant/components/smarthab/light.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Support for SmartHab device integration.""" -from datetime import timedelta -import logging - -import pysmarthab -from requests.exceptions import Timeout - -from homeassistant.components.light import LightEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import DATA_HUB, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up SmartHab lights from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id][DATA_HUB] - - entities = ( - SmartHabLight(light) - for light in await hub.async_get_device_list() - if isinstance(light, pysmarthab.Light) - ) - - async_add_entities(entities, True) - - -class SmartHabLight(LightEntity): - """Representation of a SmartHab Light.""" - - def __init__(self, light): - """Initialize a SmartHabLight.""" - self._light = light - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._light.device_id - - @property - def name(self) -> str: - """Return the display name of this light.""" - return self._light.label - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self._light.state - - async def async_turn_on(self, **kwargs): - """Instruct the light to turn on.""" - await self._light.async_turn_on() - - async def async_turn_off(self, **kwargs): - """Instruct the light to turn off.""" - await self._light.async_turn_off() - - async def async_update(self): - """Fetch new state data for this light.""" - try: - await self._light.async_update() - except Timeout: - _LOGGER.error( - "Reached timeout while updating light %s from API", self.entity_id - ) diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json deleted file mode 100644 index 7974215de64..00000000000 --- a/homeassistant/components/smarthab/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "smarthab", - "name": "SmartHab", - "documentation": "https://www.home-assistant.io/integrations/smarthab", - "config_flow": true, - "requirements": ["smarthab==0.21"], - "codeowners": ["@outadoc"], - "iot_class": "cloud_polling", - "loggers": ["pysmarthab"] -} diff --git a/homeassistant/components/smarthab/strings.json b/homeassistant/components/smarthab/strings.json deleted file mode 100644 index e1cb6eb4411..00000000000 --- a/homeassistant/components/smarthab/strings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "service": "Error while trying to reach SmartHab. Service might be down. Check your connection.", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "step": { - "user": { - "data": { - "password": "[%key:common::config_flow::data::password%]", - "email": "[%key:common::config_flow::data::email%]" - }, - "description": "For technical reasons, be sure to use a secondary account specific to your Home Assistant setup. You can create one from the SmartHab application.", - "title": "Setup SmartHab" - } - } - } -} diff --git a/homeassistant/components/smarthab/translations/bg.json b/homeassistant/components/smarthab/translations/bg.json deleted file mode 100644 index 75022ed3005..00000000000 --- a/homeassistant/components/smarthab/translations/bg.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "email": "Email", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ca.json b/homeassistant/components/smarthab/translations/ca.json deleted file mode 100644 index cac81a0acea..00000000000 --- a/homeassistant/components/smarthab/translations/ca.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "service": "Error en l'intent de connexi\u00f3 a SmartHab. Pot ser que el servei no estigui disponible. Comprova la connexi\u00f3.", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "email": "Correu electr\u00f2nic", - "password": "Contrasenya" - }, - "description": "Per motius t\u00e8cnics, assegura't utilitzar un compte secundari espec\u00edfic per a la configuraci\u00f3 de Home Assistant. Pots crear-ne un des de l'aplicaci\u00f3 SmartHab.", - "title": "Configuraci\u00f3 de SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/cs.json b/homeassistant/components/smarthab/translations/cs.json deleted file mode 100644 index 1e862ff0069..00000000000 --- a/homeassistant/components/smarthab/translations/cs.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "email": "E-mail", - "password": "Heslo" - }, - "title": "Nastaven\u00ed SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/de.json b/homeassistant/components/smarthab/translations/de.json deleted file mode 100644 index ca2bf3373f2..00000000000 --- a/homeassistant/components/smarthab/translations/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "service": "Fehler beim Versuch, SmartHab zu erreichen. Der Dienst ist m\u00f6glicherweise nicht erreichbar. Pr\u00fcfe deine Verbindung.", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "email": "E-Mail", - "password": "Passwort" - }, - "description": "Stelle aus technischen Gr\u00fcnden sicher, dass du ein sekund\u00e4res Konto speziell f\u00fcr deine Home Assistant-Einrichtung verwendest. Du kannst ein solches Konto \u00fcber die SmartHab-Anwendung erstellen.", - "title": "SmartHab einrichten" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/el.json b/homeassistant/components/smarthab/translations/el.json deleted file mode 100644 index 43de6c1ca89..00000000000 --- a/homeassistant/components/smarthab/translations/el.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "service": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf SmartHab. \u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2.", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u0393\u03b9\u03b1 \u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03bf\u03cd\u03c2 \u03bb\u03cc\u03b3\u03bf\u03c5\u03c2, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03b5\u03cd\u03bf\u03bd\u03c4\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03b5\u03b9\u03b4\u03b9\u03ba\u03ac \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae SmartHab.", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/en.json b/homeassistant/components/smarthab/translations/en.json deleted file mode 100644 index 854f7a7ddb5..00000000000 --- a/homeassistant/components/smarthab/translations/en.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Invalid authentication", - "service": "Error while trying to reach SmartHab. Service might be down. Check your connection.", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Password" - }, - "description": "For technical reasons, be sure to use a secondary account specific to your Home Assistant setup. You can create one from the SmartHab application.", - "title": "Setup SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/es.json b/homeassistant/components/smarthab/translations/es.json deleted file mode 100644 index b111ddbccd1..00000000000 --- a/homeassistant/components/smarthab/translations/es.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "service": "Error al intentar contactar con SmartHab. El servicio podr\u00eda estar ca\u00eddo. Verifica tu conexi\u00f3n.", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "email": "Correo electr\u00f3nico", - "password": "Contrase\u00f1a" - }, - "description": "Por razones t\u00e9cnicas, aseg\u00farate de usar una cuenta secundaria espec\u00edfica para su configuraci\u00f3n de Home Assistant. Puedes crear una desde la aplicaci\u00f3n SmartHab.", - "title": "Configurar SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/et.json b/homeassistant/components/smarthab/translations/et.json deleted file mode 100644 index f7c91f8dce6..00000000000 --- a/homeassistant/components/smarthab/translations/et.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Tuvastamise viga", - "service": "Viga SmartHabiga \u00fchendumisel. Teenus v\u00f5ib olla h\u00e4iritud. Kontrolli oma \u00fchendust.", - "unknown": "Tundmatu viga" - }, - "step": { - "user": { - "data": { - "email": "E-post", - "password": "Salas\u00f5na" - }, - "description": "Tehnilistel p\u00f5hjustel kasuta kindlasti oma Home Assistanti seadistustele vastavat sekundaarset kontot. Selle saad luua rakendusest SmartHab.", - "title": "Seadista SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/fr.json b/homeassistant/components/smarthab/translations/fr.json deleted file mode 100644 index 5e3381a50e6..00000000000 --- a/homeassistant/components/smarthab/translations/fr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Authentification non valide", - "service": "Erreur de connexion \u00e0 SmartHab. V\u00e9rifiez votre connexion. Le service peut \u00eatre indisponible.", - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "email": "Courriel", - "password": "Mot de passe" - }, - "description": "Pour des raisons techniques, utilisez un compte sp\u00e9cifique \u00e0 Home Assistant. Vous pouvez cr\u00e9er un compte secondaire depuis l'application SmartHab.", - "title": "Configurer SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/he.json b/homeassistant/components/smarthab/translations/he.json deleted file mode 100644 index c00515506ac..00000000000 --- a/homeassistant/components/smarthab/translations/he.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "email": "\u05d3\u05d5\u05d0\"\u05dc", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json deleted file mode 100644 index 2e3cf430a9f..00000000000 --- a/homeassistant/components/smarthab/translations/hu.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service": "Hiba t\u00f6rt\u00e9nt a SmartHab el\u00e9r\u00e9se k\u00f6zben. A szolg\u00e1ltat\u00e1s le\u00e1llhat. Ellen\u0151rizze a kapcsolatot.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "email": "E-mail", - "password": "Jelsz\u00f3" - }, - "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet.", - "title": "A SmartHab be\u00e1ll\u00edt\u00e1sa" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/id.json b/homeassistant/components/smarthab/translations/id.json deleted file mode 100644 index 7a776eac304..00000000000 --- a/homeassistant/components/smarthab/translations/id.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autentikasi tidak valid", - "service": "Terjadi kesalahan saat mencoba menjangkau SmartHab. Layanan mungkin sedang mengalami gangguan. Periksa koneksi Anda.", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Kata Sandi" - }, - "description": "Untuk alasan teknis, pastikan untuk menggunakan akun sekunder khusus untuk penyiapan Home Assistant Anda. Anda dapat membuatnya dari aplikasi SmartHab.", - "title": "Siapkan SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/it.json b/homeassistant/components/smarthab/translations/it.json deleted file mode 100644 index b74da607f77..00000000000 --- a/homeassistant/components/smarthab/translations/it.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autenticazione non valida", - "service": "Errore durante il tentativo di raggiungere SmartHab. Il servizio potrebbe non essere attivo. Controlla la connessione.", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Password" - }, - "description": "Per motivi tecnici, assicurati di utilizzare un account secondario specifico per la tua configurazione di Home Assistant. \u00c8 possibile crearne uno dall'applicazione SmartHab.", - "title": "Configurazione SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ja.json b/homeassistant/components/smarthab/translations/ja.json deleted file mode 100644 index a463f259c2a..00000000000 --- a/homeassistant/components/smarthab/translations/ja.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "service": "SmartHab\u306b\u30a2\u30af\u30bb\u30b9\u3057\u3088\u3046\u3068\u3057\u3066\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30b5\u30fc\u30d3\u30b9\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "email": "E\u30e1\u30fc\u30eb", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u6280\u8853\u7684\u306a\u7406\u7531\u304b\u3089\u3001Home Assistant\u306e\u8a2d\u5b9a\u306b\u56fa\u6709\u306e\u30bb\u30ab\u30f3\u30c0\u30ea\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002SmartHab\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u304b\u3089\u4f5c\u6210\u3067\u304d\u307e\u3059\u3002", - "title": "SmartHab\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ko.json b/homeassistant/components/smarthab/translations/ko.json deleted file mode 100644 index 1641555b412..00000000000 --- a/homeassistant/components/smarthab/translations/ko.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "service": "SmartHab \uc5d0 \uc811\uc18d\ud558\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc11c\ube44\uc2a4\uac00 \ub2e4\uc6b4\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc5f0\uacb0\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "email": "\uc774\uba54\uc77c", - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uae30\uc220\uc801\uc778 \uc774\uc720\ub85c Home Assistant \uc124\uc815\uacfc \uad00\ub828\ub41c \ubcf4\uc870 \uacc4\uc815\uc744 \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. SmartHab \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc5d0\uc11c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "SmartHab \uc124\uce58\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/lb.json b/homeassistant/components/smarthab/translations/lb.json deleted file mode 100644 index e651190a1e2..00000000000 --- a/homeassistant/components/smarthab/translations/lb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "service": "Feeler beim verbanne mat SmartHab. De Service ass viellaicht net ereechbar. Iwwerpr\u00e9if deng Verbindung.", - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "email": "E-Mail", - "password": "Passwuert" - }, - "description": "W\u00e9inst technesche Gr\u00ebnn soll een zweeten Kont benotz gin fir d\u00e4in Home Assistant. Du kanns een zous\u00e4tzleche Kont an der SmartHab Applikatioun erstellen.", - "title": "SmartHab ariichten" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/nl.json b/homeassistant/components/smarthab/translations/nl.json deleted file mode 100644 index 31a02ae2b97..00000000000 --- a/homeassistant/components/smarthab/translations/nl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ongeldige authenticatie", - "service": "Fout bij het bereiken van SmartHab. De service is mogelijk uitgevallen. Controleer uw verbinding.", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "email": "E-mail", - "password": "Wachtwoord" - }, - "description": "Om technische redenen moet u een tweede account gebruiken dat specifiek is voor uw Home Assistant-installatie. U kunt er een aanmaken vanuit de SmartHab-toepassing.", - "title": "Stel SmartHab in" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/no.json b/homeassistant/components/smarthab/translations/no.json deleted file mode 100644 index ed2beaf7836..00000000000 --- a/homeassistant/components/smarthab/translations/no.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ugyldig godkjenning", - "service": "Feil under fors\u00f8k p\u00e5 \u00e5 n\u00e5 SmartHab. Tjenesten kan v\u00e6re nede. Sjekk tilkoblingen din.", - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "email": "E-post", - "password": "Passord" - }, - "description": "Av tekniske \u00e5rsaker m\u00e5 du s\u00f8rge for \u00e5 bruke en sekund\u00e6r konto som er spesifikk for oppsettet i Home Assistant. Du kan opprette en fra SmartHab-programmet.", - "title": "Oppsett av SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pl.json b/homeassistant/components/smarthab/translations/pl.json deleted file mode 100644 index 14ca88f1c00..00000000000 --- a/homeassistant/components/smarthab/translations/pl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie", - "service": "B\u0142\u0105d podczas pr\u00f3by osi\u0105gni\u0119cia SmartHab. Us\u0142uga mo\u017ce by\u0107 wy\u0142\u0105czna. Sprawd\u017a po\u0142\u0105czenie.", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "email": "Adres e-mail", - "password": "Has\u0142o" - }, - "description": "Ze wzgl\u0119d\u00f3w technicznych, nale\u017cy u\u017cy\u0107 dodatkowego konta, specjalnie na u\u017cytek dla Home Assistanta. Mo\u017cesz je utworzy\u0107 z poziomu aplikacji SmartHab.", - "title": "Konfiguracja SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pt-BR.json b/homeassistant/components/smarthab/translations/pt-BR.json deleted file mode 100644 index ef205c53827..00000000000 --- a/homeassistant/components/smarthab/translations/pt-BR.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "service": "Erro ao tentar acessar o SmartHab. O servi\u00e7o pode estar inoperante. Verifique sua conex\u00e3o.", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Senha" - }, - "description": "Por motivos t\u00e9cnicos, certifique-se de usar uma conta secund\u00e1ria espec\u00edfica para a configura\u00e7\u00e3o do Home Assistant. Voc\u00ea pode criar um a partir do aplicativo SmartHab.", - "title": "Configurar SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/pt.json b/homeassistant/components/smarthab/translations/pt.json deleted file mode 100644 index 7430480cc09..00000000000 --- a/homeassistant/components/smarthab/translations/pt.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "email": "Email", - "password": "Palavra-passe" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/ru.json b/homeassistant/components/smarthab/translations/ru.json deleted file mode 100644 index 45e3698034f..00000000000 --- a/homeassistant/components/smarthab/translations/ru.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "service": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SmartHab. \u0421\u0435\u0440\u0432\u0438\u0441 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041f\u043e \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043f\u0440\u0438\u0447\u0438\u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0434\u043b\u044f Home Assistant. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0451 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 SmartHab.", - "title": "SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/sk.json b/homeassistant/components/smarthab/translations/sk.json deleted file mode 100644 index 72b0304f1c3..00000000000 --- a/homeassistant/components/smarthab/translations/sk.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Neplatn\u00e9 overenie" - }, - "step": { - "user": { - "data": { - "email": "Email" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/tr.json b/homeassistant/components/smarthab/translations/tr.json deleted file mode 100644 index 699967ce8ee..00000000000 --- a/homeassistant/components/smarthab/translations/tr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "service": "SmartHab'a ula\u015fmaya \u00e7al\u0131\u015f\u0131rken hata olu\u015ftu. Servis kapal\u0131 olabilir. Ba\u011flant\u0131n\u0131z\u0131 kontrol edin.", - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "email": "E-posta", - "password": "Parola" - }, - "description": "Teknik nedenlerle, Ev Asistan\u0131 kurulumunuza \u00f6zel ikincil bir hesap kulland\u0131\u011f\u0131n\u0131zdan emin olun. SmartHab uygulamas\u0131ndan bir tane olu\u015fturabilirsiniz.", - "title": "SmartHab'\u0131 kurun" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/uk.json b/homeassistant/components/smarthab/translations/uk.json deleted file mode 100644 index 036ec0a78d4..00000000000 --- a/homeassistant/components/smarthab/translations/uk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "service": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0441\u043f\u0440\u043e\u0431\u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SmartHab. \u0421\u0435\u0440\u0432\u0456\u0441 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.", - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u0417 \u0442\u0435\u0445\u043d\u0456\u0447\u043d\u0438\u0445 \u043f\u0440\u0438\u0447\u0438\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0434\u043b\u044f Home Assistant. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0457 \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 SmartHab.", - "title": "SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/zh-Hans.json b/homeassistant/components/smarthab/translations/zh-Hans.json deleted file mode 100644 index f339adebd86..00000000000 --- a/homeassistant/components/smarthab/translations/zh-Hans.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/zh-Hant.json b/homeassistant/components/smarthab/translations/zh-Hant.json deleted file mode 100644 index 9f1d903a9b1..00000000000 --- a/homeassistant/components/smarthab/translations/zh-Hant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "service": "\u5617\u8a66\u8a2a\u554f Smarthab \u6642\u767c\u751f\u932f\u8aa4\uff0c\u670d\u52d9\u53ef\u4ee5\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u6aa2\u67e5\u9023\u7dda\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "email": "\u96fb\u5b50\u90f5\u4ef6", - "password": "\u5bc6\u78bc" - }, - "description": "\u7531\u65bc\u6280\u8853\u539f\u56e0\u3001\u8acb\u78ba\u5b9a\u6307\u5b9a Home Assistant \u8a2d\u5b9a\u5099\u7528\u5e33\u6236\u3002\u53ef\u4ee5\u900f\u904e Smarthab \u61c9\u7528\u7a0b\u5f0f\u5275\u5efa\u3002", - "title": "\u8a2d\u5b9a SmartHab" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3e6b8e10547..9fd116aa715 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -300,7 +300,6 @@ FLOWS = [ "sma", "smappee", "smart_meter_texas", - "smarthab", "smartthings", "smarttub", "smhi", diff --git a/requirements_all.txt b/requirements_all.txt index ce8a66e7496..29ceac4f462 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,9 +2153,6 @@ slixmpp==1.8.0.1 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 -# homeassistant.components.smarthab -smarthab==0.21 - # homeassistant.components.smhi smhi-pkg==1.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1891d670169..9fc53629754 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1369,9 +1369,6 @@ slackclient==2.5.0 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 -# homeassistant.components.smarthab -smarthab==0.21 - # homeassistant.components.smhi smhi-pkg==1.0.15 diff --git a/tests/components/smarthab/__init__.py b/tests/components/smarthab/__init__.py deleted file mode 100644 index 0e393ee0f9e..00000000000 --- a/tests/components/smarthab/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the SmartHab integration.""" diff --git a/tests/components/smarthab/test_config_flow.py b/tests/components/smarthab/test_config_flow.py deleted file mode 100644 index e6e9dc86101..00000000000 --- a/tests/components/smarthab/test_config_flow.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Test the SmartHab config flow.""" -from unittest.mock import patch - -import pysmarthab - -from homeassistant import config_entries -from homeassistant.components.smarthab import DOMAIN -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD - - -async def test_form(hass): - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch("pysmarthab.SmartHab.async_login"), patch( - "pysmarthab.SmartHab.is_logged_in", return_value=True - ), patch( - "homeassistant.components.smarthab.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.smarthab.async_setup_entry", return_value=True - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_EMAIL: "mock@example.com", CONF_PASSWORD: "test-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] == "create_entry" - assert result2["title"] == "mock@example.com" - assert result2["data"] == { - CONF_EMAIL: "mock@example.com", - CONF_PASSWORD: "test-password", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch("pysmarthab.SmartHab.async_login"), patch( - "pysmarthab.SmartHab.is_logged_in", return_value=False - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_EMAIL: "mock@example.com", CONF_PASSWORD: "test-password"}, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_service_error(hass): - """Test we handle service errors.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pysmarthab.SmartHab.async_login", - side_effect=pysmarthab.RequestFailedException(42), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_EMAIL: "mock@example.com", CONF_PASSWORD: "test-password"}, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "service"} - - -async def test_form_unknown_error(hass): - """Test we handle unknown errors.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "pysmarthab.SmartHab.async_login", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_EMAIL: "mock@example.com", CONF_PASSWORD: "test-password"}, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - - -async def test_import(hass): - """Test import.""" - - imported_conf = { - CONF_EMAIL: "mock@example.com", - CONF_PASSWORD: "test-password", - } - - with patch("pysmarthab.SmartHab.async_login"), patch( - "pysmarthab.SmartHab.is_logged_in", return_value=True - ), patch( - "homeassistant.components.smarthab.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.smarthab.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=imported_conf - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "mock@example.com" - assert result["data"] == { - CONF_EMAIL: "mock@example.com", - CONF_PASSWORD: "test-password", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 From 314154d5c56235a962228273f4b44406d8a9ded7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 20 Mar 2022 12:13:52 -0600 Subject: [PATCH 0562/1054] Bump aioridwell to 2022.03.0 (#68423) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/pip_check | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index e02a0ba6526..63dab1ffff3 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", "requirements": [ - "aioridwell==2021.12.2" + "aioridwell==2022.03.0" ], "codeowners": [ "@bachya" diff --git a/requirements_all.txt b/requirements_all.txt index 29ceac4f462..7ba6a53525e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aiopyarr==22.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2021.12.2 +aioridwell==2022.03.0 # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fc53629754..e9d7239f024 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aiopyarr==22.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2021.12.2 +aioridwell==2022.03.0 # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/script/pip_check b/script/pip_check index 6f48ce15bb1..5b69e1569c6 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=7 +DEPENDENCY_CONFLICTS=5 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 1d35b91a14d1c68ad666e859aa77294512b1e8ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 20 Mar 2022 20:37:01 +0100 Subject: [PATCH 0563/1054] Add calendar platform to Twente Milieu (#68190) * Add calendar platform to Twente Milieu * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Sorting... Co-authored-by: Martin Hjelmare --- .../components/twentemilieu/__init__.py | 2 +- .../components/twentemilieu/calendar.py | 101 ++++++++++++++++++ .../components/twentemilieu/const.py | 10 ++ .../components/twentemilieu/entity.py | 36 +++++++ .../components/twentemilieu/sensor.py | 31 ++---- .../components/twentemilieu/test_calendar.py | 83 ++++++++++++++ 6 files changed, 241 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/twentemilieu/calendar.py create mode 100644 homeassistant/components/twentemilieu/entity.py create mode 100644 tests/components/twentemilieu/test_calendar.py diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index cee6ffbf38e..bbe306392ba 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -20,7 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=3600) SERVICE_UPDATE = "update" SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_ID): cv.string}) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.CALENDAR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py new file mode 100644 index 00000000000..0d2768e5eb2 --- /dev/null +++ b/homeassistant/components/twentemilieu/calendar.py @@ -0,0 +1,101 @@ +"""Support for Twente Milieu Calendar.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +import homeassistant.util.dt as dt_util + +from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .entity import TwenteMilieuEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Twente Milieu calendar based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.data[CONF_ID]] + async_add_entities([TwenteMilieuCalendar(coordinator, entry)]) + + +class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEventDevice): + """Defines a Twente Milieu calendar.""" + + _attr_name = "Twente Milieu" + _attr_icon = "mdi:delete-empty" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the Twente Milieu entity.""" + super().__init__(coordinator, entry) + self._attr_unique_id = str(entry.data[CONF_ID]) + self._event: dict[str, Any] | None = None + + @property + def event(self) -> dict[str, Any] | None: + """Return the next upcoming event.""" + return self._event + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[dict[str, Any]]: + """Return calendar events within a datetime range.""" + events: list[dict[str, Any]] = [] + for waste_type, waste_dates in self.coordinator.data.items(): + events.extend( + { + "all_day": True, + "start": {"date": waste_date.isoformat()}, + "end": {"date": waste_date.isoformat()}, + "summary": WASTE_TYPE_TO_DESCRIPTION[waste_type], + } + for waste_date in waste_dates + if start_date.date() <= waste_date <= end_date.date() + ) + + return events + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + next_waste_pickup_type = None + next_waste_pickup_date = None + for waste_type, waste_dates in self.coordinator.data.items(): + if ( + waste_dates + and ( + next_waste_pickup_date is None + or waste_dates[0] # type: ignore[unreachable] + < next_waste_pickup_date + ) + and waste_dates[0] >= dt_util.now().date() + ): + next_waste_pickup_date = waste_dates[0] + next_waste_pickup_type = waste_type + + self._event = None + if next_waste_pickup_date is not None and next_waste_pickup_type is not None: + self._event = { + "all_day": True, + "start": {"date": next_waste_pickup_date.isoformat()}, + "end": {"date": next_waste_pickup_date.isoformat()}, + "summary": WASTE_TYPE_TO_DESCRIPTION[next_waste_pickup_type], + } + + super()._handle_coordinator_update() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index 95ab903cc17..c9f2f935772 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging from typing import Final +from twentemilieu import WasteType + DOMAIN: Final = "twentemilieu" LOGGER = logging.getLogger(__package__) @@ -11,3 +13,11 @@ SCAN_INTERVAL = timedelta(hours=1) CONF_POST_CODE = "post_code" CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" + +WASTE_TYPE_TO_DESCRIPTION = { + WasteType.NON_RECYCLABLE: "Non-recyclable Waste Pickup", + WasteType.ORGANIC: "Organic Waste Pickup", + WasteType.PACKAGES: "Packages Waste Pickup", + WasteType.PAPER: "Paper Waste Pickup", + WasteType.TREE: "Christmas Tree Pickup", +} diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py new file mode 100644 index 00000000000..d075b1cbf6b --- /dev/null +++ b/homeassistant/components/twentemilieu/entity.py @@ -0,0 +1,36 @@ +"""Base entity for the Twente Milieu integration.""" +from __future__ import annotations + +from datetime import date + +from twentemilieu import WasteType + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +class TwenteMilieuEntity(CoordinatorEntity[dict[WasteType, list[date]]], Entity): + """Defines a Twente Milieu entity.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize the Twente Milieu entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + configuration_url="https://www.twentemilieu.nl", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, + manufacturer="Twente Milieu", + name="Twente Milieu", + ) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index a56523d53d0..ab69aba9abf 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -14,15 +14,11 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, WASTE_TYPE_TO_DESCRIPTION +from .entity import TwenteMilieuEntity @dataclass @@ -43,35 +39,35 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = ( TwenteMilieuSensorDescription( key="tree", waste_type=WasteType.TREE, - name="Christmas Tree Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.TREE], icon="mdi:pine-tree", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Non-recyclable", waste_type=WasteType.NON_RECYCLABLE, - name="Non-recyclable Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.NON_RECYCLABLE], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Organic", waste_type=WasteType.ORGANIC, - name="Organic Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.ORGANIC], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Paper", waste_type=WasteType.PAPER, - name="Paper Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PAPER], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), TwenteMilieuSensorDescription( key="Plastic", waste_type=WasteType.PACKAGES, - name="Packages Waste Pickup", + name=WASTE_TYPE_TO_DESCRIPTION[WasteType.PACKAGES], icon="mdi:delete-empty", device_class=SensorDeviceClass.DATE, ), @@ -90,7 +86,7 @@ async def async_setup_entry( ) -class TwenteMilieuSensor(CoordinatorEntity[dict[WasteType, list[date]]], SensorEntity): +class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity): """Defines a Twente Milieu sensor.""" entity_description: TwenteMilieuSensorDescription @@ -102,16 +98,9 @@ class TwenteMilieuSensor(CoordinatorEntity[dict[WasteType, list[date]]], SensorE entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" - super().__init__(coordinator=coordinator) + super().__init__(coordinator, entry) self.entity_description = description self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}" - self._attr_device_info = DeviceInfo( - configuration_url="https://www.twentemilieu.nl", - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, str(entry.data[CONF_ID]))}, - manufacturer="Twente Milieu", - name="Twente Milieu", - ) @property def native_value(self) -> date | None: diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py new file mode 100644 index 00000000000..27e3ff8ebf3 --- /dev/null +++ b/tests/components/twentemilieu/test_calendar.py @@ -0,0 +1,83 @@ +"""Tests for the Twente Milieu calendar.""" +from http import HTTPStatus + +import pytest + +from homeassistant.components.twentemilieu.const import DOMAIN +from homeassistant.const import ATTR_ICON, STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2022-01-05 00:00:00+00:00") +async def test_waste_pickup_calendar( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the Twente Milieu waste pickup calendar.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("calendar.twente_milieu") + entry = entity_registry.async_get("calendar.twente_milieu") + assert entry + assert state + assert entry.unique_id == "12345" + assert state.attributes[ATTR_ICON] == "mdi:delete-empty" + assert state.attributes["all_day"] is True + assert state.attributes["message"] == "Christmas Tree Pickup" + assert not state.attributes["location"] + assert not state.attributes["description"] + assert state.state == STATE_OFF + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "12345")} + assert device_entry.manufacturer == "Twente Milieu" + assert device_entry.name == "Twente Milieu" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert device_entry.configuration_url == "https://www.twentemilieu.nl" + assert not device_entry.model + assert not device_entry.sw_version + + +async def test_api_calendar( + hass: HomeAssistant, + init_integration: MockConfigEntry, + hass_client, +) -> None: + """Test the API returns the calendar.""" + client = await hass_client() + response = await client.get("/api/calendars") + assert response.status == HTTPStatus.OK + data = await response.json() + assert data == [ + { + "entity_id": "calendar.twente_milieu", + "name": "Twente Milieu", + } + ] + + +async def test_api_events( + hass: HomeAssistant, + init_integration: MockConfigEntry, + hass_client, +) -> None: + """Test the Twente Milieu calendar view.""" + client = await hass_client() + response = await client.get( + "/api/calendars/calendar.twente_milieu?start=2022-01-05&end=2022-01-06" + ) + assert response.status == HTTPStatus.OK + events = await response.json() + assert len(events) == 1 + assert events[0] == { + "all_day": True, + "start": {"date": "2022-01-06"}, + "end": {"date": "2022-01-06"}, + "summary": "Christmas Tree Pickup", + } From 3c10ac308d4fd83ad214b150258a6892132c55f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Mar 2022 10:49:11 -1000 Subject: [PATCH 0564/1054] Fix migration to schema v25 with Postgresql (#68426) --- homeassistant/components/recorder/migration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 096ec380cf6..5db43aa760f 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -376,6 +376,7 @@ def _drop_foreign_key_constraints(instance, engine, table, columns): def _apply_update(instance, new_version, old_version): # noqa: C901 """Perform operations to bring schema up to date.""" engine = instance.engine + dialect = engine.dialect.name if new_version == 1: _create_index(instance, "events", "ix_events_time_fired") elif new_version == 2: @@ -639,7 +640,8 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 "ix_statistics_short_term_statistic_id_start", ) elif new_version == 25: - _add_columns(instance, "states", ["attributes_id INTEGER(20)"]) + big_int = "INTEGER(20)" if dialect == "mysql" else "INTEGER" + _add_columns(instance, "states", [f"attributes_id {big_int}"]) _create_index(instance, "states", "ix_states_attributes_id") else: From 40484a34839e72c307379b3a9bcd376852d632a4 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 20 Mar 2022 17:04:55 -0400 Subject: [PATCH 0565/1054] Tweak hassfest codeowners script (#68382) --- script/hassfest/codeowners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index cf8fb02b989..09498cfca01 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -56,7 +56,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): parts.append(f"homeassistant/components/{domain}/* {' '.join(codeowners)}") - if (config.root / "tests/components" / domain).exists(): + if (config.root / "tests/components" / domain / "__init__.py").exists(): parts.append(f"tests/components/{domain}/* {' '.join(codeowners)}") parts.append(f"\n{INDIVIDUAL_FILES.strip()}") From 0cbc29caca6c4de674cbe616888cb7aa8afef55d Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 20 Mar 2022 21:48:11 +0000 Subject: [PATCH 0566/1054] Add unique_id through YAML to the integration component (#68435) --- homeassistant/components/integration/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 837886bbf56..c0bac1b2745 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_METHOD, CONF_NAME, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, TIME_DAYS, @@ -70,6 +71,7 @@ PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int), vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), @@ -125,7 +127,7 @@ async def async_setup_platform( name=config.get(CONF_NAME), round_digits=config[CONF_ROUND_DIGITS], source_entity=config[CONF_SOURCE_SENSOR], - unique_id=None, + unique_id=config.get(CONF_UNIQUE_ID), unit_of_measurement=config.get(CONF_UNIT_OF_MEASUREMENT), unit_prefix=config[CONF_UNIT_PREFIX], unit_time=config[CONF_UNIT_TIME], From 8bbbd1947d8227e0e68b38e8226c794bbcd8350c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Mar 2022 16:01:58 -0700 Subject: [PATCH 0567/1054] Raise if referenced entity does not support service (#68394) --- .../template/alarm_control_panel.py | 7 +-- homeassistant/helpers/service.py | 8 ++- tests/components/cast/test_media_player.py | 10 +++- tests/components/dynalite/test_cover.py | 8 ++- tests/components/rfxtrx/test_cover.py | 27 +++++---- .../risco/test_alarm_control_panel.py | 59 ++++++++++++------- .../components/samsungtv/test_media_player.py | 8 ++- .../template/test_alarm_control_panel.py | 32 ---------- tests/components/webostv/test_trigger.py | 22 ++++--- tests/helpers/test_service.py | 14 +++++ 10 files changed, 105 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ca037f23bc4..b1f1af4a6e0 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -220,7 +220,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): ) await super().async_added_to_hass() - async def _async_alarm_arm(self, state, script=None, code=None): + async def _async_alarm_arm(self, state, script, code): """Arm the panel to specified state with supplied script.""" optimistic_set = False @@ -228,10 +228,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._state = state optimistic_set = True - if script is not None: - await script.async_run({ATTR_CODE: code}, context=self._context) - else: - _LOGGER.error("No script action defined for %s", state) + await script.async_run({ATTR_CODE: code}, context=self._context) if optimistic_set: self.async_write_ha_state() diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index da562fcded6..966666b27fc 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -527,7 +527,7 @@ def async_set_service_schema( @bind_hass -async def entity_service_call( +async def entity_service_call( # noqa: C901 hass: HomeAssistant, platforms: Iterable[EntityPlatform], func: str | Callable[..., Any], @@ -646,6 +646,12 @@ async def entity_service_call( for feature_set in required_features ) ): + # If entity explicitly referenced, raise an error + if referenced is not None and entity.entity_id in referenced.referenced: + raise HomeAssistantError( + f"Entity {entity.entity_id} does not support this service." + ) + continue entities.append(entity) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 4cf96f1a965..350bd00a013 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -39,6 +39,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er, network from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -1225,15 +1226,18 @@ async def test_entity_control(hass: HomeAssistant): chromecast.media_controller.pause.assert_called_once_with() # Media previous - await common.async_media_previous_track(hass, entity_id) + with pytest.raises(HomeAssistantError): + await common.async_media_previous_track(hass, entity_id) chromecast.media_controller.queue_prev.assert_not_called() # Media next - await common.async_media_next_track(hass, entity_id) + with pytest.raises(HomeAssistantError): + await common.async_media_next_track(hass, entity_id) chromecast.media_controller.queue_next.assert_not_called() # Media seek - await common.async_media_seek(hass, 123, entity_id) + with pytest.raises(HomeAssistantError): + await common.async_media_seek(hass, 123, entity_id) chromecast.media_controller.seek.assert_not_called() # Enable support for queue and seek diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index 4f696d905d3..fd671365ba1 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -3,6 +3,7 @@ from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice import pytest from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME +from homeassistant.exceptions import HomeAssistantError from .common import ( ATTR_ARGS, @@ -65,9 +66,10 @@ async def test_cover_without_tilt(hass, mock_device): """Test a cover with no tilt.""" mock_device.has_tilt = False await create_entity_from_device(hass, mock_device) - await hass.services.async_call( - "cover", "open_cover_tilt", {"entity_id": "cover.name"}, blocking=True - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": "cover.name"}, blocking=True + ) await hass.async_block_till_done() mock_device.async_open_cover_tilt.assert_not_called() diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index a05a456d221..e3d44edda82 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State +from homeassistant.exceptions import HomeAssistantError from tests.common import MockConfigEntry, mock_restore_cache from tests.components.rfxtrx.conftest import create_rfx_test_cfg @@ -181,19 +182,21 @@ async def test_rfy_cover(hass, rfxtrx): blocking=True, ) - await hass.services.async_call( - "cover", - "open_cover_tilt", - {"entity_id": "cover.rfy_010203_1"}, - blocking=True, - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) - await hass.services.async_call( - "cover", - "close_cover_tilt", - {"entity_id": "cover.rfy_010203_1"}, - blocking=True, - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) assert rfxtrx.transport.send.mock_calls == [ call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 23c4de4c7aa..70ec7844624 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -27,6 +27,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNKNOWN, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity @@ -337,17 +338,24 @@ async def test_sets_with_correct_code(hass, two_part_alarm): await _test_service_call( hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C", **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0, **code - ) - await _test_no_service_call( - hass, - SERVICE_ALARM_ARM_CUSTOM_BYPASS, - "partial_arm", - SECOND_ENTITY_ID, - 1, - **code, - ) + with pytest.raises(HomeAssistantError): + await _test_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + FIRST_ENTITY_ID, + 0, + **code, + ) + with pytest.raises(HomeAssistantError): + await _test_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + SECOND_ENTITY_ID, + 1, + **code, + ) async def test_sets_with_incorrect_code(hass, two_part_alarm): @@ -379,14 +387,21 @@ async def test_sets_with_incorrect_code(hass, two_part_alarm): await _test_no_service_call( hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, **code ) - await _test_no_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0, **code - ) - await _test_no_service_call( - hass, - SERVICE_ALARM_ARM_CUSTOM_BYPASS, - "partial_arm", - SECOND_ENTITY_ID, - 1, - **code, - ) + with pytest.raises(HomeAssistantError): + await _test_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + FIRST_ENTITY_ID, + 0, + **code, + ) + with pytest.raises(HomeAssistantError): + await _test_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + SECOND_ENTITY_ID, + 1, + **code, + ) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c76b9e9efb9..74849fb9fee 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -60,6 +60,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -896,9 +897,10 @@ async def test_turn_on_wol(hass: HomeAssistant) -> None: async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None: """Test turn on.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) - assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True - ) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True + ) # nothing called as not supported feature assert remote.control.call_count == 0 diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index a7502576de1..cd29794db8d 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -129,38 +129,6 @@ async def test_optimistic_states(hass, start_ha): assert hass.states.get(TEMPLATE_NAME).state == set_state -@pytest.mark.parametrize("count,domain", [(1, "alarm_control_panel")]) -@pytest.mark.parametrize( - "config", - [ - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{{ states('alarm_control_panel.test') }}", - } - }, - } - }, - ], -) -async def test_no_action_scripts(hass, start_ha): - """Test no action scripts per state.""" - hass.states.async_set("alarm_control_panel.test", STATE_ALARM_ARMED_AWAY) - await hass.async_block_till_done() - - for func, set_state in [ - (common.async_alarm_arm_away, STATE_ALARM_ARMED_AWAY), - (common.async_alarm_arm_home, STATE_ALARM_ARMED_AWAY), - (common.async_alarm_arm_night, STATE_ALARM_ARMED_AWAY), - (common.async_alarm_disarm, STATE_ALARM_ARMED_AWAY), - ]: - await func(hass, entity_id=TEMPLATE_NAME) - await hass.async_block_till_done() - assert hass.states.get(TEMPLATE_NAME).state == set_state - - @pytest.mark.parametrize("count,domain", [(0, "alarm_control_panel")]) @pytest.mark.parametrize( "config,msg", diff --git a/tests/components/webostv/test_trigger.py b/tests/components/webostv/test_trigger.py index cbc72638ad9..e11f2db3bad 100644 --- a/tests/components/webostv/test_trigger.py +++ b/tests/components/webostv/test_trigger.py @@ -1,9 +1,12 @@ """The tests for WebOS TV automation triggers.""" from unittest.mock import patch +import pytest + from homeassistant.components import automation from homeassistant.components.webostv import DOMAIN from homeassistant.const import SERVICE_RELOAD +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import async_get as get_dev_reg from homeassistant.setup import async_setup_component @@ -57,17 +60,18 @@ async def test_webostv_turn_on_trigger_device_id(hass, calls, client): with patch("homeassistant.config.load_yaml", return_value={}): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) - await hass.services.async_call( - "media_player", - "turn_on", - {"entity_id": ENTITY_ID}, - blocking=True, - ) + calls.clear() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "media_player", + "turn_on", + {"entity_id": ENTITY_ID}, + blocking=True, + ) await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == device.id - assert calls[0].data["id"] == 0 + assert len(calls) == 0 async def test_webostv_turn_on_trigger_entity_id(hass, calls, client): diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index bbf4a72430c..ce4a32bb2a4 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -552,6 +552,20 @@ async def test_call_with_required_features(hass, mock_entities): actual = [call[0][0] for call in test_service_mock.call_args_list] assert all(entity in actual for entity in expected) + # Test we raise if we target entity ID that does not support the service + test_service_mock.reset_mock() + with pytest.raises(exceptions.HomeAssistantError): + await service.entity_service_call( + hass, + [Mock(entities=mock_entities)], + test_service_mock, + ha.ServiceCall( + "test_domain", "test_service", {"entity_id": "light.living_room"} + ), + required_features=[SUPPORT_A], + ) + assert test_service_mock.call_count == 0 + async def test_call_with_both_required_features(hass, mock_entities): """Test service calls invoked only if entity has both features.""" From ed94cc36735bdd94e7466204dcca6e365de1d467 Mon Sep 17 00:00:00 2001 From: Jeef Date: Sun, 20 Mar 2022 17:51:54 -0600 Subject: [PATCH 0568/1054] Intellifire DHCP Auto Discovery (#67053) Co-authored-by: J. Nick Koston --- .../components/intellifire/config_flow.py | 63 +++++++++++++++++-- .../components/intellifire/manifest.json | 4 +- .../components/intellifire/strings.json | 7 ++- .../intellifire/translations/en.json | 43 ++++++------- homeassistant/generated/dhcp.py | 1 + .../intellifire/test_config_flow.py | 51 +++++++++++++++ 6 files changed, 142 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 869504d22a2..a8d9fa135d2 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -1,6 +1,7 @@ """Config flow for IntelliFire integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from aiohttp import ClientConnectionError @@ -8,6 +9,7 @@ from intellifire4py import AsyncUDPFireplaceFinder, IntellifireAsync import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult @@ -18,6 +20,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) MANUAL_ENTRY_STRING = "IP Address" # Simplified so it does not have to be translated +@dataclass +class DiscoveredHostInfo: + """Host info for discovery.""" + + ip: str + serial: str | None + + async def validate_host_input(host: str) -> str: """Validate the user input allows us to connect. @@ -39,7 +49,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Config Flow Handler.""" self._config_context = {} - self._not_configured_hosts: list[str] = [] + self._not_configured_hosts: list[DiscoveredHostInfo] = [] + self._discovered_host: DiscoveredHostInfo async def _find_fireplaces(self): """Perform UDP discovery.""" @@ -52,7 +63,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } self._not_configured_hosts = [ - ip for ip in discovered_hosts if ip not in configured_hosts + DiscoveredHostInfo(ip, None) + for ip in discovered_hosts + if ip not in configured_hosts ] LOGGER.debug("Discovered Hosts: %s", discovered_hosts) LOGGER.debug("Configured Hosts: %s", configured_hosts) @@ -62,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Validate and create the entry.""" self._async_abort_entries_match({CONF_HOST: host}) serial = await validate_host_input(host) - await self.async_set_unique_id(serial) + await self.async_set_unique_id(serial, raise_on_progress=False) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) return self.async_create_entry( title=f"Fireplace {serial}", @@ -108,7 +121,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema( { vol.Required(CONF_HOST): vol.In( - self._not_configured_hosts + [MANUAL_ENTRY_STRING] + [host.ip for host in self._not_configured_hosts] + + [MANUAL_ENTRY_STRING] ) } ), @@ -127,3 +141,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_pick_device() LOGGER.debug("Running Step: manual_device_entry") return await self.async_step_manual_device_entry() + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle DHCP Discovery.""" + + # Run validation logic on ip + host = discovery_info.ip + + self._async_abort_entries_match({CONF_HOST: host}) + try: + serial = await validate_host_input(host) + except (ConnectionError, ClientConnectionError): + return self.async_abort(reason="not_intellifire_device") + + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._discovered_host = DiscoveredHostInfo(ip=host, serial=serial) + + placeholders = {CONF_HOST: host, "serial": serial} + self.context["title_placeholders"] = placeholders + self._set_confirm_only() + + return await self.async_step_dhcp_confirm() + + async def async_step_dhcp_confirm(self, user_input=None): + """Attempt to confirm.""" + + # Add the hosts one by one + host = self._discovered_host.ip + serial = self._discovered_host.serial + + if user_input is None: + # Show the confirmation dialog + return self.async_show_form( + step_id="dhcp_confirm", + description_placeholders={CONF_HOST: host, "serial": serial}, + ) + + return self.async_create_entry( + title=f"Fireplace {serial}", + data={CONF_HOST: host}, + ) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 75d4ee2e75f..5809748787c 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -6,5 +6,7 @@ "requirements": ["intellifire4py==1.0.1"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", - "loggers": ["intellifire4py"] + "loggers": ["intellifire4py"], + "dhcp": [{"hostname": "zentrios-*"}] + } diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index d5d3f344c8e..a85b807c4c0 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{serial} ({host})", "step": { "manual_device_entry": { "description": "Local Configuration", @@ -7,6 +8,9 @@ "host": "[%key:common::config_flow::data::host%]" } }, + "dhcp_confirm": { + "description": "Do you want to setup {host}\nSerial: {serial}?" + }, "pick_device": { "data": { "host": "[%key:common::config_flow::data::host%]" @@ -17,7 +21,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_intellifire_device":"Not an IntelliFire Device." } } } diff --git a/homeassistant/components/intellifire/translations/en.json b/homeassistant/components/intellifire/translations/en.json index 0f7538e7413..844d77427ca 100644 --- a/homeassistant/components/intellifire/translations/en.json +++ b/homeassistant/components/intellifire/translations/en.json @@ -1,27 +1,28 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again" - }, - "step": { - "manual_device_entry": { - "title": "IntelliFire - Local Config", - "description": "Enter the IP address of the IntelliFire unit on your local network.", - "data": { - "host": "Host (IP Address)" - } + "abort": { + "already_configured": "Device is already configured", + "not_intellifire_device": "Not an IntelliFire Device." }, - "user": { - "description": "Username and password are the same information used in your IntelliFire Android/iOS application.", - "title": "IntelliFire Config" + "error": { + "cannot_connect": "Failed to connect" }, - "pick_device": { - "title": "Device Selection", - "description": "The following IntelliFire devices were discovered. Please select which you wish to configure." + "flow_title": "{serial} ({host})", + "step": { + "dhcp_confirm": { + "description": "Do you want to setup {host}\nSerial: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host" + }, + "description": "Local Configuration" + }, + "pick_device": { + "data": { + "host": "Host" + } + } } - } } - } \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 8633534e976..4b4c2d7b979 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -49,6 +49,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'hunterdouglas_powerview', 'hostname': 'hunter*', 'macaddress': '002674*'}, + {'domain': 'intellifire', 'hostname': 'zentrios-*'}, {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 2e130bfa14e..1283c9db0b2 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import DOMAIN from homeassistant.const import CONF_HOST @@ -203,3 +204,53 @@ async def test_picker_already_discovered( assert result2["title"] == "Fireplace 12345" assert result2["data"] == {CONF_HOST: "192.168.1.4"} assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_dhcp_discovery_intellifire_device( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_intellifire_config_flow: MagicMock, +) -> None: + """Test successful DHCP Discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="zentrios-Test", + ), + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "dhcp_confirm" + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "dhcp_confirm" + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={} + ) + assert result3["title"] == "Fireplace 12345" + assert result3["data"] == {"host": "1.1.1.1"} + + +async def test_dhcp_discovery_non_intellifire_device( + hass: HomeAssistant, + mock_intellifire_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test failed DHCP Discovery.""" + + mock_intellifire_config_flow.poll.side_effect = ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname="zentrios-Evil", + ), + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_intellifire_device" From 929df2bc29b377b746079afcbb9b5d94b4a51f11 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Mar 2022 20:25:15 -0700 Subject: [PATCH 0569/1054] Improve error handling process_play_media_url (#68322) --- .../components/media_player/browse_media.py | 26 +++++++++++++++---- homeassistant/components/roku/media_player.py | 6 ++--- homeassistant/helpers/network.py | 8 +++++- homeassistant/util/network.py | 2 +- tests/components/cast/test_media_player.py | 14 +++++----- .../forked_daapd/test_media_player.py | 10 +++---- .../media_player/test_browse_media.py | 10 +++++++ tests/helpers/test_network.py | 3 +++ tests/util/test_network.py | 1 + 9 files changed, 59 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index d85dcc0a3e3..e4cad5c3201 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -10,7 +10,9 @@ import yarl from homeassistant.components.http.auth import async_sign_path from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.network import ( + NoURLAvailableError, get_supervisor_network_url, get_url, is_hass_url, @@ -28,11 +30,15 @@ def async_process_play_media_url( for_supervisor_network: bool = False, ) -> str: """Update a media URL with authentication if it points at Home Assistant.""" - if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): - return media_content_id - parsed = yarl.URL(media_content_id) + if parsed.is_absolute(): + if not is_hass_url(hass, media_content_id): + return media_content_id + else: + if media_content_id[0] != "/": + raise ValueError("URL is relative, but does not start with a /") + if parsed.query: logging.getLogger(__name__).debug( "Not signing path for content with query param" @@ -46,13 +52,23 @@ def async_process_play_media_url( media_content_id = str(parsed.join(yarl.URL(signed_path))) # convert relative URL to absolute URL - if media_content_id[0] == "/" and not allow_relative_url: + if not parsed.is_absolute() and not allow_relative_url: base_url = None if for_supervisor_network: base_url = get_supervisor_network_url(hass) if not base_url: - base_url = get_url(hass) + try: + base_url = get_url(hass) + except NoURLAvailableError as err: + msg = "Unable to determine Home Assistant URL to send to device" + if ( + hass.config.api + and hass.config.api.use_ssl + and (not hass.config.external_url or not hass.config.internal_url) + ): + msg += ". Configure internal and external URL in general settings." + raise HomeAssistantError(msg) from err media_content_id = f"{base_url}{media_content_id}" diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 8dd76f0b9cb..f7e88ad1ed1 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -402,9 +402,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): stream_name = original_media_id stream_format = guess_stream_format(media_id, mime_type) - # If media ID is a relative URL, we serve it from HA. - media_id = async_process_play_media_url(self.hass, media_id) - if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: media_type = MEDIA_TYPE_VIDEO mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] @@ -412,6 +409,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): stream_format = "hls" if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + # If media ID is a relative URL, we serve it from HA. + media_id = async_process_play_media_url(self.hass, media_id) + parsed = yarl.URL(media_id) if mime_type is None: diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 76c51fa29d2..5fa10fd6fe8 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -62,7 +62,13 @@ def get_supervisor_network_url( def is_hass_url(hass: HomeAssistant, url: str) -> bool: """Return if the URL points at this Home Assistant instance.""" - parsed = yarl.URL(normalize_url(url)) + parsed = yarl.URL(url) + + if not parsed.is_absolute(): + return False + + if parsed.is_default_port(): + parsed = parsed.with_port(None) def host_ip() -> str | None: if hass.config.api is None or is_loopback(ip_address(hass.config.api.local_ip)): diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index 396ab56b8c2..87077a0eb0a 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -82,6 +82,6 @@ def is_ipv6_address(address: str) -> bool: def normalize_url(address: str) -> str: """Normalize a given URL.""" url = yarl.URL(address.rstrip("/")) - if url.is_default_port(): + if url.is_absolute() and url.is_default_port(): return str(url.with_port(None)) return str(url) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 350bd00a013..2e6fafb0287 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -974,7 +974,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): { ATTR_ENTITY_ID: entity_id, media_player.ATTR_MEDIA_CONTENT_TYPE: "audio", - media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3", + media_player.ATTR_MEDIA_CONTENT_ID: "http://example.com/best.mp3", media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, }, blocking=True, @@ -985,7 +985,7 @@ async def test_entity_play_media(hass: HomeAssistant, quick_play_mock): chromecast, "default_media_receiver", { - "media_id": "best.mp3", + "media_id": "http://example.com/best.mp3", "media_type": "audio", "metadata": {"metadatatype": 3}, }, @@ -1523,13 +1523,15 @@ async def test_group_media_control(hass, mz_mock, quick_play_mock): assert not chromecast.media_controller.stop.called # Verify play_media is not forwarded - await common.async_play_media(hass, "music", "best.mp3", entity_id) + await common.async_play_media( + hass, "music", "http://example.com/best.mp3", entity_id + ) assert not grp_media.play_media.called assert not chromecast.media_controller.play_media.called quick_play_mock.assert_called_once_with( chromecast, "default_media_receiver", - {"media_id": "best.mp3", "media_type": "music"}, + {"media_id": "http://example.com/best.mp3", "media_type": "music"}, ) @@ -1803,7 +1805,7 @@ async def test_cast_platform_play_media(hass: HomeAssistant, quick_play_mock, ca { ATTR_ENTITY_ID: entity_id, media_player.ATTR_MEDIA_CONTENT_TYPE: "audio", - media_player.ATTR_MEDIA_CONTENT_ID: "best.mp3", + media_player.ATTR_MEDIA_CONTENT_ID: "http://example.com/best.mp3", media_player.ATTR_MEDIA_EXTRA: {"metadata": {"metadatatype": 3}}, }, blocking=True, @@ -1811,7 +1813,7 @@ async def test_cast_platform_play_media(hass: HomeAssistant, quick_play_mock, ca # Assert the media player attempt to play media through the cast platform cast_platform_mock.async_play_media.assert_called_once_with( - hass, entity_id, chromecast, "audio", "best.mp3" + hass, entity_id, chromecast, "audio", "http://example.com/best.mp3" ) # Assert pychromecast is used to play media diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index a2e0050c3d9..c18b8df3f1c 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -557,7 +557,7 @@ async def test_async_play_media_from_paused(hass, mock_api_object): SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -581,7 +581,7 @@ async def test_async_play_media_from_stopped( SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -616,7 +616,7 @@ async def test_async_play_media_tts_timeout(hass, mock_api_object): SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -725,7 +725,7 @@ async def test_librespot_java_play_media(hass, pipe_control_api_object): SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) @@ -747,7 +747,7 @@ async def test_librespot_java_play_media_pause_timeout(hass, pipe_control_api_ob SERVICE_PLAY_MEDIA, { ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "somefile.mp3", + ATTR_MEDIA_CONTENT_ID: "http://example.com/somefile.mp3", }, ) state = hass.states.get(TEST_MASTER_ENTITY_NAME) diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index 5e4bac2c635..6741432024e 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -7,6 +7,8 @@ from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) from homeassistant.config import async_process_ha_core_config +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.network import NoURLAvailableError from tests.common import mock_component @@ -48,6 +50,11 @@ async def test_process_play_media_url(hass, mock_sign_path): async_process_play_media_url(hass, "http://192.168.123.123:8123/path") == "http://192.168.123.123:8123/path?authSig=bla" ) + with pytest.raises(HomeAssistantError), patch( + "homeassistant.components.media_player.browse_media.get_url", + side_effect=NoURLAvailableError, + ): + async_process_play_media_url(hass, "/path") # Test skip signing URLs that have a query param assert ( @@ -61,6 +68,9 @@ async def test_process_play_media_url(hass, mock_sign_path): == "http://192.168.123.123:8123/path?hello=world" ) + with pytest.raises(ValueError): + async_process_play_media_url(hass, "hello") + async def test_process_play_media_url_for_addon(hass, mock_sign_path): """Test it uses the hostname for an addon if available.""" diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 0c9e8361104..13f1a3cdd78 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -681,6 +681,9 @@ async def test_is_hass_url(hass): assert hass.config.external_url is None assert is_hass_url(hass, "http://example.com") is False + assert is_hass_url(hass, "bad_url") is False + assert is_hass_url(hass, "bad_url.com") is False + assert is_hass_url(hass, "http:/bad_url.com") is False hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") assert is_hass_url(hass, "http://192.168.123.123:8123") is True diff --git a/tests/util/test_network.py b/tests/util/test_network.py index b5c6b1a3e24..4f372e5e1a7 100644 --- a/tests/util/test_network.py +++ b/tests/util/test_network.py @@ -91,3 +91,4 @@ def test_normalize_url(): network_util.normalize_url("https://example.com:443/test/") == "https://example.com/test" ) + assert network_util.normalize_url("/test/") == "/test" From 4f9df1fd0fd070e84db847207da87046c30b83b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 20 Mar 2022 17:34:02 -1000 Subject: [PATCH 0570/1054] Fix logbook tests (#68443) --- tests/components/logbook/test_init.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 29dea015921..998efc1c167 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -60,6 +60,12 @@ async def hass_(hass): return hass +@pytest.fixture() +def set_utc(hass): + """Set timezone to UTC.""" + hass.config.set_time_zone("UTC") + + async def test_service_call_create_logbook_entry(hass_): """Test if service call create log book entry.""" calls = async_capture_events(hass_, logbook.EVENT_LOGBOOK_ENTRY) @@ -314,7 +320,7 @@ async def test_logbook_view(hass, hass_client): assert response.status == HTTPStatus.OK -async def test_logbook_view_period_entity(hass, hass_client): +async def test_logbook_view_period_entity(hass, hass_client, set_utc): """Test the logbook view with period and entity.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -680,7 +686,7 @@ async def test_logbook_entity_no_longer_in_state_machine(hass, hass_client): assert json_dict[0]["name"] == "Alarm Control Panel" -async def test_filter_continuous_sensor_values(hass, hass_client): +async def test_filter_continuous_sensor_values(hass, hass_client, set_utc): """Test remove continuous sensor events from logbook.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -716,7 +722,7 @@ async def test_filter_continuous_sensor_values(hass, hass_client): assert response_json[1]["entity_id"] == entity_id_third -async def test_exclude_new_entities(hass, hass_client): +async def test_exclude_new_entities(hass, hass_client, set_utc): """Test if events are excluded on first update.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -751,7 +757,7 @@ async def test_exclude_new_entities(hass, hass_client): assert response_json[1]["message"] == "started" -async def test_exclude_removed_entities(hass, hass_client): +async def test_exclude_removed_entities(hass, hass_client, set_utc): """Test if events are excluded on last update.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) @@ -793,7 +799,7 @@ async def test_exclude_removed_entities(hass, hass_client): assert response_json[2]["entity_id"] == entity_id2 -async def test_exclude_attribute_changes(hass, hass_client): +async def test_exclude_attribute_changes(hass, hass_client, set_utc): """Test if events of attribute changes are filtered.""" await async_init_recorder_component(hass) await async_setup_component(hass, "logbook", {}) From 3213091b8df5fa649264cd653ad38aa29ae9cce0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Mar 2022 20:38:13 -0700 Subject: [PATCH 0571/1054] Add integration type (#68349) --- .../components/config/config_entries.py | 41 +- .../components/derivative/manifest.json | 1 + homeassistant/generated/config_flows.py | 786 +++++++++--------- homeassistant/loader.py | 25 +- script/hassfest/config_flow.py | 7 +- script/hassfest/manifest.py | 1 + script/hassfest/model.py | 5 + .../components/config/test_config_entries.py | 235 ++++-- tests/helpers/test_translation.py | 6 +- 9 files changed, 608 insertions(+), 499 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 07bdc794128..f3a239b5822 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,13 +1,14 @@ """Http views to control the config manager.""" from __future__ import annotations +import asyncio from http import HTTPStatus from aiohttp import web import aiohttp.web_exceptions import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries, data_entry_flow, loader from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView @@ -48,11 +49,36 @@ class ConfigManagerEntryIndexView(HomeAssistantView): async def get(self, request): """List available config entries.""" - hass = request.app["hass"] + hass: HomeAssistant = request.app["hass"] - return self.json( - [entry_json(entry) for entry in hass.config_entries.async_entries()] - ) + kwargs = {} + if "domain" in request.query: + kwargs["domain"] = request.query["domain"] + + entries = hass.config_entries.async_entries(**kwargs) + + if "type" not in request.query: + return self.json([entry_json(entry) for entry in entries]) + + integrations = {} + type_filter = request.query["type"] + + # Fetch all the integrations so we can check their type + for integration in await asyncio.gather( + *( + loader.async_get_integration(hass, domain) + for domain in {entry.domain for entry in entries} + ) + ): + integrations[integration.domain] = integration + + entries = [ + entry + for entry in entries + if integrations[entry.domain].integration_type == type_filter + ] + + return self.json([entry_json(entry) for entry in entries]) class ConfigManagerEntryResourceView(HomeAssistantView): @@ -179,7 +205,10 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" hass = request.app["hass"] - return self.json(await async_get_config_flows(hass)) + kwargs = {} + if "type" in request.query: + kwargs["type_filter"] = request.query["type"] + return self.json(await async_get_config_flows(hass, **kwargs)) class OptionManagerFlowIndexView(FlowManagerIndexView): diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json index bed23d33e15..665c4cb0192 100644 --- a/homeassistant/components/derivative/manifest.json +++ b/homeassistant/components/derivative/manifest.json @@ -1,5 +1,6 @@ { "domain": "derivative", + "integration_type": "helper", "name": "Derivative", "documentation": "https://www.home-assistant.io/integrations/derivative", "codeowners": [ diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9fd116aa715..6a668b0d666 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -5,394 +5,398 @@ To update, run python3 -m script.hassfest # fmt: off -FLOWS = [ - "abode", - "accuweather", - "acmeda", - "adax", - "adguard", - "advantage_air", - "aemet", - "agent_dvr", - "airly", - "airnow", - "airthings", - "airtouch4", - "airvisual", - "airzone", - "alarmdecoder", - "almond", - "ambee", - "amberelectric", - "ambiclimate", - "ambient_station", - "androidtv", - "apple_tv", - "arcam_fmj", - "aseko_pool_live", - "asuswrt", - "atag", - "august", - "aurora", - "aurora_abb_powerone", - "aussie_broadband", - "awair", - "axis", - "azure_devops", - "azure_event_hub", - "balboa", - "blebox", - "blink", - "bmw_connected_drive", - "bond", - "bosch_shc", - "braviatv", - "broadlink", - "brother", - "brunt", - "bsblan", - "buienradar", - "canary", - "cast", - "cert_expiry", - "cloudflare", - "co2signal", - "coinbase", - "control4", - "coolmaster", - "coronavirus", - "cpuspeed", - "crownstone", - "daikin", - "deconz", - "denonavr", - "derivative", - "devolo_home_control", - "devolo_home_network", - "dexcom", - "dialogflow", - "directv", - "dlna_dmr", - "dlna_dms", - "dnsip", - "doorbird", - "dsmr", - "dunehd", - "dynalite", - "eafm", - "ecobee", - "econet", - "efergy", - "elgato", - "elkm1", - "elmax", - "emonitor", - "emulated_roku", - "enocean", - "enphase_envoy", - "environment_canada", - "epson", - "esphome", - "evil_genius_labs", - "ezviz", - "faa_delays", - "fireservicerota", - "fivem", - "fjaraskupan", - "flick_electric", - "flipr", - "flo", - "flume", - "flunearyou", - "flux_led", - "forecast_solar", - "forked_daapd", - "foscam", - "freebox", - "freedompro", - "fritz", - "fritzbox", - "fritzbox_callmonitor", - "fronius", - "garages_amsterdam", - "gdacs", - "geofency", - "geonetnz_quakes", - "geonetnz_volcano", - "gios", - "github", - "glances", - "goalzero", - "gogogate2", - "goodwe", - "google", - "google_travel_time", - "gpslogger", - "gree", - "group", - "growatt_server", - "guardian", - "habitica", - "hangouts", - "harmony", - "heos", - "hisense_aehw4a1", - "hive", - "hlk_sw16", - "home_connect", - "home_plus_control", - "homekit", - "homekit_controller", - "homematicip_cloud", - "homewizard", - "honeywell", - "huawei_lte", - "hue", - "huisbaasje", - "hunterdouglas_powerview", - "hvv_departures", - "hyperion", - "ialarm", - "iaqualink", - "icloud", - "ifttt", - "insteon", - "integration", - "intellifire", - "ios", - "iotawatt", - "ipma", - "ipp", - "iqvia", - "islamic_prayer_times", - "iss", - "isy994", - "izone", - "jellyfin", - "juicenet", - "kaleidescape", - "keenetic_ndms2", - "kmtronic", - "knx", - "kodi", - "konnected", - "kostal_plenticore", - "kraken", - "kulersky", - "launch_library", - "life360", - "lifx", - "litejet", - "litterrobot", - "local_ip", - "locative", - "logi_circle", - "lookin", - "luftdaten", - "lutron_caseta", - "lyric", - "mailgun", - "mazda", - "melcloud", - "met", - "met_eireann", - "meteo_france", - "meteoclimatic", - "metoffice", - "mikrotik", - "mill", - "minecraft_server", - "mjpeg", - "mobile_app", - "modem_callerid", - "modern_forms", - "moehlenhoff_alpha2", - "monoprice", - "moon", - "motion_blinds", - "motioneye", - "mqtt", - "mullvad", - "mutesync", - "myq", - "mysensors", - "nam", - "nanoleaf", - "neato", - "nest", - "netatmo", - "netgear", - "nexia", - "nfandroidtv", - "nightscout", - "nina", - "nmap_tracker", - "notion", - "nuheat", - "nuki", - "nut", - "nws", - "nzbget", - "octoprint", - "omnilogic", - "oncue", - "ondilo_ico", - "onewire", - "onvif", - "open_meteo", - "opengarage", - "opentherm_gw", - "openuv", - "openweathermap", - "overkiz", - "ovo_energy", - "owntracks", - "p1_monitor", - "panasonic_viera", - "philips_js", - "pi_hole", - "picnic", - "plaato", - "plex", - "plugwise", - "plum_lightpad", - "point", - "poolsense", - "powerwall", - "profiler", - "progettihwsw", - "prosegur", - "ps4", - "pure_energie", - "pvoutput", - "pvpc_hourly_pricing", - "rachio", - "radio_browser", - "rainforest_eagle", - "rainmachine", - "rdw", - "recollect_waste", - "renault", - "rfxtrx", - "ridwell", - "ring", - "risco", - "rituals_perfume_genie", - "roku", - "roomba", - "roon", - "rpi_power", - "rtsp_to_webrtc", - "ruckus_unleashed", - "samsungtv", - "screenlogic", - "season", - "sense", - "senseme", - "sensibo", - "sentry", - "sharkiq", - "shelly", - "shopping_list", - "sia", - "simplisafe", - "sleepiq", - "sma", - "smappee", - "smart_meter_texas", - "smartthings", - "smarttub", - "smhi", - "sms", - "solaredge", - "solarlog", - "solax", - "soma", - "somfy", - "somfy_mylink", - "sonarr", - "songpal", - "sonos", - "speedtestdotnet", - "spider", - "spotify", - "squeezebox", - "srp_energy", - "starline", - "steamist", - "stookalert", - "subaru", - "sun", - "surepetcare", - "switch_as_x", - "switchbot", - "switcher_kis", - "syncthing", - "syncthru", - "synology_dsm", - "system_bridge", - "tado", - "tailscale", - "tasmota", - "tellduslive", - "tesla_wall_connector", - "tibber", - "tile", - "tolo", - "tomorrowio", - "toon", - "totalconnect", - "tplink", - "traccar", - "tractive", - "tradfri", - "trafikverket_weatherstation", - "transmission", - "tuya", - "twentemilieu", - "twilio", - "twinkly", - "unifi", - "unifiprotect", - "upb", - "upcloud", - "upnp", - "uptime", - "uptimerobot", - "vallox", - "velbus", - "venstar", - "vera", - "verisure", - "version", - "vesync", - "vicare", - "vilfo", - "vizio", - "vlc_telnet", - "volumio", - "wallbox", - "watttime", - "waze_travel_time", - "webostv", - "wemo", - "whirlpool", - "whois", - "wiffi", - "wilight", - "withings", - "wiz", - "wled", - "wolflink", - "xbox", - "xiaomi_aqara", - "xiaomi_miio", - "yale_smart_alarm", - "yamaha_musiccast", - "yeelight", - "youless", - "zerproc", - "zha", - "zwave_js", - "zwave_me" -] +FLOWS = { + "integration": [ + "abode", + "accuweather", + "acmeda", + "adax", + "adguard", + "advantage_air", + "aemet", + "agent_dvr", + "airly", + "airnow", + "airthings", + "airtouch4", + "airvisual", + "airzone", + "alarmdecoder", + "almond", + "ambee", + "amberelectric", + "ambiclimate", + "ambient_station", + "androidtv", + "apple_tv", + "arcam_fmj", + "aseko_pool_live", + "asuswrt", + "atag", + "august", + "aurora", + "aurora_abb_powerone", + "aussie_broadband", + "awair", + "axis", + "azure_devops", + "azure_event_hub", + "balboa", + "blebox", + "blink", + "bmw_connected_drive", + "bond", + "bosch_shc", + "braviatv", + "broadlink", + "brother", + "brunt", + "bsblan", + "buienradar", + "canary", + "cast", + "cert_expiry", + "cloudflare", + "co2signal", + "coinbase", + "control4", + "coolmaster", + "coronavirus", + "cpuspeed", + "crownstone", + "daikin", + "deconz", + "denonavr", + "devolo_home_control", + "devolo_home_network", + "dexcom", + "dialogflow", + "directv", + "dlna_dmr", + "dlna_dms", + "dnsip", + "doorbird", + "dsmr", + "dunehd", + "dynalite", + "eafm", + "ecobee", + "econet", + "efergy", + "elgato", + "elkm1", + "elmax", + "emonitor", + "emulated_roku", + "enocean", + "enphase_envoy", + "environment_canada", + "epson", + "esphome", + "evil_genius_labs", + "ezviz", + "faa_delays", + "fireservicerota", + "fivem", + "fjaraskupan", + "flick_electric", + "flipr", + "flo", + "flume", + "flunearyou", + "flux_led", + "forecast_solar", + "forked_daapd", + "foscam", + "freebox", + "freedompro", + "fritz", + "fritzbox", + "fritzbox_callmonitor", + "fronius", + "garages_amsterdam", + "gdacs", + "geofency", + "geonetnz_quakes", + "geonetnz_volcano", + "gios", + "github", + "glances", + "goalzero", + "gogogate2", + "goodwe", + "google", + "google_travel_time", + "gpslogger", + "gree", + "group", + "growatt_server", + "guardian", + "habitica", + "hangouts", + "harmony", + "heos", + "hisense_aehw4a1", + "hive", + "hlk_sw16", + "home_connect", + "home_plus_control", + "homekit", + "homekit_controller", + "homematicip_cloud", + "homewizard", + "honeywell", + "huawei_lte", + "hue", + "huisbaasje", + "hunterdouglas_powerview", + "hvv_departures", + "hyperion", + "ialarm", + "iaqualink", + "icloud", + "ifttt", + "insteon", + "integration", + "intellifire", + "ios", + "iotawatt", + "ipma", + "ipp", + "iqvia", + "islamic_prayer_times", + "iss", + "isy994", + "izone", + "jellyfin", + "juicenet", + "kaleidescape", + "keenetic_ndms2", + "kmtronic", + "knx", + "kodi", + "konnected", + "kostal_plenticore", + "kraken", + "kulersky", + "launch_library", + "life360", + "lifx", + "litejet", + "litterrobot", + "local_ip", + "locative", + "logi_circle", + "lookin", + "luftdaten", + "lutron_caseta", + "lyric", + "mailgun", + "mazda", + "melcloud", + "met", + "met_eireann", + "meteo_france", + "meteoclimatic", + "metoffice", + "mikrotik", + "mill", + "minecraft_server", + "mjpeg", + "mobile_app", + "modem_callerid", + "modern_forms", + "moehlenhoff_alpha2", + "monoprice", + "moon", + "motion_blinds", + "motioneye", + "mqtt", + "mullvad", + "mutesync", + "myq", + "mysensors", + "nam", + "nanoleaf", + "neato", + "nest", + "netatmo", + "netgear", + "nexia", + "nfandroidtv", + "nightscout", + "nina", + "nmap_tracker", + "notion", + "nuheat", + "nuki", + "nut", + "nws", + "nzbget", + "octoprint", + "omnilogic", + "oncue", + "ondilo_ico", + "onewire", + "onvif", + "open_meteo", + "opengarage", + "opentherm_gw", + "openuv", + "openweathermap", + "overkiz", + "ovo_energy", + "owntracks", + "p1_monitor", + "panasonic_viera", + "philips_js", + "pi_hole", + "picnic", + "plaato", + "plex", + "plugwise", + "plum_lightpad", + "point", + "poolsense", + "powerwall", + "profiler", + "progettihwsw", + "prosegur", + "ps4", + "pure_energie", + "pvoutput", + "pvpc_hourly_pricing", + "rachio", + "radio_browser", + "rainforest_eagle", + "rainmachine", + "rdw", + "recollect_waste", + "renault", + "rfxtrx", + "ridwell", + "ring", + "risco", + "rituals_perfume_genie", + "roku", + "roomba", + "roon", + "rpi_power", + "rtsp_to_webrtc", + "ruckus_unleashed", + "samsungtv", + "screenlogic", + "season", + "sense", + "senseme", + "sensibo", + "sentry", + "sharkiq", + "shelly", + "shopping_list", + "sia", + "simplisafe", + "sleepiq", + "sma", + "smappee", + "smart_meter_texas", + "smartthings", + "smarttub", + "smhi", + "sms", + "solaredge", + "solarlog", + "solax", + "soma", + "somfy", + "somfy_mylink", + "sonarr", + "songpal", + "sonos", + "speedtestdotnet", + "spider", + "spotify", + "squeezebox", + "srp_energy", + "starline", + "steamist", + "stookalert", + "subaru", + "sun", + "surepetcare", + "switch_as_x", + "switchbot", + "switcher_kis", + "syncthing", + "syncthru", + "synology_dsm", + "system_bridge", + "tado", + "tailscale", + "tasmota", + "tellduslive", + "tesla_wall_connector", + "tibber", + "tile", + "tolo", + "tomorrowio", + "toon", + "totalconnect", + "tplink", + "traccar", + "tractive", + "tradfri", + "trafikverket_weatherstation", + "transmission", + "tuya", + "twentemilieu", + "twilio", + "twinkly", + "unifi", + "unifiprotect", + "upb", + "upcloud", + "upnp", + "uptime", + "uptimerobot", + "vallox", + "velbus", + "venstar", + "vera", + "verisure", + "version", + "vesync", + "vicare", + "vilfo", + "vizio", + "vlc_telnet", + "volumio", + "wallbox", + "watttime", + "waze_travel_time", + "webostv", + "wemo", + "whirlpool", + "whois", + "wiffi", + "wilight", + "withings", + "wiz", + "wled", + "wolflink", + "xbox", + "xiaomi_aqara", + "xiaomi_miio", + "yale_smart_alarm", + "yamaha_musiccast", + "yeelight", + "youless", + "zerproc", + "zha", + "zwave_js", + "zwave_me" + ], + "helper": [ + "derivative" + ] +} diff --git a/homeassistant/loader.py b/homeassistant/loader.py index be70dc50f0c..364f212a1be 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -16,7 +16,7 @@ import logging import pathlib import sys from types import ModuleType -from typing import TYPE_CHECKING, Any, TypedDict, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, cast from awesomeversion import ( AwesomeVersion, @@ -87,6 +87,7 @@ class Manifest(TypedDict, total=False): name: str disabled: str domain: str + integration_type: Literal["integration", "helper"] dependencies: list[str] after_dependencies: list[str] requirements: list[str] @@ -180,20 +181,29 @@ async def async_get_custom_components( return cast(dict[str, "Integration"], reg_or_evt) -async def async_get_config_flows(hass: HomeAssistant) -> set[str]: +async def async_get_config_flows( + hass: HomeAssistant, + type_filter: Literal["helper", "integration"] | None = None, +) -> set[str]: """Return cached list of config flows.""" # pylint: disable=import-outside-toplevel from .generated.config_flows import FLOWS - flows: set[str] = set() - flows.update(FLOWS) - integrations = await async_get_custom_components(hass) + flows: set[str] = set() + + if type_filter is not None: + flows.update(FLOWS[type_filter]) + else: + for type_flows in FLOWS.values(): + flows.update(type_flows) + flows.update( [ integration.domain for integration in integrations.values() if integration.config_flow + and (type_filter is None or integration.integration_type == type_filter) ] ) @@ -474,6 +484,11 @@ class Integration: """Return the integration IoT Class.""" return self.manifest.get("iot_class") + @property + def integration_type(self) -> Literal["integration", "helper"]: + """Return the integration type.""" + return self.manifest.get("integration_type", "integration") + @property def mqtt(self) -> list[str] | None: """Return Integration MQTT entries.""" diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 87e9bea6291..169ccedf4a1 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -69,7 +69,10 @@ def validate_integration(config: Config, integration: Integration): def generate_and_validate(integrations: dict[str, Integration], config: Config): """Validate and generate config flow data.""" - domains = [] + domains = { + "integration": [], + "helper": [], + } for domain in sorted(integrations): integration = integrations[domain] @@ -79,7 +82,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config): validate_integration(config, integration) - domains.append(domain) + domains[integration.integration_type].append(domain) return BASE.format(json.dumps(domains, indent=4)) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5b344ed505d..ca9acedd515 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -152,6 +152,7 @@ MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, vol.Required("name"): str, + vol.Optional("integration_type"): "helper", vol.Optional("config_flow"): bool, vol.Optional("mqtt"): [str], vol.Optional("zeroconf"): [ diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 7006c1e6032..2a6ea9ca85f 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -112,6 +112,11 @@ class Integration: """List of dependencies.""" return self.manifest.get("dependencies", []) + @property + def integration_type(self) -> str: + """Get integration_type.""" + return self.manifest.get("integration_type", "integration") + def add_error(self, *args: Any, **kwargs: Any) -> None: """Add an error.""" self.errors.append(Error(*args, **kwargs)) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index a24e0961f9c..6366eca4c6d 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -23,6 +23,13 @@ from tests.common import ( ) +@pytest.fixture +def clear_handlers(): + """Clear config entry handlers.""" + with patch.dict(HANDLERS, clear=True): + yield + + @pytest.fixture(autouse=True) def mock_test_component(hass): """Ensure a component called 'test' exists.""" @@ -30,104 +37,133 @@ def mock_test_component(hass): @pytest.fixture -def client(hass, hass_client): +async def client(hass, hass_client): """Fixture that can interact with the config manager API.""" - hass.loop.run_until_complete(async_setup_component(hass, "http", {})) - hass.loop.run_until_complete(config_entries.async_setup(hass)) - yield hass.loop.run_until_complete(hass_client()) + await async_setup_component(hass, "http", {}) + await config_entries.async_setup(hass) + return await hass_client() -async def test_get_entries(hass, client): +async def test_get_entries(hass, client, clear_handlers): """Test get entries.""" - with patch.dict(HANDLERS, clear=True): + mock_integration(hass, MockModule("comp1")) + mock_integration( + hass, MockModule("comp2", partial_manifest={"integration_type": "helper"}) + ) + mock_integration(hass, MockModule("comp3")) - @HANDLERS.register("comp1") - class Comp1ConfigFlow: - """Config flow with options flow.""" + @HANDLERS.register("comp1") + class Comp1ConfigFlow: + """Config flow with options flow.""" - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get options flow.""" - pass + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + pass - @classmethod - @callback - def async_supports_options_flow(cls, config_entry): - """Return options flow support for this handler.""" - return True + @classmethod + @callback + def async_supports_options_flow(cls, config_entry): + """Return options flow support for this handler.""" + return True - hass.helpers.config_entry_flow.register_discovery_flow( - "comp2", "Comp 2", lambda: None - ) + hass.helpers.config_entry_flow.register_discovery_flow( + "comp2", "Comp 2", lambda: None + ) - entry = MockConfigEntry( - domain="comp1", - title="Test 1", - source="bla", - ) - entry.supports_unload = True - entry.add_to_hass(hass) - MockConfigEntry( - domain="comp2", - title="Test 2", - source="bla2", - state=core_ce.ConfigEntryState.SETUP_ERROR, - reason="Unsupported API", - ).add_to_hass(hass) - MockConfigEntry( - domain="comp3", - title="Test 3", - source="bla3", - disabled_by=core_ce.ConfigEntryDisabler.USER, - ).add_to_hass(hass) + entry = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry.supports_unload = True + entry.add_to_hass(hass) + MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ConfigEntryState.SETUP_ERROR, + reason="Unsupported API", + ).add_to_hass(hass) + MockConfigEntry( + domain="comp3", + title="Test 3", + source="bla3", + disabled_by=core_ce.ConfigEntryDisabler.USER, + ).add_to_hass(hass) - resp = await client.get("/api/config/config_entries/entry") - assert resp.status == HTTPStatus.OK - data = await resp.json() - for entry in data: - entry.pop("entry_id") - assert data == [ - { - "domain": "comp1", - "title": "Test 1", - "source": "bla", - "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supports_options": True, - "supports_remove_device": False, - "supports_unload": True, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": None, - "reason": None, - }, - { - "domain": "comp2", - "title": "Test 2", - "source": "bla2", - "state": core_ce.ConfigEntryState.SETUP_ERROR.value, - "supports_options": False, - "supports_remove_device": False, - "supports_unload": False, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": None, - "reason": "Unsupported API", - }, - { - "domain": "comp3", - "title": "Test 3", - "source": "bla3", - "state": core_ce.ConfigEntryState.NOT_LOADED.value, - "supports_options": False, - "supports_remove_device": False, - "supports_unload": False, - "pref_disable_new_entities": False, - "pref_disable_polling": False, - "disabled_by": core_ce.ConfigEntryDisabler.USER, - "reason": None, - }, - ] + resp = await client.get("/api/config/config_entries/entry") + assert resp.status == HTTPStatus.OK + data = await resp.json() + for entry in data: + entry.pop("entry_id") + assert data == [ + { + "domain": "comp1", + "title": "Test 1", + "source": "bla", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_options": True, + "supports_remove_device": False, + "supports_unload": True, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": None, + "reason": None, + }, + { + "domain": "comp2", + "title": "Test 2", + "source": "bla2", + "state": core_ce.ConfigEntryState.SETUP_ERROR.value, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": None, + "reason": "Unsupported API", + }, + { + "domain": "comp3", + "title": "Test 3", + "source": "bla3", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "disabled_by": core_ce.ConfigEntryDisabler.USER, + "reason": None, + }, + ] + + resp = await client.get("/api/config/config_entries/entry?domain=comp3") + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 1 + assert data[0]["domain"] == "comp3" + + resp = await client.get("/api/config/config_entries/entry?domain=comp3&type=helper") + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 0 + + resp = await client.get( + "/api/config/config_entries/entry?domain=comp3&type=integration" + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 1 + + resp = await client.get("/api/config/config_entries/entry?type=integration") + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert len(data) == 2 + assert data[0]["domain"] == "comp1" + assert data[1]["domain"] == "comp3" async def test_remove_entry(hass, client): @@ -224,13 +260,28 @@ async def test_reload_entry_in_setup_retry(hass, client, hass_admin_user): assert len(hass.config_entries.async_entries()) == 1 -async def test_available_flows(hass, client): +@pytest.mark.parametrize( + "type_filter,result", + ( + (None, {"hello", "another", "world"}), + ("integration", {"hello", "another"}), + ("helper", {"world"}), + ), +) +async def test_available_flows(hass, client, type_filter, result): """Test querying the available flows.""" - with patch.object(config_flows, "FLOWS", ["hello", "world"]): - resp = await client.get("/api/config/config_entries/flow_handlers") + with patch.object( + config_flows, + "FLOWS", + {"integration": ["hello", "another"], "helper": ["world"]}, + ): + resp = await client.get( + "/api/config/config_entries/flow_handlers", + params={"type": type_filter} if type_filter else {}, + ) assert resp.status == HTTPStatus.OK data = await resp.json() - assert set(data) == {"hello", "world"} + assert set(data) == result ############################ diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index be57b2d52e7..5520269ca9d 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture def mock_config_flows(): """Mock the config flows.""" - flows = [] + flows = {"integration": [], "helper": {}} with patch.object(config_flows, "FLOWS", flows): yield flows @@ -124,7 +124,7 @@ async def test_get_translations(hass, mock_config_flows, enable_custom_integrati async def test_get_translations_loads_config_flows(hass, mock_config_flows): """Test the get translations helper loads config flow translations.""" - mock_config_flows.append("component1") + mock_config_flows["integration"].append("component1") integration = Mock(file_path=pathlib.Path(__file__)) integration.name = "Component 1" @@ -153,7 +153,7 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): assert "component1" not in hass.config.components - mock_config_flows.append("component2") + mock_config_flows["integration"].append("component2") integration = Mock(file_path=pathlib.Path(__file__)) integration.name = "Component 2" From ba814af701e339596ee056e66a36224083dbe70b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 21 Mar 2022 07:21:26 +0000 Subject: [PATCH 0572/1054] Adopt SelectEntity in Utility Meter (#55690) Co-authored-by: Erik Montnemery --- .../components/utility_meter/__init__.py | 114 ++-------- .../components/utility_meter/const.py | 4 + .../components/utility_meter/select.py | 204 ++++++++++++++++++ .../components/utility_meter/services.yaml | 4 +- tests/components/utility_meter/test_init.py | 107 ++++++++- tests/components/utility_meter/test_sensor.py | 14 +- 6 files changed, 329 insertions(+), 118 deletions(-) create mode 100644 homeassistant/components/utility_meter/select.py diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 525b4f3b43c..6cd6cc46933 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -5,18 +5,16 @@ import logging from croniter import croniter import voluptuous as vol +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from .const import ( - ATTR_TARIFF, CONF_CRON_PATTERN, CONF_METER, CONF_METER_DELTA_VALUES, @@ -27,22 +25,15 @@ from .const import ( CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_TARIFFS, + DATA_LEGACY_COMPONENT, DATA_TARIFF_SENSORS, DATA_UTILITY, DOMAIN, METER_TYPES, - SERVICE_RESET, - SERVICE_SELECT_NEXT_TARIFF, - SERVICE_SELECT_TARIFF, - SIGNAL_RESET_METER, ) _LOGGER = logging.getLogger(__name__) -TARIFF_ICON = "mdi:clock-outline" - -ATTR_TARIFFS = "tariffs" - DEFAULT_OFFSET = timedelta(hours=0) @@ -105,9 +96,9 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an Utility Meter.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DATA_LEGACY_COMPONENT] = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DATA_UTILITY] = {} - register_services = False for meter, conf in config[DOMAIN].items(): _LOGGER.debug("Setup %s.%s", DOMAIN, meter) @@ -129,11 +120,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) else: # create tariff selection - await component.async_add_entities( - [TariffSelect(meter, list(conf[CONF_TARIFFS]))] + hass.async_create_task( + discovery.async_load_platform( + hass, + SELECT_DOMAIN, + DOMAIN, + {CONF_METER: meter, CONF_TARIFFS: conf[CONF_TARIFFS]}, + config, + ) ) + hass.data[DATA_UTILITY][meter][CONF_TARIFF_ENTITY] = "{}.{}".format( - DOMAIN, meter + SELECT_DOMAIN, meter ) # add one meter for each tariff @@ -151,89 +149,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config ) ) - register_services = True - - if register_services: - component.async_register_entity_service(SERVICE_RESET, {}, "async_reset_meters") - - component.async_register_entity_service( - SERVICE_SELECT_TARIFF, - {vol.Required(ATTR_TARIFF): cv.string}, - "async_select_tariff", - ) - - component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" - ) return True - - -class TariffSelect(RestoreEntity): - """Representation of a Tariff selector.""" - - def __init__(self, name, tariffs): - """Initialize a tariff selector.""" - self._name = name - self._current_tariff = None - self._tariffs = tariffs - self._icon = TARIFF_ICON - - async def async_added_to_hass(self): - """Run when entity about to be added.""" - await super().async_added_to_hass() - - state = await self.async_get_last_state() - if not state or state.state not in self._tariffs: - self._current_tariff = self._tariffs[0] - else: - self._current_tariff = state.state - - @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return the name of the select input.""" - return self._name - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._icon - - @property - def state(self): - """Return the state of the component.""" - return self._current_tariff - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_TARIFFS: self._tariffs} - - async def async_reset_meters(self): - """Reset all sensors of this meter.""" - _LOGGER.debug("reset meter %s", self.entity_id) - async_dispatcher_send(self.hass, SIGNAL_RESET_METER, self.entity_id) - - async def async_select_tariff(self, tariff): - """Select new option.""" - if tariff not in self._tariffs: - _LOGGER.warning( - "Invalid tariff: %s (possible tariffs: %s)", - tariff, - ", ".join(self._tariffs), - ) - return - self._current_tariff = tariff - self.async_write_ha_state() - - async def async_next_tariff(self): - """Offset current index.""" - current_index = self._tariffs.index(self._current_tariff) - new_index = (current_index + 1) % len(self._tariffs) - self._current_tariff = self._tariffs[new_index] - self.async_write_ha_state() diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 097496e231d..2bac649aace 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -1,6 +1,8 @@ """Constants for the utility meter component.""" DOMAIN = "utility_meter" +TARIFF_ICON = "mdi:clock-outline" + QUARTER_HOURLY = "quarter-hourly" HOURLY = "hourly" DAILY = "daily" @@ -23,6 +25,7 @@ METER_TYPES = [ DATA_UTILITY = "utility_meter_data" DATA_TARIFF_SENSORS = "utility_meter_sensors" +DATA_LEGACY_COMPONENT = "utility_meter_legacy_component" CONF_METER = "meter" CONF_SOURCE_SENSOR = "source" @@ -37,6 +40,7 @@ CONF_TARIFF_ENTITY = "tariff_entity" CONF_CRON_PATTERN = "cron" ATTR_TARIFF = "tariff" +ATTR_TARIFFS = "tariffs" ATTR_VALUE = "value" ATTR_CRON_PATTERN = "cron pattern" diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py new file mode 100644 index 00000000000..b523d72aba4 --- /dev/null +++ b/homeassistant/components/utility_meter/select.py @@ -0,0 +1,204 @@ +"""Support for tariff selection.""" +from __future__ import annotations + +import logging + +import voluptuous as vol + +from homeassistant.components.select import SelectEntity +from homeassistant.components.select.const import ( + ATTR_OPTION, + ATTR_OPTIONS, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE +from homeassistant.core import Event, callback, split_entity_id +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + ATTR_TARIFF, + ATTR_TARIFFS, + CONF_METER, + CONF_TARIFFS, + DATA_LEGACY_COMPONENT, + DOMAIN, + SERVICE_RESET, + SERVICE_SELECT_NEXT_TARIFF, + SERVICE_SELECT_TARIFF, + SIGNAL_RESET_METER, + TARIFF_ICON, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, conf, async_add_entities, discovery_info=None): + """Set up the utility meter select.""" + legacy_component = hass.data[DATA_LEGACY_COMPONENT] + async_add_entities( + [ + TariffSelect( + discovery_info[CONF_METER], + discovery_info[CONF_TARIFFS], + legacy_component.async_add_entities, + ) + ] + ) + + async def async_reset_meters(service_call): + """Reset all sensors of a meter.""" + entity_id = service_call.data["entity_id"] + + domain = split_entity_id(entity_id)[0] + if domain == DOMAIN: + for entity in legacy_component.entities: + if entity_id == entity.entity_id: + _LOGGER.debug( + "forward reset meter from %s to %s", + entity_id, + entity.tracked_entity_id, + ) + entity_id = entity.tracked_entity_id + + _LOGGER.debug("reset meter %s", entity_id) + async_dispatcher_send(hass, SIGNAL_RESET_METER, entity_id) + + hass.services.async_register( + DOMAIN, + SERVICE_RESET, + async_reset_meters, + vol.Schema({ATTR_ENTITY_ID: cv.entity_id}), + ) + + legacy_component.async_register_entity_service( + SERVICE_SELECT_TARIFF, + {vol.Required(ATTR_TARIFF): cv.string}, + "async_select_tariff", + ) + + legacy_component.async_register_entity_service( + SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" + ) + + +class TariffSelect(SelectEntity, RestoreEntity): + """Representation of a Tariff selector.""" + + def __init__(self, name, tariffs, add_legacy_entities): + """Initialize a tariff selector.""" + self._attr_name = name + self._current_tariff = None + self._tariffs = tariffs + self._attr_icon = TARIFF_ICON + self._attr_should_poll = False + self._add_legacy_entities = add_legacy_entities + + @property + def options(self): + """Return the available tariffs.""" + return self._tariffs + + @property + def current_option(self): + """Return current tariff.""" + return self._current_tariff + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + await self._add_legacy_entities([LegacyTariffSelect(self.entity_id)]) + + state = await self.async_get_last_state() + if not state or state.state not in self._tariffs: + self._current_tariff = self._tariffs[0] + else: + self._current_tariff = state.state + + async def async_select_option(self, option: str) -> None: + """Select new tariff (option).""" + self._current_tariff = option + self.async_write_ha_state() + + +class LegacyTariffSelect(Entity): + """Backwards compatibility for deprecated utility_meter select entity.""" + + def __init__(self, tracked_entity_id): + """Initialize the entity.""" + self._attr_icon = TARIFF_ICON + # Set name to influence enity_id + self._attr_name = split_entity_id(tracked_entity_id)[1] + self.tracked_entity_id = tracked_entity_id + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self.tracked_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + + self._attr_available = True + + self._attr_name = state.attributes.get(ATTR_FRIENDLY_NAME) + self._attr_state = state.state + self._attr_extra_state_attributes = { + ATTR_TARIFFS: state.attributes.get(ATTR_OPTIONS) + } + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def _async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + self.async_state_changed_listener(event) + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self.tracked_entity_id], _async_state_changed_listener + ) + ) + + # Call once on adding + _async_state_changed_listener() + + async def async_select_tariff(self, tariff): + """Select new option.""" + _LOGGER.warning( + "The 'utility_meter.select_tariff' service has been deprecated and will " + "be removed in HA Core 2022.7. Please use 'select.select_option' instead", + ) + await self.hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: self.tracked_entity_id, ATTR_OPTION: tariff}, + blocking=True, + context=self._context, + ) + + async def async_next_tariff(self): + """Offset current index.""" + _LOGGER.warning( + "The 'utility_meter.next_tariff' service has been deprecated and will " + "be removed in HA Core 2022.7. Please use 'select.select_option' instead", + ) + if ( + not self.available + or (state := self.hass.states.get(self.tracked_entity_id)) is None + ): + return + tariffs = state.attributes.get(ATTR_OPTIONS) + current_tariff = state.state + current_index = tariffs.index(current_tariff) + new_index = (current_index + 1) % len(tariffs) + + await self.async_select_tariff(tariffs[new_index]) diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index c3f95d22175..800e001f6ff 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -2,10 +2,10 @@ reset: name: Reset - description: Resets the counter of a utility meter. + description: Resets all counters of an utility meter. target: entity: - domain: utility_meter + domain: select next_tariff: name: Next Tariff diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 3297c696ca1..8b600865d44 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -2,12 +2,16 @@ from datetime import timedelta from unittest.mock import patch +from homeassistant.components.select.const import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) from homeassistant.components.utility_meter.const import ( - ATTR_TARIFF, DOMAIN, SERVICE_RESET, SERVICE_SELECT_NEXT_TARIFF, SERVICE_SELECT_TARIFF, + SIGNAL_RESET_METER, ) import homeassistant.components.utility_meter.sensor as um_sensor from homeassistant.const import ( @@ -19,6 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import State +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -39,7 +44,7 @@ async def test_restore_state(hass): hass, [ State( - "utility_meter.energy_bill", + "select.energy_bill", "midpeak", ), ], @@ -50,7 +55,7 @@ async def test_restore_state(hass): await hass.async_block_till_done() # restore from cache - state = hass.states.get("utility_meter.energy_bill") + state = hass.states.get("select.energy_bill") assert state.state == "midpeak" @@ -98,7 +103,7 @@ async def test_services(hass): state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "0" - # Next tariff + # Next tariff - only supported on legacy entity data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) await hass.async_block_till_done() @@ -120,15 +125,15 @@ async def test_services(hass): assert state.state == "1" # Change tariff - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill", ATTR_TARIFF: "wrong_tariff"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_TARIFF, data) + data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "wrong_tariff"} + await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) await hass.async_block_till_done() # Inexisting tariff, ignoring - assert hass.states.get("utility_meter.energy_bill").state != "wrong_tariff" + assert hass.states.get("select.energy_bill").state != "wrong_tariff" - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill", ATTR_TARIFF: "peak"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_TARIFF, data) + data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "peak"} + await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) await hass.async_block_till_done() now += timedelta(seconds=10) @@ -148,7 +153,7 @@ async def test_services(hass): assert state.state == "1" # Reset meters - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} + data = {ATTR_ENTITY_ID: "select.energy_bill"} await hass.services.async_call(DOMAIN, SERVICE_RESET, data) await hass.async_block_till_done() @@ -240,3 +245,85 @@ async def test_bad_cron(hass, legacy_patchable_time): async def test_setup_missing_discovery(hass): """Test setup with configuration missing discovery_info.""" assert not await um_sensor.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) + + +async def test_legacy_support(hass): + """Test legacy entity support.""" + config = { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + "cycle": "hourly", + "tariffs": ["peak", "offpeak"], + }, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, Platform.SENSOR, config) + await hass.async_block_till_done() + + select_state = hass.states.get("select.energy_bill") + legacy_state = hass.states.get("utility_meter.energy_bill") + + assert select_state.state == legacy_state.state == "peak" + select_attributes = select_state.attributes + legacy_attributes = legacy_state.attributes + assert select_attributes.keys() == { + "friendly_name", + "icon", + "options", + } + assert legacy_attributes.keys() == {"friendly_name", "icon", "tariffs"} + assert select_attributes["friendly_name"] == legacy_attributes["friendly_name"] + assert select_attributes["icon"] == legacy_attributes["icon"] + assert select_attributes["options"] == legacy_attributes["tariffs"] + + # Change tariff on the select + data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"} + await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) + await hass.async_block_till_done() + + select_state = hass.states.get("select.energy_bill") + legacy_state = hass.states.get("utility_meter.energy_bill") + assert select_state.state == legacy_state.state == "offpeak" + + # Change tariff on the legacy entity + data = {ATTR_ENTITY_ID: "utility_meter.energy_bill", "tariff": "offpeak"} + await hass.services.async_call(DOMAIN, SERVICE_SELECT_TARIFF, data) + await hass.async_block_till_done() + + select_state = hass.states.get("select.energy_bill") + legacy_state = hass.states.get("utility_meter.energy_bill") + assert select_state.state == legacy_state.state == "offpeak" + + # Cycle tariffs on the select - not supported + data = {ATTR_ENTITY_ID: "select.energy_bill"} + await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) + await hass.async_block_till_done() + + select_state = hass.states.get("select.energy_bill") + legacy_state = hass.states.get("utility_meter.energy_bill") + assert select_state.state == legacy_state.state == "offpeak" + + # Cycle tariffs on the legacy entity + data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} + await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) + await hass.async_block_till_done() + + select_state = hass.states.get("select.energy_bill") + legacy_state = hass.states.get("utility_meter.energy_bill") + assert select_state.state == legacy_state.state == "peak" + + # Reset the legacy entity + reset_calls = [] + + def async_reset_meter(entity_id): + reset_calls.append(entity_id) + + async_dispatcher_connect(hass, SIGNAL_RESET_METER, async_reset_meter) + + data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} + await hass.services.async_call(DOMAIN, SERVICE_RESET, data) + await hass.async_block_till_done() + assert reset_calls == ["select.energy_bill"] diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index fbaf795f9e2..df8e1c5e6a1 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -3,20 +3,22 @@ from contextlib import contextmanager from datetime import timedelta from unittest.mock import patch +from homeassistant.components.select.const import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, ) from homeassistant.components.utility_meter.const import ( - ATTR_TARIFF, ATTR_VALUE, DAILY, DOMAIN, HOURLY, QUARTER_HOURLY, SERVICE_CALIBRATE_METER, - SERVICE_SELECT_TARIFF, ) from homeassistant.components.utility_meter.sensor import ( ATTR_LAST_RESET, @@ -117,9 +119,9 @@ async def test_state(hass): assert state.attributes.get("status") == PAUSED await hass.services.async_call( - DOMAIN, - SERVICE_SELECT_TARIFF, - {ATTR_ENTITY_ID: "utility_meter.energy_bill", ATTR_TARIFF: "offpeak"}, + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"}, blocking=True, ) @@ -343,7 +345,7 @@ async def test_restore_state(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - state = hass.states.get("utility_meter.energy_bill") + state = hass.states.get("select.energy_bill") assert state.state == "onpeak" state = hass.states.get("sensor.energy_bill_onpeak") From 3320606a1b25ac366364fabf50c3f935313ee993 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Mar 2022 02:16:19 -0700 Subject: [PATCH 0573/1054] Hue handle HTTP errors (#68396) --- homeassistant/components/hue/bridge.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 346cc67d235..dd6182b244a 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -6,6 +6,7 @@ from collections.abc import Callable import logging from typing import Any +import aiohttp from aiohttp import client_exceptions from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized from aiohue.errors import AiohueException, BridgeBusy @@ -14,7 +15,7 @@ import async_timeout from homeassistant import core from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import aiohttp_client from .const import CONF_API_VERSION, DOMAIN @@ -131,7 +132,11 @@ class HueBridge: # log only self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) return None - raise err + raise HomeAssistantError(f"Request failed: {err}") from err + except aiohttp.ClientError as err: + raise HomeAssistantError( + f"Request failed due connection error: {err}" + ) from err async def async_reset(self) -> bool: """Reset this bridge to default state. From 830cc278d3a4decb2dd9305305da281ac72282ed Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 10:22:30 +0100 Subject: [PATCH 0574/1054] Improve `CoordinatorEntity` typing (#68441) --- homeassistant/components/co2signal/sensor.py | 6 +++--- .../components/fjaraskupan/binary_sensor.py | 2 +- homeassistant/components/fjaraskupan/fan.py | 2 +- homeassistant/components/fjaraskupan/light.py | 2 +- .../components/fjaraskupan/number.py | 4 +++- .../components/fjaraskupan/sensor.py | 2 +- homeassistant/components/github/sensor.py | 3 +-- homeassistant/components/hassio/entity.py | 4 ++-- homeassistant/components/homewizard/sensor.py | 2 +- homeassistant/components/homewizard/switch.py | 6 +++--- homeassistant/components/kraken/sensor.py | 9 +++++++-- homeassistant/components/met/weather.py | 6 ++---- .../components/modern_forms/__init__.py | 2 -- homeassistant/components/plugwise/entity.py | 6 ++---- homeassistant/components/powerwall/entity.py | 7 +++++-- .../components/pure_energie/sensor.py | 5 +++-- .../components/renault/renault_coordinator.py | 4 ++-- .../components/renault/renault_entities.py | 10 ++++++---- homeassistant/components/renault/sensor.py | 14 +++++++------- .../components/template/trigger_entity.py | 5 +++-- homeassistant/components/tibber/sensor.py | 19 +++++++++++-------- .../components/twentemilieu/entity.py | 6 ++++-- homeassistant/helpers/update_coordinator.py | 9 ++++++--- 23 files changed, 75 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 0c637506447..23303474e3b 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -9,13 +9,13 @@ from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import CO2SignalCoordinator, CO2SignalResponse +from . import CO2SignalCoordinator from .const import ATTRIBUTION, DOMAIN SCAN_INTERVAL = timedelta(minutes=3) @@ -55,7 +55,7 @@ async def async_setup_entry( async_add_entities(CO2Sensor(coordinator, description) for description in SENSORS) -class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorEntity): +class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" _attr_state_class = SensorStateClass.MEASUREMENT diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index 9f24a3d39d2..f1672530c45 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -67,7 +67,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class BinarySensor(CoordinatorEntity[State], BinarySensorEntity): +class BinarySensor(CoordinatorEntity[DataUpdateCoordinator[State]], BinarySensorEntity): """Grease filter sensor.""" entity_description: EntityDescription diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index ea4327fc4b6..4b04910a167 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -70,7 +70,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class Fan(CoordinatorEntity[State], FanEntity): +class Fan(CoordinatorEntity[DataUpdateCoordinator[State]], FanEntity): """Fan entity.""" def __init__( diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 8c44460a099..7c1e5d34138 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -37,7 +37,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class Light(CoordinatorEntity[State], LightEntity): +class Light(CoordinatorEntity[DataUpdateCoordinator[State]], LightEntity): """Light device.""" def __init__( diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index eecb0b3b8e1..bbde9bd8898 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -34,7 +34,9 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): +class PeriodicVentingTime( + CoordinatorEntity[DataUpdateCoordinator[State]], NumberEntity +): """Periodic Venting.""" _attr_max_value: float = 59 diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index 8c19b3e3cec..fbd9d5f6d08 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -39,7 +39,7 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class RssiSensor(CoordinatorEntity[State], SensorEntity): +class RssiSensor(CoordinatorEntity[DataUpdateCoordinator[State]], SensorEntity): """Sensor device.""" def __init__( diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a09e440e2ce..8dff4b04b01 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -172,12 +172,11 @@ async def async_setup_entry( ) -class GitHubSensorEntity(CoordinatorEntity[dict[str, Any]], SensorEntity): +class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorEntity): """Defines a GitHub sensor entity.""" _attr_attribution = "Data provided by the GitHub API" - coordinator: GitHubDataUpdateCoordinator entity_description: GitHubSensorEntityDescription def __init__( diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 5dd41166c32..9f727269fed 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -11,7 +11,7 @@ from . import DOMAIN, HassioDataUpdateCoordinator from .const import ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_OS -class HassioAddonEntity(CoordinatorEntity): +class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base entity for a Hass.io add-on.""" def __init__( @@ -38,7 +38,7 @@ class HassioAddonEntity(CoordinatorEntity): ) -class HassioOSEntity(CoordinatorEntity): +class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Hass.io OS.""" def __init__( diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index e9a09f9db86..8863d819db2 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -142,7 +142,7 @@ async def async_setup_entry( async_add_entities(entities) -class HWEnergySensor(CoordinatorEntity[DeviceResponseEntry], SensorEntity): +class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): """Representation of a HomeWizard Sensor.""" def __init__( diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 7860370baa7..3c6b1a1c5dc 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -31,11 +31,11 @@ async def async_setup_entry( ) -class HWEnergySwitchEntity(CoordinatorEntity, SwitchEntity): +class HWEnergySwitchEntity( + CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SwitchEntity +): """Representation switchable entity.""" - coordinator: HWEnergyDeviceUpdateCoordinator - def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index d82f85e9ba9..f98a48d8dc3 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -11,7 +11,10 @@ from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import KrakenData from .const import ( @@ -86,7 +89,9 @@ async def async_setup_entry( ) -class KrakenSensor(CoordinatorEntity[Optional[KrakenResponse]], SensorEntity): +class KrakenSensor( + CoordinatorEntity[DataUpdateCoordinator[Optional[KrakenResponse]]], SensorEntity +): """Define a Kraken sensor.""" entity_description: KrakenSensorEntityDescription diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 5791620d5ac..d17bfda03f1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -45,7 +45,7 @@ from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed -from . import MetDataUpdateCoordinator, MetWeatherData +from . import MetDataUpdateCoordinator from .const import ( ATTR_FORECAST_PRECIPITATION, ATTR_MAP, @@ -127,11 +127,9 @@ def format_condition(condition: str) -> str: return condition -class MetWeather(CoordinatorEntity[MetWeatherData], WeatherEntity): +class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" - coordinator: MetDataUpdateCoordinator - def __init__( self, coordinator: MetDataUpdateCoordinator, diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index dfd6a9a8807..af4f05a1536 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -126,8 +126,6 @@ class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceSt class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator]): """Defines a Modern Forms device entity.""" - coordinator: ModernFormsDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index b172b5468b0..b0896c3cd6d 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -12,14 +12,12 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import PlugwiseData, PlugwiseDataUpdateCoordinator +from .coordinator import PlugwiseDataUpdateCoordinator -class PlugwiseEntity(CoordinatorEntity[PlugwiseData]): +class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): """Represent a PlugWise Entity.""" - coordinator: PlugwiseDataUpdateCoordinator - def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 20871944663..5d55b8b8bf1 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -1,7 +1,10 @@ """The Tesla Powerwall integration base entity.""" from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( DOMAIN, @@ -13,7 +16,7 @@ from .const import ( from .models import PowerwallData, PowerwallRuntimeData -class PowerWallEntity(CoordinatorEntity[PowerwallData]): +class PowerWallEntity(CoordinatorEntity[DataUpdateCoordinator[PowerwallData]]): """Base class for powerwall entities.""" def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: diff --git a/homeassistant/components/pure_energie/sensor.py b/homeassistant/components/pure_energie/sensor.py index fffbfd7c7bb..8b07a2818f8 100644 --- a/homeassistant/components/pure_energie/sensor.py +++ b/homeassistant/components/pure_energie/sensor.py @@ -78,10 +78,11 @@ async def async_setup_entry( ) -class PureEnergieSensorEntity(CoordinatorEntity[PureEnergieData], SensorEntity): +class PureEnergieSensorEntity( + CoordinatorEntity[PureEnergieDataUpdateCoordinator], SensorEntity +): """Defines an Pure Energie sensor.""" - coordinator: PureEnergieDataUpdateCoordinator entity_description: PureEnergieSensorEntityDescription def __init__( diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py index 4487d9db9ab..7db5ed0c4e1 100644 --- a/homeassistant/components/renault/renault_coordinator.py +++ b/homeassistant/components/renault/renault_coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import timedelta import logging -from typing import TypeVar +from typing import Optional, TypeVar from renault_api.kamereon.exceptions import ( AccessDeniedException, @@ -16,7 +16,7 @@ from renault_api.kamereon.models import KamereonVehicleDataAttributes from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -T = TypeVar("T", bound=KamereonVehicleDataAttributes) +T = TypeVar("T", bound=Optional[KamereonVehicleDataAttributes]) class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 14ebcf2c2e4..82f071decae 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -2,14 +2,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .renault_coordinator import T +from .renault_coordinator import RenaultDataUpdateCoordinator, T from .renault_vehicle import RenaultVehicleProxy @@ -50,7 +50,9 @@ class RenaultEntity(Entity): return f"{self.vehicle.device_info[ATTR_NAME]} {self.entity_description.name}" -class RenaultDataEntity(CoordinatorEntity[Optional[T]], RenaultEntity): +class RenaultDataEntity( + CoordinatorEntity[RenaultDataUpdateCoordinator[T]], RenaultEntity +): """Implementation of a Renault entity with a data coordinator.""" def __init__( @@ -65,5 +67,5 @@ class RenaultDataEntity(CoordinatorEntity[Optional[T]], RenaultEntity): def _get_data_attr(self, key: str) -> StateType: """Return the attribute value from the coordinator data.""" if self.coordinator.data is None: - return None + return None # type: ignore[unreachable] return cast(StateType, getattr(self.coordinator.data, key)) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index c6621b16bbc..3b486af175b 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, Generic, cast from renault_api.kamereon.enums import ChargeState, PlugState from renault_api.kamereon.models import ( @@ -45,18 +45,18 @@ from .renault_vehicle import RenaultVehicleProxy @dataclass -class RenaultSensorRequiredKeysMixin: +class RenaultSensorRequiredKeysMixin(Generic[T]): """Mixin for required keys.""" data_key: str - entity_class: type[RenaultSensor] + entity_class: type[RenaultSensor[T]] @dataclass class RenaultSensorEntityDescription( SensorEntityDescription, RenaultDataEntityDescription, - RenaultSensorRequiredKeysMixin, + RenaultSensorRequiredKeysMixin[T], ): """Class describing Renault sensor entities.""" @@ -73,7 +73,7 @@ async def async_setup_entry( ) -> None: """Set up the Renault entities from config entry.""" proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] - entities: list[RenaultSensor] = [ + entities: list[RenaultSensor[Any]] = [ description.entity_class(vehicle, description) for vehicle in proxy.vehicles.values() for description in SENSOR_TYPES @@ -87,7 +87,7 @@ async def async_setup_entry( class RenaultSensor(RenaultDataEntity[T], SensorEntity): """Mixin for sensor specific attributes.""" - entity_description: RenaultSensorEntityDescription + entity_description: RenaultSensorEntityDescription[T] @property def data(self) -> StateType: @@ -157,7 +157,7 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: return as_utc(original_dt) -SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( +SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( RenaultSensorEntityDescription( key="battery_level", coordinator="battery", diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 2768609e65d..123b365d697 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -13,13 +13,14 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template, update_coordinator +from homeassistant.helpers import template +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import TriggerUpdateCoordinator from .const import CONF_ATTRIBUTES, CONF_AVAILABILITY, CONF_PICTURE -class TriggerEntity(update_coordinator.CoordinatorEntity): +class TriggerEntity(CoordinatorEntity[TriggerUpdateCoordinator]): """Template entity based on trigger data.""" domain: str diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 12bcec295d0..cd5148ba58a 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -32,11 +32,14 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import async_get as async_get_dev_reg from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER @@ -239,7 +242,7 @@ async def async_setup_entry( entity_registry = async_get_entity_reg(hass) device_registry = async_get_dev_reg(hass) - coordinator: update_coordinator.DataUpdateCoordinator | None = None + coordinator: TibberDataCoordinator | None = None entities: list[TibberSensor] = [] for home in tibber_connection.get_homes(only_active=False): try: @@ -392,13 +395,13 @@ class TibberSensorElPrice(TibberSensor): ]["estimatedAnnualConsumption"] -class TibberDataSensor(TibberSensor, update_coordinator.CoordinatorEntity): +class TibberDataSensor(TibberSensor, CoordinatorEntity["TibberDataCoordinator"]): """Representation of a Tibber sensor.""" def __init__( self, tibber_home, - coordinator: update_coordinator.DataUpdateCoordinator, + coordinator: TibberDataCoordinator, entity_description: SensorEntityDescription, ): """Initialize the sensor.""" @@ -420,7 +423,7 @@ class TibberDataSensor(TibberSensor, update_coordinator.CoordinatorEntity): return getattr(self._tibber_home, self.entity_description.key) -class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): +class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]): """Representation of a Tibber sensor for real time consumption.""" def __init__( @@ -450,7 +453,7 @@ class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): @callback def _handle_coordinator_update(self) -> None: - if not (live_measurement := self.coordinator.get_live_measurement()): # type: ignore[attr-defined] + if not (live_measurement := self.coordinator.get_live_measurement()): return state = live_measurement.get(self.entity_description.key) if state is None: @@ -479,7 +482,7 @@ class TibberSensorRT(TibberSensor, update_coordinator.CoordinatorEntity): self.async_write_ha_state() -class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): +class TibberRtDataCoordinator(DataUpdateCoordinator): """Handle Tibber realtime data.""" def __init__(self, async_add_entities, tibber_home, hass): @@ -538,7 +541,7 @@ class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): return self.data.get("data", {}).get("liveMeasurement") -class TibberDataCoordinator(update_coordinator.DataUpdateCoordinator): +class TibberDataCoordinator(DataUpdateCoordinator): """Handle Tibber data and insert statistics.""" def __init__(self, hass, tibber_connection): diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py index d075b1cbf6b..008c0fa441e 100644 --- a/homeassistant/components/twentemilieu/entity.py +++ b/homeassistant/components/twentemilieu/entity.py @@ -17,12 +17,14 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN -class TwenteMilieuEntity(CoordinatorEntity[dict[WasteType, list[date]]], Entity): +class TwenteMilieuEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[WasteType, list[date]]]], Entity +): """Defines a Twente Milieu entity.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 7453a845e9d..0c0d647d48a 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from time import monotonic -from typing import Generic, TypeVar +from typing import Any, Generic, TypeVar # pylint: disable=unused-import import urllib.error import aiohttp @@ -24,6 +24,9 @@ REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 REQUEST_REFRESH_DEFAULT_IMMEDIATE = True _T = TypeVar("_T") +_DataUpdateCoordinatorT = TypeVar( + "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" +) class UpdateFailed(Exception): @@ -295,10 +298,10 @@ class DataUpdateCoordinator(Generic[_T]): self._unsub_refresh = None -class CoordinatorEntity(Generic[_T], entity.Entity): +class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """A class for entities using DataUpdateCoordinator.""" - def __init__(self, coordinator: DataUpdateCoordinator[_T]) -> None: + def __init__(self, coordinator: _DataUpdateCoordinatorT) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator From 073fb40b79cf8aa06790fdceb23b6857db888c99 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Mar 2022 11:02:48 +0100 Subject: [PATCH 0575/1054] Add update entity platform (#68248) Co-authored-by: Glenn Waters --- .core_files.yaml | 1 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/update.py | 153 +++++ homeassistant/components/update/__init__.py | 353 +++++++++++ homeassistant/components/update/const.py | 30 + homeassistant/components/update/manifest.json | 7 + homeassistant/components/update/services.yaml | 27 + .../components/update/significant_change.py | 30 + homeassistant/components/update/strings.json | 3 + homeassistant/const.py | 1 + mypy.ini | 11 + tests/components/demo/test_update.py | 157 +++++ tests/components/update/__init__.py | 1 + tests/components/update/test_init.py | 583 ++++++++++++++++++ .../update/test_significant_change.py | 90 +++ .../custom_components/test/update.py | 138 +++++ 18 files changed, 1589 insertions(+) create mode 100644 homeassistant/components/demo/update.py create mode 100644 homeassistant/components/update/__init__.py create mode 100644 homeassistant/components/update/const.py create mode 100644 homeassistant/components/update/manifest.json create mode 100644 homeassistant/components/update/services.yaml create mode 100644 homeassistant/components/update/significant_change.py create mode 100644 homeassistant/components/update/strings.json create mode 100644 tests/components/demo/test_update.py create mode 100644 tests/components/update/__init__.py create mode 100644 tests/components/update/test_init.py create mode 100644 tests/components/update/test_significant_change.py create mode 100644 tests/testing_config/custom_components/test/update.py diff --git a/.core_files.yaml b/.core_files.yaml index f154af62560..654730e5613 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -39,6 +39,7 @@ base_platforms: &base_platforms - homeassistant/components/stt/** - homeassistant/components/switch/** - homeassistant/components/tts/** + - homeassistant/components/update/** - homeassistant/components/vacuum/** - homeassistant/components/water_heater/** - homeassistant/components/weather/** diff --git a/.strict-typing b/.strict-typing index c9f3a978047..7b1fc47b3a3 100644 --- a/.strict-typing +++ b/.strict-typing @@ -207,6 +207,7 @@ homeassistant.components.tts.* homeassistant.components.twentemilieu.* homeassistant.components.unifiprotect.* homeassistant.components.upcloud.* +homeassistant.components.update.* homeassistant.components.uptime.* homeassistant.components.uptimerobot.* homeassistant.components.usb.* diff --git a/CODEOWNERS b/CODEOWNERS index d013572790f..24a7793f6a6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1059,6 +1059,8 @@ tests/components/upb/* @gwww homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop tests/components/upcloud/* @scop +homeassistant/components/update/* @home-assistant/core +tests/components/update/* @home-assistant/core homeassistant/components/updater/* @home-assistant/core tests/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman @ehendrix23 diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index abee8310e17..2afb58aff70 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -40,6 +40,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "sensor", "siren", "switch", + "update", "vacuum", "water_heater", ] diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py new file mode 100644 index 00000000000..a48c4a3cab2 --- /dev/null +++ b/homeassistant/components/demo/update.py @@ -0,0 +1,153 @@ +"""Demo platform that offers fake update entities.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from homeassistant.components.update import UpdateDeviceClass, UpdateEntity +from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import DOMAIN + +FAKE_INSTALL_SLEEP_TIME = 0.5 + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up demo update entities.""" + async_add_entities( + [ + DemoUpdate( + unique_id="update_no_install", + name="Demo Update No Install", + title="Awesomesoft Inc.", + current_version="1.0.0", + latest_version="1.0.1", + release_summary="Awesome update, fixing everything!", + release_url="https://www.example.com/release/1.0.1", + support_install=False, + ), + DemoUpdate( + unique_id="update_2_date", + name="Demo No Update", + title="AdGuard Home", + current_version="1.0.0", + latest_version="1.0.0", + ), + DemoUpdate( + unique_id="update_addon", + name="Demo add-on", + title="AdGuard Home", + current_version="1.0.0", + latest_version="1.0.1", + release_summary="Awesome update, fixing everything!", + release_url="https://www.example.com/release/1.0.1", + ), + DemoUpdate( + unique_id="update_light_bulb", + name="Demo Living Room Bulb Update", + title="Philips Lamps Firmware", + current_version="1.93.3", + latest_version="1.94.2", + release_summary="Added support for effects", + release_url="https://www.example.com/release/1.93.3", + device_class=UpdateDeviceClass.FIRMWARE, + ), + DemoUpdate( + unique_id="update_support_progress", + name="Demo Update with Progress", + title="Philips Lamps Firmware", + current_version="1.93.3", + latest_version="1.94.2", + support_progress=True, + release_summary="Added support for effects", + release_url="https://www.example.com/release/1.93.3", + device_class=UpdateDeviceClass.FIRMWARE, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +async def _fake_install() -> None: + """Fake install an update.""" + await asyncio.sleep(FAKE_INSTALL_SLEEP_TIME) + + +class DemoUpdate(UpdateEntity): + """Representation of a demo update entity.""" + + _attr_should_poll = False + + def __init__( + self, + *, + unique_id: str, + name: str, + title: str | None, + current_version: str | None, + latest_version: str | None, + release_summary: str | None = None, + release_url: str | None = None, + support_progress: bool = False, + support_install: bool = True, + device_class: UpdateDeviceClass | None = None, + ) -> None: + """Initialize the Demo select entity.""" + self._attr_current_version = current_version + self._attr_device_class = device_class + self._attr_latest_version = latest_version + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_release_summary = release_summary + self._attr_release_url = release_url + self._attr_title = title + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) + if support_install: + self._attr_supported_features |= ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.BACKUP + | UpdateEntityFeature.SPECIFIC_VERSION + ) + if support_progress: + self._attr_supported_features |= UpdateEntityFeature.PROGRESS + + async def async_install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update.""" + if self.supported_features & UpdateEntityFeature.PROGRESS: + for progress in range(0, 100, 10): + self._attr_in_progress = progress + self.async_write_ha_state() + await _fake_install() + + self._attr_in_progress = False + self._attr_current_version = ( + version if version is not None else self.latest_version + ) + self.async_write_ha_state() diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py new file mode 100644 index 00000000000..a19ecc535a2 --- /dev/null +++ b/homeassistant/components/update/__init__.py @@ -0,0 +1,353 @@ +"""Component to allow for providing device or service updates.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging +from typing import Any, Final, final + +import voluptuous as vol + +from homeassistant.backports.enum import StrEnum +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_validation import ( + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import EntityCategory, EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_BACKUP, + ATTR_CURRENT_VERSION, + ATTR_IN_PROGRESS, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, + ATTR_TITLE, + ATTR_VERSION, + DOMAIN, + SERVICE_INSTALL, + SERVICE_SKIP, + UpdateEntityFeature, +) + +SCAN_INTERVAL = timedelta(minutes=15) + +ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" + +_LOGGER = logging.getLogger(__name__) + + +class UpdateDeviceClass(StrEnum): + """Device class for update.""" + + FIRMWARE = "firmware" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(UpdateDeviceClass)) + + +__all__ = [ + "ATTR_BACKUP", + "ATTR_VERSION", + "DEVICE_CLASSES_SCHEMA", + "DOMAIN", + "PLATFORM_SCHEMA_BASE", + "PLATFORM_SCHEMA", + "SERVICE_INSTALL", + "SERVICE_SKIP", + "UpdateDeviceClass", + "UpdateEntity", + "UpdateEntityDescription", + "UpdateEntityFeature", +] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Select entities.""" + component = hass.data[DOMAIN] = EntityComponent( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_INSTALL, + { + vol.Optional(ATTR_VERSION): cv.string, + vol.Optional(ATTR_BACKUP): cv.boolean, + }, + async_install, + [UpdateEntityFeature.INSTALL], + ) + + component.async_register_entity_service( + SERVICE_SKIP, + {}, + UpdateEntity.async_skip.__name__, + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None: + """Service call wrapper to validate the call.""" + # If version is not specified, but no update is available. + if (version := service_call.data.get(ATTR_VERSION)) is None and ( + entity.current_version == entity.latest_version or entity.latest_version is None + ): + raise HomeAssistantError(f"No update available for {entity.name}") + + # If version is specified, but not supported by the entity. + if ( + version is not None + and not entity.supported_features & UpdateEntityFeature.SPECIFIC_VERSION + ): + raise HomeAssistantError( + f"Installing a specific version is not supported for {entity.name}" + ) + + # If backup is requested, but not supported by the entity. + if ( + backup := service_call.data.get(ATTR_BACKUP) + ) and not entity.supported_features & UpdateEntityFeature.BACKUP: + raise HomeAssistantError(f"Backup is not supported for {entity.name}") + + # Update is already in progress. + if entity.in_progress is not False: + raise HomeAssistantError( + f"Update installation already in progress for {entity.name}" + ) + + await entity.async_install_with_progress(version, backup) + + +@dataclass +class UpdateEntityDescription(EntityDescription): + """A class that describes update entities.""" + + device_class: UpdateDeviceClass | str | None = None + entity_category: EntityCategory | None = EntityCategory.CONFIG + + +class UpdateEntity(RestoreEntity): + """Representation of an update entity.""" + + entity_description: UpdateEntityDescription + _attr_current_version: str | None = None + _attr_device_class: UpdateDeviceClass | str | None + _attr_in_progress: bool | int = False + _attr_latest_version: str | None = None + _attr_release_summary: str | None = None + _attr_release_url: str | None = None + _attr_state: None = None + _attr_supported_features: int = 0 + _attr_title: str | None = None + __skipped_version: str | None = None + __in_progress: bool = False + + @property + def current_version(self) -> str | None: + """Version currently in use.""" + return self._attr_current_version + + @property + def device_class(self) -> UpdateDeviceClass | str | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + + @property + def entity_category(self) -> EntityCategory | str | None: + """Return the category of the entity, if any.""" + if hasattr(self, "_attr_entity_category"): + return self._attr_entity_category + if hasattr(self, "entity_description"): + return self.entity_description.entity_category + return EntityCategory.CONFIG + + @property + def in_progress(self) -> bool | int | None: + """Update installation progress. + + Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used. + + Can either return a boolean (True if in progress, False if not) + or an integer to indicate the progress in from 0 to 100%. + """ + return self._attr_in_progress + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self._attr_latest_version + + @property + def release_summary(self) -> str | None: + """Summary of the release notes or changelog. + + This is not suitable for long changelogs, but merely suitable + for a short excerpt update description of max 255 characters. + """ + return self._attr_release_summary + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return self._attr_release_url + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._attr_supported_features + + @property + def title(self) -> str | None: + """Title of the software. + + This helps to differentiate between the device or entity name + versus the title of the software installed. + """ + return self._attr_title + + @final + async def async_skip(self) -> None: + """Skip the current offered version to update.""" + if (latest_version := self.latest_version) is None: + raise HomeAssistantError(f"Cannot skip an unknown version for {self.name}") + if self.current_version == latest_version: + raise HomeAssistantError(f"No update available to skip for {self.name}") + self.__skipped_version = latest_version + self.async_write_ha_state() + + async def async_install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update. + + Version can be specified to install a specific version. When `None`, the + latest version needs to be installed. + + The backup parameter indicates a backup should be taken before + installing the update. + """ + await self.hass.async_add_executor_job(self.install, version, backup) + + def install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update. + + Version can be specified to install a specific version. When `None`, the + latest version needs to be installed. + + The backup parameter indicates a backup should be taken before + installing the update. + """ + raise NotImplementedError() + + @property + @final + def state(self) -> str | None: + """Return the entity state.""" + if (current_version := self.current_version) is None or ( + latest_version := self.latest_version + ) is None: + return None + + if latest_version not in (current_version, self.__skipped_version): + return STATE_ON + return STATE_OFF + + @final + @property + def state_attributes(self) -> dict[str, Any] | None: + """Return state attributes.""" + if (release_summary := self.release_summary) is not None: + release_summary = release_summary[:255] + + # If entity supports progress, return the in_progress value. + # Otherwise, we use the internal progress value. + if self.supported_features & UpdateEntityFeature.PROGRESS: + in_progress = self.in_progress + else: + in_progress = self.__in_progress + + # Clear skipped version in case it matches the current version or + # the latest version diverged. + if ( + self.__skipped_version == self.current_version + or self.__skipped_version != self.latest_version + ): + self.__skipped_version = None + + return { + ATTR_CURRENT_VERSION: self.current_version, + ATTR_IN_PROGRESS: in_progress, + ATTR_LATEST_VERSION: self.latest_version, + ATTR_RELEASE_SUMMARY: release_summary, + ATTR_RELEASE_URL: self.release_url, + ATTR_SKIPPED_VERSION: self.__skipped_version, + ATTR_TITLE: self.title, + } + + @final + async def async_install_with_progress( + self, + version: str | None = None, + backup: bool | None = None, + ) -> None: + """Install update and handle progress if needed. + + Handles setting the in_progress state in case the entity doesn't + support it natively. + """ + if not self.supported_features & UpdateEntityFeature.PROGRESS: + self.__in_progress = True + self.async_write_ha_state() + + try: + await self.async_install(version, backup) + finally: + # No matter what happens, we always stop progress in the end + self._attr_in_progress = False + self.__in_progress = False + self.async_write_ha_state() + + async def async_internal_added_to_hass(self) -> None: + """Call when the update entity is added to hass. + + It is used to restore the skipped version, if any. + """ + await super().async_internal_added_to_hass() + state = await self.async_get_last_state() + if state is not None and state.attributes.get(ATTR_SKIPPED_VERSION) is not None: + self.__skipped_version = state.attributes[ATTR_SKIPPED_VERSION] diff --git a/homeassistant/components/update/const.py b/homeassistant/components/update/const.py new file mode 100644 index 00000000000..2ddf08a20ff --- /dev/null +++ b/homeassistant/components/update/const.py @@ -0,0 +1,30 @@ +"""Constants for the update component.""" +from __future__ import annotations + +from enum import IntEnum +from typing import Final + +DOMAIN: Final = "update" + + +class UpdateEntityFeature(IntEnum): + """Supported features of the update entity.""" + + INSTALL = 1 + SPECIFIC_VERSION = 2 + PROGRESS = 4 + BACKUP = 8 + + +SERVICE_INSTALL: Final = "install" +SERVICE_SKIP: Final = "skip" + +ATTR_BACKUP: Final = "backup" +ATTR_CURRENT_VERSION: Final = "current_version" +ATTR_IN_PROGRESS: Final = "in_progress" +ATTR_LATEST_VERSION: Final = "latest_version" +ATTR_RELEASE_SUMMARY: Final = "release_summary" +ATTR_RELEASE_URL: Final = "release_url" +ATTR_SKIPPED_VERSION: Final = "skipped_version" +ATTR_TITLE: Final = "title" +ATTR_VERSION: Final = "version" diff --git a/homeassistant/components/update/manifest.json b/homeassistant/components/update/manifest.json new file mode 100644 index 00000000000..f5fe74c9d02 --- /dev/null +++ b/homeassistant/components/update/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "update", + "name": "Update", + "documentation": "https://www.home-assistant.io/integrations/update", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/update/services.yaml b/homeassistant/components/update/services.yaml new file mode 100644 index 00000000000..2a3370493cc --- /dev/null +++ b/homeassistant/components/update/services.yaml @@ -0,0 +1,27 @@ +install: + name: Install update + description: Install an update for this device or service + target: + entity: + domain: update + fields: + version: + name: Version + description: Version to install, if omitted, the latest version will be installed. + required: false + example: "1.0.0" + selector: + text: + backup: + name: Backup + description: Backup before installing the update, if supported by the integration. + required: false + selector: + boolean: + +skip: + name: Skip update + description: Mark currently available update as skipped. + target: + entity: + domain: update diff --git a/homeassistant/components/update/significant_change.py b/homeassistant/components/update/significant_change.py new file mode 100644 index 00000000000..400734f2e43 --- /dev/null +++ b/homeassistant/components/update/significant_change.py @@ -0,0 +1,30 @@ +"""Helper to test significant update state changes.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback + +from .const import ATTR_CURRENT_VERSION, ATTR_LATEST_VERSION + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> bool | None: + """Test if state significantly changed.""" + if old_state != new_state: + return True + + if old_attrs.get(ATTR_CURRENT_VERSION) != new_attrs.get(ATTR_CURRENT_VERSION): + return True + + if old_attrs.get(ATTR_LATEST_VERSION) != new_attrs.get(ATTR_LATEST_VERSION): + return True + + return False diff --git a/homeassistant/components/update/strings.json b/homeassistant/components/update/strings.json new file mode 100644 index 00000000000..b079c9ec8b6 --- /dev/null +++ b/homeassistant/components/update/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Update" +} diff --git a/homeassistant/const.py b/homeassistant/const.py index 4f2f5ed7478..2c2676d8bb4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -50,6 +50,7 @@ class Platform(StrEnum): SWITCH = "switch" TTS = "tts" VACUUM = "vacuum" + UPDATE = "update" WATER_HEATER = "water_heater" WEATHER = "weather" diff --git a/mypy.ini b/mypy.ini index 1a87e77298f..b4d51d7f4e1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2079,6 +2079,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.update.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.uptime.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/demo/test_update.py b/tests/components/demo/test_update.py new file mode 100644 index 00000000000..37e0ea903d1 --- /dev/null +++ b/tests/components/demo/test_update.py @@ -0,0 +1,157 @@ +"""The tests for the demo update platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.update import DOMAIN, SERVICE_INSTALL, UpdateDeviceClass +from homeassistant.components.update.const import ( + ATTR_CURRENT_VERSION, + ATTR_IN_PROGRESS, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_TITLE, +) +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +async def setup_demo_update(hass: HomeAssistant) -> None: + """Initialize setup demo update entity.""" + assert await async_setup_component(hass, DOMAIN, {"update": {"platform": "demo"}}) + await hass.async_block_till_done() + + +def test_setup_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + state = hass.states.get("update.demo_update_no_install") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "Awesomesoft Inc." + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert ( + state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!" + ) + assert state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.0.1" + + state = hass.states.get("update.demo_no_update") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_TITLE] == "AdGuard Home" + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + assert state.attributes[ATTR_RELEASE_URL] is None + + state = hass.states.get("update.demo_add_on") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "AdGuard Home" + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert ( + state.attributes[ATTR_RELEASE_SUMMARY] == "Awesome update, fixing everything!" + ) + assert state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.0.1" + + state = hass.states.get("update.demo_living_room_bulb_update") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware" + assert state.attributes[ATTR_CURRENT_VERSION] == "1.93.3" + assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2" + assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects" + assert ( + state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.93.3" + ) + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + + state = hass.states.get("update.demo_update_with_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_TITLE] == "Philips Lamps Firmware" + assert state.attributes[ATTR_CURRENT_VERSION] == "1.93.3" + assert state.attributes[ATTR_LATEST_VERSION] == "1.94.2" + assert state.attributes[ATTR_RELEASE_SUMMARY] == "Added support for effects" + assert ( + state.attributes[ATTR_RELEASE_URL] == "https://www.example.com/release/1.93.3" + ) + assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + + +async def test_update_with_progress(hass: HomeAssistant) -> None: + """Test update with progress.""" + state = hass.states.get("update.demo_update_with_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_IN_PROGRESS] is False + + events = [] + async_track_state_change_event( + hass, + "update.demo_update_with_progress", + callback(lambda event: events.append(event)), + ) + + with patch("homeassistant.components.demo.update.FAKE_INSTALL_SLEEP_TIME", new=0): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.demo_update_with_progress"}, + blocking=True, + ) + + assert len(events) == 10 + assert events[0].data["new_state"].state == STATE_ON + assert events[0].data["new_state"].attributes[ATTR_IN_PROGRESS] == 10 + assert events[1].data["new_state"].attributes[ATTR_IN_PROGRESS] == 20 + assert events[2].data["new_state"].attributes[ATTR_IN_PROGRESS] == 30 + assert events[3].data["new_state"].attributes[ATTR_IN_PROGRESS] == 40 + assert events[4].data["new_state"].attributes[ATTR_IN_PROGRESS] == 50 + assert events[5].data["new_state"].attributes[ATTR_IN_PROGRESS] == 60 + assert events[6].data["new_state"].attributes[ATTR_IN_PROGRESS] == 70 + assert events[7].data["new_state"].attributes[ATTR_IN_PROGRESS] == 80 + assert events[8].data["new_state"].attributes[ATTR_IN_PROGRESS] == 90 + assert events[9].data["new_state"].attributes[ATTR_IN_PROGRESS] is False + assert events[9].data["new_state"].state == STATE_OFF + + +async def test_update_with_progress_raising(hass: HomeAssistant) -> None: + """Test update with progress failing to install.""" + state = hass.states.get("update.demo_update_with_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_IN_PROGRESS] is False + + events = [] + async_track_state_change_event( + hass, + "update.demo_update_with_progress", + callback(lambda event: events.append(event)), + ) + + with patch( + "homeassistant.components.demo.update._fake_install", + side_effect=[None, None, None, None, RuntimeError], + ) as fake_sleep, pytest.raises(RuntimeError): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.demo_update_with_progress"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert fake_sleep.call_count == 5 + assert len(events) == 5 + assert events[0].data["new_state"].state == STATE_ON + assert events[0].data["new_state"].attributes[ATTR_IN_PROGRESS] == 10 + assert events[1].data["new_state"].attributes[ATTR_IN_PROGRESS] == 20 + assert events[2].data["new_state"].attributes[ATTR_IN_PROGRESS] == 30 + assert events[3].data["new_state"].attributes[ATTR_IN_PROGRESS] == 40 + assert events[4].data["new_state"].attributes[ATTR_IN_PROGRESS] is False + assert events[4].data["new_state"].state == STATE_ON diff --git a/tests/components/update/__init__.py b/tests/components/update/__init__.py new file mode 100644 index 00000000000..c711b2779b9 --- /dev/null +++ b/tests/components/update/__init__.py @@ -0,0 +1 @@ +"""The tests for the Update integration.""" diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py new file mode 100644 index 00000000000..cc8315c5aec --- /dev/null +++ b/tests/components/update/test_init.py @@ -0,0 +1,583 @@ +"""The tests for the Update component.""" +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components.update import ( + ATTR_BACKUP, + ATTR_VERSION, + DOMAIN, + SERVICE_INSTALL, + SERVICE_SKIP, + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) +from homeassistant.components.update.const import ( + ATTR_CURRENT_VERSION, + ATTR_IN_PROGRESS, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, + ATTR_TITLE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_PLATFORM, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.setup import async_setup_component + +from tests.common import mock_restore_cache + + +class MockUpdateEntity(UpdateEntity): + """Mock UpdateEntity to use in tests.""" + + +async def test_update(hass: HomeAssistant) -> None: + """Test getting data from the mocked update entity.""" + update = MockUpdateEntity() + update.hass = hass + + update._attr_current_version = "1.0.0" + update._attr_latest_version = "1.0.1" + update._attr_release_summary = "Summary" + update._attr_release_url = "https://example.com" + update._attr_title = "Title" + + assert update.entity_category is EntityCategory.CONFIG + assert update.current_version == "1.0.0" + assert update.latest_version == "1.0.1" + assert update.release_summary == "Summary" + assert update.release_url == "https://example.com" + assert update.title == "Title" + assert update.in_progress is False + assert update.state == STATE_ON + assert update.state_attributes == { + ATTR_CURRENT_VERSION: "1.0.0", + ATTR_IN_PROGRESS: False, + ATTR_LATEST_VERSION: "1.0.1", + ATTR_RELEASE_SUMMARY: "Summary", + ATTR_RELEASE_URL: "https://example.com", + ATTR_SKIPPED_VERSION: None, + ATTR_TITLE: "Title", + } + + # Test no update available + update._attr_current_version = "1.0.0" + update._attr_latest_version = "1.0.0" + assert update.state is STATE_OFF + + # Test state becomes unknown if current version is unknown + update._attr_current_version = None + update._attr_latest_version = "1.0.0" + assert update.state is None + + # Test state becomes unknown if latest version is unknown + update._attr_current_version = "1.0.0" + update._attr_latest_version = None + assert update.state is None + + # UpdateEntityDescription was set + update.entity_description = UpdateEntityDescription(key="F5 - Its very refreshing") + assert update.device_class is None + assert update.entity_category is EntityCategory.CONFIG + update.entity_description = UpdateEntityDescription( + key="F5 - Its very refreshing", + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=None, + ) + assert update.device_class is UpdateDeviceClass.FIRMWARE + assert update.entity_category is None + + # Device class via attribute (override entity description) + update._attr_device_class = None + assert update.device_class is None + update._attr_device_class = UpdateDeviceClass.FIRMWARE + assert update.device_class is UpdateDeviceClass.FIRMWARE + + # Entity Attribute via attribute (override entity description) + update._attr_entity_category = None + assert update.entity_category is None + update._attr_entity_category = EntityCategory.DIAGNOSTIC + assert update.entity_category is EntityCategory.DIAGNOSTIC + + with pytest.raises(NotImplementedError): + await update.async_install() + + with pytest.raises(NotImplementedError): + update.install() + + update.install = MagicMock() + await update.async_install(version="1.0.1", backup=True) + + assert update.install.called + assert update.install.call_args[0][0] == "1.0.1" + assert update.install.call_args[0][1] is True + + +async def test_entity_with_no_install( + hass: HomeAssistant, + enable_custom_integrations: None, +) -> None: + """Test entity with no updates.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + # Update is available + state = hass.states.get("update.update_no_install") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + + # Should not be able to install as the entity doesn't support that + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_no_install"}, + blocking=True, + ) + + # Nothing changed + state = hass.states.get("update.update_no_install") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] is None + + # We can mark the update as skipped + await hass.services.async_call( + DOMAIN, + SERVICE_SKIP, + {ATTR_ENTITY_ID: "update.update_no_install"}, + blocking=True, + ) + + state = hass.states.get("update.update_no_install") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" + + +async def test_entity_with_no_updates( + hass: HomeAssistant, + enable_custom_integrations: None, +) -> None: + """Test entity with no updates.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + # No update available + state = hass.states.get("update.no_update") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" + + # Should not be able to skip when there is no update available + with pytest.raises(HomeAssistantError, match="No update available to skip for"): + await hass.services.async_call( + DOMAIN, + SERVICE_SKIP, + {ATTR_ENTITY_ID: "update.no_update"}, + blocking=True, + ) + + # Should not be able to install an update when there is no update available + with pytest.raises(HomeAssistantError, match="No update available for"): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.no_update"}, + blocking=True, + ) + + # Updating to a specific version is not supported by this entity + with pytest.raises( + HomeAssistantError, + match="Installing a specific version is not supported for", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_VERSION: "0.9.0", ATTR_ENTITY_ID: "update.no_update"}, + blocking=True, + ) + + +async def test_entity_with_updates_available( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test basic update entity with updates available.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + # Entity has an update available + state = hass.states.get("update.update_available") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] is None + + # Skip skip the update + await hass.services.async_call( + DOMAIN, + SERVICE_SKIP, + {ATTR_ENTITY_ID: "update.update_available"}, + blocking=True, + ) + + # The state should have changed to off, skipped version should be set + state = hass.states.get("update.update_available") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" + + # Even though skipped, we can still update if we want to + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_available"}, + blocking=True, + ) + + # The state should have changed to off, skipped version should be set + state = hass.states.get("update.update_available") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.1" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] is None + assert "Installed latest update" in caplog.text + + +async def test_entity_with_unknown_version( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity that has an unknown version.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("update.update_unknown") + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] is None + assert state.attributes[ATTR_SKIPPED_VERSION] is None + + # Should not be able to install an update when there is no update available + with pytest.raises(HomeAssistantError, match="No update available for"): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_unknown"}, + blocking=True, + ) + + # Should not be to skip the update + with pytest.raises(HomeAssistantError, match="Cannot skip an unknown version for"): + await hass.services.async_call( + DOMAIN, + SERVICE_SKIP, + {ATTR_ENTITY_ID: "update.update_unknown"}, + blocking=True, + ) + + +async def test_entity_with_specific_version( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity that support specific version.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("update.update_specific_version") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" + + # Update to a specific version + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_VERSION: "0.9.9", ATTR_ENTITY_ID: "update.update_specific_version"}, + blocking=True, + ) + + # Version has changed, state should be on as there is an update available + state = hass.states.get("update.update_specific_version") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "0.9.9" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" + assert "Installed update with version: 0.9.9" in caplog.text + + # Update back to the latest version + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_specific_version"}, + blocking=True, + ) + + state = hass.states.get("update.update_specific_version") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.0" + assert "Installed latest update" in caplog.text + + # This entity does not support doing a backup before upgrade + with pytest.raises(HomeAssistantError, match="Backup is not supported for"): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + { + ATTR_VERSION: "0.9.9", + ATTR_BACKUP: True, + ATTR_ENTITY_ID: "update.update_specific_version", + }, + blocking=True, + ) + + +async def test_entity_with_backup_support( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity with backup support.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + # This entity support backing up before install the update + state = hass.states.get("update.update_backup") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + + # Without a backup + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + { + ATTR_BACKUP: False, + ATTR_ENTITY_ID: "update.update_backup", + }, + blocking=True, + ) + + state = hass.states.get("update.update_backup") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.1" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert "Creating backup before installing update" not in caplog.text + assert "Installed latest update" in caplog.text + + # Specific version, do create a backup this time + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + { + ATTR_BACKUP: True, + ATTR_VERSION: "0.9.8", + ATTR_ENTITY_ID: "update.update_backup", + }, + blocking=True, + ) + + # This entity support backing up before install the update + state = hass.states.get("update.update_backup") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "0.9.8" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert "Creating backup before installing update" in caplog.text + assert "Installed update with version: 0.9.8" in caplog.text + + +async def test_entity_already_in_progress( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update install already in progress.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("update.update_already_in_progress") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_IN_PROGRESS] == 50 + + with pytest.raises( + HomeAssistantError, + match="Update installation already in progress for", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_already_in_progress"}, + blocking=True, + ) + + +async def test_entity_without_progress_support( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity without progress support. + + In that case, progress is still handled by Home Assistant. + """ + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + events = [] + async_track_state_change_event( + hass, "update.update_available", callback(lambda event: events.append(event)) + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_available"}, + blocking=True, + ) + + assert len(events) == 2 + assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False + assert events[0].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[0].data.get("new_state").attributes[ATTR_IN_PROGRESS] is True + assert events[0].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + + assert events[1].data.get("old_state").attributes[ATTR_IN_PROGRESS] is True + assert events[1].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[1].data.get("new_state").attributes[ATTR_IN_PROGRESS] is False + assert events[1].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.1" + + +async def test_entity_without_progress_support_raising( + hass: HomeAssistant, + enable_custom_integrations: None, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity without progress support that raises during install. + + In that case, progress is still handled by Home Assistant. + """ + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + events = [] + async_track_state_change_event( + hass, "update.update_available", callback(lambda event: events.append(event)) + ) + + with patch( + "homeassistant.components.update.UpdateEntity.async_install", + side_effect=RuntimeError, + ), pytest.raises(RuntimeError): + await hass.services.async_call( + DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.update_available"}, + blocking=True, + ) + + assert len(events) == 2 + assert events[0].data.get("old_state").attributes[ATTR_IN_PROGRESS] is False + assert events[0].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[0].data.get("new_state").attributes[ATTR_IN_PROGRESS] is True + assert events[0].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + + assert events[1].data.get("old_state").attributes[ATTR_IN_PROGRESS] is True + assert events[1].data.get("old_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert events[1].data.get("new_state").attributes[ATTR_IN_PROGRESS] is False + assert events[1].data.get("new_state").attributes[ATTR_CURRENT_VERSION] == "1.0.0" + + +async def test_restore_state( + hass: HomeAssistant, enable_custom_integrations: None +) -> None: + """Test we restore skipped version state.""" + mock_restore_cache( + hass, + ( + State( + "update.update_available", + STATE_ON, # Incorrect, but helps checking if it is ignored + { + ATTR_SKIPPED_VERSION: "1.0.1", + }, + ), + ), + ) + + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("update.update_available") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_CURRENT_VERSION] == "1.0.0" + assert state.attributes[ATTR_LATEST_VERSION] == "1.0.1" + assert state.attributes[ATTR_SKIPPED_VERSION] == "1.0.1" diff --git a/tests/components/update/test_significant_change.py b/tests/components/update/test_significant_change.py new file mode 100644 index 00000000000..699d3e60f57 --- /dev/null +++ b/tests/components/update/test_significant_change.py @@ -0,0 +1,90 @@ +"""Test the update significant change platform.""" +from homeassistant.components.update.const import ( + ATTR_CURRENT_VERSION, + ATTR_IN_PROGRESS, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_SKIPPED_VERSION, + ATTR_TITLE, +) +from homeassistant.components.update.significant_change import ( + async_check_significant_change, +) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + + +async def test_significant_change(hass: HomeAssistant) -> None: + """Detect update significant changes.""" + assert async_check_significant_change(hass, STATE_ON, {}, STATE_OFF, {}) + assert async_check_significant_change(hass, STATE_OFF, {}, STATE_ON, {}) + assert not async_check_significant_change(hass, STATE_OFF, {}, STATE_OFF, {}) + assert not async_check_significant_change(hass, STATE_ON, {}, STATE_ON, {}) + + attrs = { + ATTR_CURRENT_VERSION: "1.0.0", + ATTR_IN_PROGRESS: False, + ATTR_LATEST_VERSION: "1.0.1", + ATTR_RELEASE_SUMMARY: "Fixes!", + ATTR_RELEASE_URL: "https://www.example.com", + ATTR_SKIPPED_VERSION: None, + ATTR_TITLE: "Piece of Software", + } + assert not async_check_significant_change(hass, STATE_ON, attrs, STATE_ON, attrs) + + assert async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_CURRENT_VERSION: "1.0.1"}, + ) + + assert async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_LATEST_VERSION: "1.0.2"}, + ) + + assert not async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_IN_PROGRESS: True}, + ) + + assert not async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_RELEASE_SUMMARY: "More fixes!"}, + ) + + assert not async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_RELEASE_URL: "https://www.example.com/changed_url"}, + ) + + assert not async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_SKIPPED_VERSION: "1.0.0"}, + ) + + assert not async_check_significant_change( + hass, + STATE_ON, + attrs, + STATE_ON, + attrs.copy() | {ATTR_TITLE: "Renamed the software..."}, + ) diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py new file mode 100644 index 00000000000..aeac37d198e --- /dev/null +++ b/tests/testing_config/custom_components/test/update.py @@ -0,0 +1,138 @@ +""" +Provide a mock update platform. + +Call init before using it in your tests to ensure clean test data. +""" +from __future__ import annotations + +import logging + +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature + +from tests.common import MockEntity + +ENTITIES = [] + +_LOGGER = logging.getLogger(__name__) + + +class MockUpdateEntity(MockEntity, UpdateEntity): + """Mock UpdateEntity class.""" + + @property + def current_version(self) -> str | None: + """Version currently in use.""" + return self._handle("current_version") + + @property + def in_progress(self) -> bool | int | None: + """Update installation progress.""" + return self._handle("in_progress") + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self._handle("latest_version") + + @property + def release_summary(self) -> str | None: + """Summary of the release notes or changelog.""" + return self._handle("release_summary") + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return self._handle("release_url") + + @property + def title(self) -> str | None: + """Title of the software.""" + return self._handle("title") + + def install( + self, + version: str | None = None, + backup: bool | None = None, + ) -> None: + """Install an update.""" + if backup: + _LOGGER.info("Creating backup before installing update") + + if version is not None: + self._values["current_version"] = version + _LOGGER.info(f"Installed update with version: {version}") + else: + self._values["current_version"] = self.latest_version + _LOGGER.info("Installed latest update") + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockUpdateEntity( + name="No Update", + unique_id="no_update", + current_version="1.0.0", + latest_version="1.0.0", + supported_features=UpdateEntityFeature.INSTALL, + ), + MockUpdateEntity( + name="Update Available", + unique_id="update_available", + current_version="1.0.0", + latest_version="1.0.1", + supported_features=UpdateEntityFeature.INSTALL, + ), + MockUpdateEntity( + name="Update Unknown", + unique_id="update_unknown", + current_version="1.0.0", + latest_version=None, + supported_features=UpdateEntityFeature.INSTALL, + ), + MockUpdateEntity( + name="Update Specific Version", + unique_id="update_specific_version", + current_version="1.0.0", + latest_version="1.0.0", + supported_features=UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION, + ), + MockUpdateEntity( + name="Update Backup", + unique_id="update_backup", + current_version="1.0.0", + latest_version="1.0.1", + supported_features=UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.BACKUP, + ), + MockUpdateEntity( + name="Update Already in Progress", + unique_id="update_already_in_progres", + current_version="1.0.0", + latest_version="1.0.1", + in_progress=50, + supported_features=UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS, + ), + MockUpdateEntity( + name="Update No Install", + unique_id="no_install", + current_version="1.0.0", + latest_version="1.0.1", + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) From 354fc4c1aed51b4f1a9d72257906b550471255d9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:13:16 +0100 Subject: [PATCH 0576/1054] Update coordinator typing (2) [e-f] (#68462) --- homeassistant/components/eight_sleep/__init__.py | 7 ++++++- homeassistant/components/elgato/light.py | 8 ++++---- homeassistant/components/elmax/common.py | 4 +--- .../components/evil_genius_labs/__init__.py | 16 +++++++--------- homeassistant/components/ezviz/binary_sensor.py | 2 -- homeassistant/components/ezviz/camera.py | 2 -- homeassistant/components/ezviz/entity.py | 2 +- homeassistant/components/ezviz/switch.py | 1 - homeassistant/components/fivem/__init__.py | 3 +-- homeassistant/components/flux_led/entity.py | 4 +--- homeassistant/components/flux_led/light.py | 4 +++- homeassistant/components/flux_led/number.py | 8 ++++++-- homeassistant/components/flux_led/switch.py | 4 +++- homeassistant/components/fritz/common.py | 2 +- homeassistant/components/fritzbox/__init__.py | 4 +--- homeassistant/components/fronius/sensor.py | 3 +-- 16 files changed, 36 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 3c08a75448f..6d99ba59735 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Union from pyeight.eight import EightSleep from pyeight.user import EightUser @@ -227,7 +228,11 @@ class EightSleepUserDataCoordinator(DataUpdateCoordinator): await self.api.update_user_data() -class EightSleepBaseEntity(CoordinatorEntity): +class EightSleepBaseEntity( + CoordinatorEntity[ + Union[EightSleepUserDataCoordinator, EightSleepHeatDataCoordinator] + ] +): """The base Eight Sleep entity class.""" def __init__( diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index cc3dd93d9c4..d9f73963870 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -61,14 +61,14 @@ async def async_setup_entry( ) -class ElgatoLight(ElgatoEntity, CoordinatorEntity, LightEntity): +class ElgatoLight( + ElgatoEntity, CoordinatorEntity[DataUpdateCoordinator[State]], LightEntity +): """Defines an Elgato Light.""" - coordinator: DataUpdateCoordinator[State] - def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[State], client: Elgato, info: Info, mac: str | None, diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index e9854cc5d7a..2d66ca9f72e 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -116,11 +116,9 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): ) from err -class ElmaxEntity(CoordinatorEntity): +class ElmaxEntity(CoordinatorEntity[ElmaxCoordinator]): """Wrapper for Elmax entities.""" - coordinator: ElmaxCoordinator - def __init__( self, panel: PanelEntry, diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index 2fbd85938f0..ff858db566a 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -11,12 +11,12 @@ import pyevilgenius from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - aiohttp_client, - device_registry as dr, - update_coordinator, -) +from homeassistant.helpers import aiohttp_client, device_registry as dr from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN @@ -49,7 +49,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class EvilGeniusUpdateCoordinator(update_coordinator.DataUpdateCoordinator[dict]): +class EvilGeniusUpdateCoordinator(DataUpdateCoordinator[dict]): """Update coordinator for Evil Genius data.""" info: dict @@ -81,11 +81,9 @@ class EvilGeniusUpdateCoordinator(update_coordinator.DataUpdateCoordinator[dict] return cast(dict, await self.client.get_data()) -class EvilGeniusEntity(update_coordinator.CoordinatorEntity): +class EvilGeniusEntity(CoordinatorEntity[EvilGeniusUpdateCoordinator]): """Base entity for Evil Genius.""" - coordinator: EvilGeniusUpdateCoordinator - @property def device_info(self) -> DeviceInfo: """Return device info.""" diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 942ceeecdb2..43ac914a50c 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -54,8 +54,6 @@ async def async_setup_entry( class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """Representation of a Ezviz sensor.""" - coordinator: EzvizDataUpdateCoordinator - def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 6680466ecf0..58b45eeafd3 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -228,8 +228,6 @@ async def async_setup_entry( class EzvizCamera(EzvizEntity, Camera): """An implementation of a Ezviz security camera.""" - coordinator: EzvizDataUpdateCoordinator - def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 288c4a5d9eb..2ab42a93286 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -10,7 +10,7 @@ from .const import DOMAIN, MANUFACTURER from .coordinator import EzvizDataUpdateCoordinator -class EzvizEntity(CoordinatorEntity, Entity): +class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): """Generic entity encapsulating common features of Ezviz device.""" def __init__( diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index ea8f1e83f70..c2e562f62da 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -39,7 +39,6 @@ async def async_setup_entry( class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a Ezviz sensor.""" - coordinator: EzvizDataUpdateCoordinator _attr_device_class = SwitchDeviceClass.SWITCH def __init__( diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 2004aacd165..1fe5ccf0b8f 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -128,10 +128,9 @@ class FiveMEntityDescription(EntityDescription): extra_attrs: list[str] | None = None -class FiveMEntity(CoordinatorEntity): +class FiveMEntity(CoordinatorEntity[FiveMDataUpdateCoordinator]): """Representation of a FiveM base entity.""" - coordinator: FiveMDataUpdateCoordinator entity_description: FiveMEntityDescription def __init__( diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index da92931d1e6..6a77dba948c 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -66,11 +66,9 @@ class FluxBaseEntity(Entity): self._attr_device_info = _async_device_info(self._device, entry) -class FluxEntity(CoordinatorEntity): +class FluxEntity(CoordinatorEntity[FluxLedUpdateCoordinator]): """Representation of a Flux entity with a coordinator.""" - coordinator: FluxLedUpdateCoordinator - def __init__( self, coordinator: FluxLedUpdateCoordinator, diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0d179cd2b77..2942a13c734 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -187,7 +187,9 @@ async def async_setup_entry( ) -class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity): +class FluxLight( + FluxOnOffEntity, CoordinatorEntity[FluxLedUpdateCoordinator], LightEntity +): """Representation of a Flux light.""" _attr_supported_features = SUPPORT_TRANSITION | SUPPORT_EFFECT diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index b4e6e87a829..06f706aee21 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -92,7 +92,9 @@ async def async_setup_entry( async_add_entities(entities) -class FluxSpeedNumber(FluxEntity, CoordinatorEntity, NumberEntity): +class FluxSpeedNumber( + FluxEntity, CoordinatorEntity[FluxLedUpdateCoordinator], NumberEntity +): """Defines a flux_led speed number.""" _attr_min_value = 1 @@ -122,7 +124,9 @@ class FluxSpeedNumber(FluxEntity, CoordinatorEntity, NumberEntity): await self.coordinator.async_request_refresh() -class FluxConfigNumber(FluxEntity, CoordinatorEntity, NumberEntity): +class FluxConfigNumber( + FluxEntity, CoordinatorEntity[FluxLedUpdateCoordinator], NumberEntity +): """Base class for flux config numbers.""" _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py index e8c34f12b11..18b079beff9 100644 --- a/homeassistant/components/flux_led/switch.py +++ b/homeassistant/components/flux_led/switch.py @@ -52,7 +52,9 @@ async def async_setup_entry( async_add_entities(entities) -class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity): +class FluxSwitch( + FluxOnOffEntity, CoordinatorEntity[FluxLedUpdateCoordinator], SwitchEntity +): """Representation of a Flux switch.""" async def _async_turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 21039d45afa..514697a32f9 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -817,7 +817,7 @@ class FritzData: profile_switches: dict = field(default_factory=dict) -class FritzDeviceBase(update_coordinator.CoordinatorEntity): +class FritzDeviceBase(update_coordinator.CoordinatorEntity[AvmWrapper]): """Entity base class for a device connected to a FRITZ!Box device.""" def __init__(self, avm_wrapper: AvmWrapper, device: FritzDevice) -> None: diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index e5ef4536bc4..7bb71e52560 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -93,11 +93,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class FritzBoxEntity(CoordinatorEntity): +class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator]): """Basis FritzBox entity.""" - coordinator: FritzboxDataUpdateCoordinator - def __init__( self, coordinator: FritzboxDataUpdateCoordinator, diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index a266998a9b5..0e00bbd135e 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -691,10 +691,9 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ] -class _FroniusSensorEntity(CoordinatorEntity, SensorEntity): +class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEntity): """Defines a Fronius coordinator entity.""" - coordinator: FroniusCoordinatorBase entity_descriptions: list[SensorEntityDescription] _entity_id_prefix: str From 0d29b7cbb3bbd9574d48b9f990d00ca7aeeed242 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:14:46 +0100 Subject: [PATCH 0577/1054] Update coordinator typing (3) [g-n] (#68463) --- homeassistant/components/gios/sensor.py | 3 +-- homeassistant/components/gogogate2/common.py | 2 +- homeassistant/components/gree/entity.py | 2 +- homeassistant/components/intellifire/entity.py | 3 +-- homeassistant/components/iotawatt/sensor.py | 6 +++--- homeassistant/components/ipp/entity.py | 2 +- homeassistant/components/launch_library/sensor.py | 5 +++-- homeassistant/components/lookin/entity.py | 12 ++++++------ .../components/moehlenhoff_alpha2/climate.py | 3 +-- homeassistant/components/nam/button.py | 4 +--- homeassistant/components/nam/sensor.py | 4 +--- homeassistant/components/nina/binary_sensor.py | 2 +- homeassistant/components/nsw_fuel_station/sensor.py | 4 +++- homeassistant/components/nzbget/__init__.py | 2 +- 14 files changed, 25 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index ff589d34791..c14a99051e4 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -69,10 +69,9 @@ async def async_setup_entry( async_add_entities(sensors) -class GiosSensor(CoordinatorEntity, SensorEntity): +class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): """Define an GIOS sensor.""" - coordinator: GiosDataUpdateCoordinator entity_description: GiosSensorEntityDescription def __init__( diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 5d0392e6db2..bfbadf86ee2 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -66,7 +66,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): self.api = api -class GoGoGate2Entity(CoordinatorEntity): +class GoGoGate2Entity(CoordinatorEntity[DeviceDataUpdateCoordinator]): """Base class for gogogate2 entities.""" def __init__( diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index 7407a90b4d0..66be66f9dc9 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -7,7 +7,7 @@ from .bridge import DeviceDataUpdateCoordinator from .const import DOMAIN -class GreeEntity(CoordinatorEntity): +class GreeEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]): """Generic Gree entity (base class).""" def __init__(self, coordinator: DeviceDataUpdateCoordinator, desc: str) -> None: diff --git a/homeassistant/components/intellifire/entity.py b/homeassistant/components/intellifire/entity.py index eeb5e7b51bd..6d20c015ab9 100644 --- a/homeassistant/components/intellifire/entity.py +++ b/homeassistant/components/intellifire/entity.py @@ -7,10 +7,9 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import IntellifireDataUpdateCoordinator -class IntellifireEntity(CoordinatorEntity): +class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]): """Define a generic class for Intellifire entities.""" - coordinator: IntellifireDataUpdateCoordinator _attr_attribution = "Data provided by unpublished Intellifire API" def __init__( diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index c3c173f778e..8bcf5bfae9a 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -24,11 +24,12 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity, entity_registry, update_coordinator +from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt from .const import ( @@ -159,11 +160,10 @@ async def async_setup_entry( coordinator.async_add_listener(new_data_received) -class IotaWattSensor(update_coordinator.CoordinatorEntity, SensorEntity): +class IotaWattSensor(CoordinatorEntity[IotawattUpdater], SensorEntity): """Defines a IoTaWatt Energy Sensor.""" entity_description: IotaWattSensorEntityDescription - coordinator: IotawattUpdater def __init__( self, diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index 7bd01b4cd12..b2f3a4a1469 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -8,7 +8,7 @@ from .const import DOMAIN from .coordinator import IPPDataUpdateCoordinator -class IPPEntity(CoordinatorEntity): +class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]): """Defines a base IPP entity.""" def __init__( diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index d468c3a653f..fac62c9bb87 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -179,13 +179,14 @@ async def async_setup_entry( ) -class LaunchLibrarySensor(CoordinatorEntity, SensorEntity): +class LaunchLibrarySensor( + CoordinatorEntity[DataUpdateCoordinator[LaunchLibraryData]], SensorEntity +): """Representation of the next launch sensors.""" _attr_attribution = "Data provided by Launch Library." _next_event: Launch | Event | None = None entity_description: LaunchLibrarySensorEntityDescription - coordinator: DataUpdateCoordinator[LaunchLibraryData] def __init__( self, diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index 6ff167d86fe..1c641b76f32 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -52,11 +52,11 @@ class LookinDeviceMixIn: self._lookin_udp_subs = lookin_data.lookin_udp_subs -class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity): +class LookinDeviceCoordinatorEntity( + LookinDeviceMixIn, CoordinatorEntity[LookinDataUpdateCoordinator] +): """A lookin device entity on the device itself that uses the coordinator.""" - coordinator: LookinDataUpdateCoordinator - _attr_should_poll = False def __init__(self, lookin_data: LookinData) -> None: @@ -84,11 +84,11 @@ class LookinEntityMixIn: self._function_names = {function.name for function in self._device.functions} -class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity): +class LookinCoordinatorEntity( + LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity[LookinDataUpdateCoordinator] +): """A lookin device entity for an external device that uses the coordinator.""" - coordinator: LookinDataUpdateCoordinator - _attr_should_poll = False _attr_assumed_state = True diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index d99eb0e4c8c..783e2df5967 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -38,10 +38,9 @@ async def async_setup_entry( # https://developers.home-assistant.io/docs/core/entity/climate/ -class Alpha2Climate(CoordinatorEntity, ClimateEntity): +class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): """Alpha2 ClimateEntity.""" - coordinator: Alpha2BaseCoordinator target_temperature_step = 0.2 _attr_supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index f73307d09ce..db5474ec925 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -41,11 +41,9 @@ async def async_setup_entry( async_add_entities(buttons, False) -class NAMButton(CoordinatorEntity, ButtonEntity): +class NAMButton(CoordinatorEntity[NAMDataUpdateCoordinator], ButtonEntity): """Define an Nettigo Air Monitor button.""" - coordinator: NAMDataUpdateCoordinator - def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 88f6008b45f..af729cf9066 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -58,11 +58,9 @@ async def async_setup_entry( async_add_entities(sensors, False) -class NAMSensor(CoordinatorEntity, SensorEntity): +class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" - coordinator: NAMDataUpdateCoordinator - def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index eca6668d0f5..a4277b2908d 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -46,7 +46,7 @@ async def async_setup_entry( async_add_entities(entities) -class NINAMessage(CoordinatorEntity, BinarySensorEntity): +class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEntity): """Representation of an NINA warning.""" def __init__( diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 2e022a7ec13..82a5e3a59a8 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -83,7 +83,9 @@ def setup_platform( add_entities(entities) -class StationPriceSensor(CoordinatorEntity, SensorEntity): +class StationPriceSensor( + CoordinatorEntity[DataUpdateCoordinator[StationPriceData]], SensorEntity +): """Implementation of a sensor that reports the fuel price for a station.""" def __init__( diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index d9a41c90535..cb906495d58 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -154,7 +154,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) -class NZBGetEntity(CoordinatorEntity): +class NZBGetEntity(CoordinatorEntity[NZBGetDataUpdateCoordinator]): """Defines a base NZBGet entity.""" def __init__( From add741d7896c282411b88e62d994ff6dbc1ee13f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:20:35 +0100 Subject: [PATCH 0578/1054] Update coordinator typing (6) [t-v] (#68466) --- homeassistant/components/tautulli/sensor.py | 4 +--- homeassistant/components/tolo/__init__.py | 4 +--- .../components/tomorrowio/__init__.py | 2 +- homeassistant/components/toon/models.py | 4 +--- homeassistant/components/tplink/entity.py | 4 +--- homeassistant/components/tplink/light.py | 1 - homeassistant/components/tplink/sensor.py | 1 - homeassistant/components/tplink/switch.py | 3 --- homeassistant/components/tradfri/base_class.py | 4 +--- homeassistant/components/tradfri/light.py | 2 +- homeassistant/components/upcloud/__init__.py | 2 +- homeassistant/components/upnp/__init__.py | 3 +-- homeassistant/components/uptimerobot/entity.py | 3 +-- homeassistant/components/vallox/__init__.py | 4 +--- .../components/vallox/binary_sensor.py | 5 +++-- homeassistant/components/vallox/fan.py | 4 +--- homeassistant/components/vallox/sensor.py | 3 +-- homeassistant/components/venstar/__init__.py | 4 +--- .../components/verisure/alarm_control_panel.py | 6 +++--- .../components/verisure/binary_sensor.py | 12 ++++++------ homeassistant/components/verisure/camera.py | 4 +--- homeassistant/components/verisure/lock.py | 4 +--- homeassistant/components/verisure/sensor.py | 18 +++++++++--------- homeassistant/components/verisure/switch.py | 4 +--- homeassistant/components/version/entity.py | 4 +--- 25 files changed, 39 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 814f6c9da50..f28e334e1c0 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -94,11 +94,9 @@ async def async_setup_platform( ) -class TautulliSensor(CoordinatorEntity, SensorEntity): +class TautulliSensor(CoordinatorEntity[TautulliDataUpdateCoordinator], SensorEntity): """Representation of a Tautulli sensor.""" - coordinator: TautulliDataUpdateCoordinator - def __init__( self, coordinator: TautulliDataUpdateCoordinator, diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 6f17379dfbf..3b78adacbac 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -92,11 +92,9 @@ class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): return ToloSaunaData(status, settings) -class ToloSaunaCoordinatorEntity(CoordinatorEntity): +class ToloSaunaCoordinatorEntity(CoordinatorEntity[ToloSaunaUpdateCoordinator]): """CoordinatorEntity for TOLO Sauna.""" - coordinator: ToloSaunaUpdateCoordinator - def __init__( self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry ) -> None: diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index a46f9a9222e..68070164312 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -316,7 +316,7 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed from error -class TomorrowioEntity(CoordinatorEntity): +class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]): """Base Tomorrow.io Entity.""" def __init__( diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 57db44beb6b..e39faa1efc6 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -10,11 +10,9 @@ from .const import DOMAIN from .coordinator import ToonDataUpdateCoordinator -class ToonEntity(CoordinatorEntity): +class ToonEntity(CoordinatorEntity[ToonDataUpdateCoordinator]): """Defines a base Toon entity.""" - coordinator: ToonDataUpdateCoordinator - class ToonDisplayDeviceEntity(ToonEntity): """Defines a Toon display device entity.""" diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 4380b1397b6..173d1d7930f 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -30,11 +30,9 @@ def async_refresh_after( return _async_wrap -class CoordinatedTPLinkEntity(CoordinatorEntity): +class CoordinatedTPLinkEntity(CoordinatorEntity[TPLinkDataUpdateCoordinator]): """Common base class for all coordinated tplink entities.""" - coordinator: TPLinkDataUpdateCoordinator - def __init__( self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 6efabe537f7..30d7fbde40a 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -49,7 +49,6 @@ async def async_setup_entry( class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Representation of a TPLink Smart Bulb.""" - coordinator: TPLinkDataUpdateCoordinator device: SmartBulb def __init__( diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index fe9cb699114..7ba28702114 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -140,7 +140,6 @@ async def async_setup_entry( class SmartPlugSensor(CoordinatedTPLinkEntity, SensorEntity): """Representation of a TPLink Smart Plug energy sensor.""" - coordinator: TPLinkDataUpdateCoordinator entity_description: TPLinkSensorEntityDescription def __init__( diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 451ec6d5f8b..2b53c67d296 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -47,7 +47,6 @@ async def async_setup_entry( class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of switch for the LED of a TPLink Smart Plug.""" - coordinator: TPLinkDataUpdateCoordinator device: SmartPlug _attr_entity_category = EntityCategory.CONFIG @@ -85,8 +84,6 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity): class SmartPlugSwitch(CoordinatedTPLinkEntity, SwitchEntity): """Representation of a TPLink Smart Plug switch.""" - coordinator: TPLinkDataUpdateCoordinator - def __init__( self, device: SmartDevice, diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index a2bd28e3868..727e611dca4 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -37,11 +37,9 @@ def handle_error( return wrapper -class TradfriBaseEntity(CoordinatorEntity): +class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]): """Base Tradfri device.""" - coordinator: TradfriDeviceDataUpdateCoordinator - def __init__( self, device_coordinator: TradfriDeviceDataUpdateCoordinator, diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 9b6ad3e9f06..bae626017e7 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -78,7 +78,7 @@ async def async_setup_entry( async_add_entities(entities) -class TradfriGroup(CoordinatorEntity, LightEntity): +class TradfriGroup(CoordinatorEntity[TradfriGroupDataUpdateCoordinator], LightEntity): """The platform class for light groups required by hass.""" _attr_supported_features = SUPPORTED_GROUP_FEATURES diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index a824bc596d0..e42659948d8 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -174,7 +174,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -class UpCloudServerEntity(CoordinatorEntity): +class UpCloudServerEntity(CoordinatorEntity[UpCloudDataUpdateCoordinator]): """Entity class for UpCloud servers.""" def __init__( diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index c7988fe98c9..e15c90c7079 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -235,10 +235,9 @@ class UpnpDataUpdateCoordinator(DataUpdateCoordinator): } -class UpnpEntity(CoordinatorEntity): +class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]): """Base class for UPnP/IGD entities.""" - coordinator: UpnpDataUpdateCoordinator entity_description: UpnpSensorEntityDescription | UpnpBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 6f7c616b7a4..7991525c2a0 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -11,11 +11,10 @@ from . import UptimeRobotDataUpdateCoordinator from .const import ATTR_TARGET, ATTRIBUTION, DOMAIN -class UptimeRobotEntity(CoordinatorEntity): +class UptimeRobotEntity(CoordinatorEntity[UptimeRobotDataUpdateCoordinator]): """Base UptimeRobot entity.""" _attr_attribution = ATTRIBUTION - coordinator: UptimeRobotDataUpdateCoordinator def __init__( self, diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index aeb9e59e286..23e5cb53f97 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -131,11 +131,9 @@ class ValloxState: return next_filter_change_date -class ValloxDataUpdateCoordinator(DataUpdateCoordinator): +class ValloxDataUpdateCoordinator(DataUpdateCoordinator[ValloxState]): """The DataUpdateCoordinator for Vallox.""" - data: ValloxState - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the integration from configuration.yaml (DEPRECATED).""" diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index e7b25ca2a80..348bad97158 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -16,11 +16,12 @@ from . import ValloxDataUpdateCoordinator from .const import DOMAIN -class ValloxBinarySensor(CoordinatorEntity, BinarySensorEntity): +class ValloxBinarySensor( + CoordinatorEntity[ValloxDataUpdateCoordinator], BinarySensorEntity +): """Representation of a Vallox binary sensor.""" entity_description: ValloxBinarySensorEntityDescription - coordinator: ValloxDataUpdateCoordinator def __init__( self, diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 21e89f49d41..52535be3a29 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -80,11 +80,9 @@ async def async_setup_entry( async_add_entities([device]) -class ValloxFan(CoordinatorEntity, FanEntity): +class ValloxFan(CoordinatorEntity[ValloxDataUpdateCoordinator], FanEntity): """Representation of the fan.""" - coordinator: ValloxDataUpdateCoordinator - def __init__( self, name: str, diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index eece054c82e..92f0bc32e76 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -32,11 +32,10 @@ from .const import ( ) -class ValloxSensor(CoordinatorEntity, SensorEntity): +class ValloxSensor(CoordinatorEntity[ValloxDataUpdateCoordinator], SensorEntity): """Representation of a Vallox sensor.""" entity_description: ValloxSensorEntityDescription - coordinator: ValloxDataUpdateCoordinator def __init__( self, diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 66a458f210a..759908c87e4 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -126,11 +126,9 @@ class VenstarDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): return None -class VenstarEntity(CoordinatorEntity): +class VenstarEntity(CoordinatorEntity[VenstarDataUpdateCoordinator]): """Representation of a Venstar entity.""" - coordinator: VenstarDataUpdateCoordinator - def __init__( self, venstar_data_coordinator: VenstarDataUpdateCoordinator, diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index e9cb2f49842..4cc5e8f6cb3 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -30,11 +30,11 @@ async def async_setup_entry( async_add_entities([VerisureAlarm(coordinator=hass.data[DOMAIN][entry.entry_id])]) -class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): +class VerisureAlarm( + CoordinatorEntity[VerisureDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of a Verisure alarm status.""" - coordinator: VerisureDataUpdateCoordinator - _attr_code_format = FORMAT_NUMBER _attr_name = "Verisure Alarm" _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 05e0d77845a..217890b8a01 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -33,11 +33,11 @@ async def async_setup_entry( async_add_entities(sensors) -class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): +class VerisureDoorWindowSensor( + CoordinatorEntity[VerisureDataUpdateCoordinator], BinarySensorEntity +): """Representation of a Verisure door window sensor.""" - coordinator: VerisureDataUpdateCoordinator - _attr_device_class = BinarySensorDeviceClass.OPENING def __init__( @@ -79,11 +79,11 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): ) -class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): +class VerisureEthernetStatus( + CoordinatorEntity[VerisureDataUpdateCoordinator], BinarySensorEntity +): """Representation of a Verisure VBOX internet status.""" - coordinator: VerisureDataUpdateCoordinator - _attr_name = "Verisure Ethernet status" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 787e496202f..c753bf2c5dc 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -43,11 +43,9 @@ async def async_setup_entry( ) -class VerisureSmartcam(CoordinatorEntity, Camera): +class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera): """Representation of a Verisure camera.""" - coordinator: VerisureDataUpdateCoordinator - def __init__( self, coordinator: VerisureDataUpdateCoordinator, diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 86b232d54fd..0e28298b2e8 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -55,11 +55,9 @@ async def async_setup_entry( ) -class VerisureDoorlock(CoordinatorEntity, LockEntity): +class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEntity): """Representation of a Verisure doorlock.""" - coordinator: VerisureDataUpdateCoordinator - def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 9e19e5d865f..3b8f722c6f7 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -45,11 +45,11 @@ async def async_setup_entry( async_add_entities(sensors) -class VerisureThermometer(CoordinatorEntity, SensorEntity): +class VerisureThermometer( + CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity +): """Representation of a Verisure thermometer.""" - coordinator: VerisureDataUpdateCoordinator - _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT @@ -100,11 +100,11 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): ) -class VerisureHygrometer(CoordinatorEntity, SensorEntity): +class VerisureHygrometer( + CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity +): """Representation of a Verisure hygrometer.""" - coordinator: VerisureDataUpdateCoordinator - _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT @@ -155,11 +155,11 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): ) -class VerisureMouseDetection(CoordinatorEntity, SensorEntity): +class VerisureMouseDetection( + CoordinatorEntity[VerisureDataUpdateCoordinator], SensorEntity +): """Representation of a Verisure mouse detector.""" - coordinator: VerisureDataUpdateCoordinator - _attr_native_unit_of_measurement = "Mice" def __init__( diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 777195d1a51..5d1fd728f4a 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -27,11 +27,9 @@ async def async_setup_entry( ) -class VerisureSmartplug(CoordinatorEntity, SwitchEntity): +class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], SwitchEntity): """Representation of a Verisure smartplug.""" - coordinator: VerisureDataUpdateCoordinator - def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: diff --git a/homeassistant/components/version/entity.py b/homeassistant/components/version/entity.py index 1dcdc23fa9f..d950c6394b8 100644 --- a/homeassistant/components/version/entity.py +++ b/homeassistant/components/version/entity.py @@ -8,7 +8,7 @@ from .const import DOMAIN, HOME_ASSISTANT from .coordinator import VersionDataUpdateCoordinator -class VersionEntity(CoordinatorEntity): +class VersionEntity(CoordinatorEntity[VersionDataUpdateCoordinator]): """Common entity class for Version integration.""" _attr_device_info = DeviceInfo( @@ -18,8 +18,6 @@ class VersionEntity(CoordinatorEntity): entry_type=DeviceEntryType.SERVICE, ) - coordinator: VersionDataUpdateCoordinator - def __init__( self, coordinator: VersionDataUpdateCoordinator, From 2424564d2cab120654807f29189956e7041d9b24 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Mar 2022 14:35:40 +0100 Subject: [PATCH 0579/1054] Change update default entity category based on features (#68455) --- homeassistant/components/update/__init__.py | 4 +++- tests/components/update/test_init.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index a19ecc535a2..7fb92d8164c 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -186,7 +186,9 @@ class UpdateEntity(RestoreEntity): return self._attr_entity_category if hasattr(self, "entity_description"): return self.entity_description.entity_category - return EntityCategory.CONFIG + if self.supported_features & UpdateEntityFeature.INSTALL: + return EntityCategory.CONFIG + return EntityCategory.DIAGNOSTIC @property def in_progress(self) -> bool | int | None: diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index cc8315c5aec..35366217a3d 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -21,6 +21,7 @@ from homeassistant.components.update.const import ( ATTR_RELEASE_URL, ATTR_SKIPPED_VERSION, ATTR_TITLE, + UpdateEntityFeature, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -53,7 +54,7 @@ async def test_update(hass: HomeAssistant) -> None: update._attr_release_url = "https://example.com" update._attr_title = "Title" - assert update.entity_category is EntityCategory.CONFIG + assert update.entity_category is EntityCategory.DIAGNOSTIC assert update.current_version == "1.0.0" assert update.latest_version == "1.0.1" assert update.release_summary == "Summary" @@ -86,7 +87,12 @@ async def test_update(hass: HomeAssistant) -> None: update._attr_latest_version = None assert update.state is None + # Test entity category becomes config when its possible to install + update._attr_supported_features = UpdateEntityFeature.INSTALL + assert update.entity_category is EntityCategory.CONFIG + # UpdateEntityDescription was set + update._attr_supported_features = 0 update.entity_description = UpdateEntityDescription(key="F5 - Its very refreshing") assert update.device_class is None assert update.entity_category is EntityCategory.CONFIG From b664bcd0070fc19a4c92021c3ce469ae60f4d737 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:45:24 +0100 Subject: [PATCH 0580/1054] Update coordinator typing (4) [o-p] (#68464) --- homeassistant/components/octoprint/binary_sensor.py | 6 +++--- homeassistant/components/octoprint/button.py | 3 +-- homeassistant/components/octoprint/sensor.py | 6 +++--- homeassistant/components/omnilogic/common.py | 2 +- homeassistant/components/open_meteo/weather.py | 10 +++++++--- homeassistant/components/overkiz/entity.py | 4 +--- homeassistant/components/p1_monitor/sensor.py | 6 +++--- homeassistant/components/philips_js/light.py | 4 +++- homeassistant/components/philips_js/media_player.py | 5 +++-- homeassistant/components/philips_js/remote.py | 4 +--- homeassistant/components/philips_js/switch.py | 6 +++--- homeassistant/components/pvoutput/sensor.py | 5 +++-- homeassistant/components/pvpc_hourly_pricing/sensor.py | 4 +--- 13 files changed, 33 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index e1db7a95136..b0e43bd74e0 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -36,11 +36,11 @@ async def async_setup_entry( async_add_entities(entities) -class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): +class OctoPrintBinarySensorBase( + CoordinatorEntity[OctoprintDataUpdateCoordinator], BinarySensorEntity +): """Representation an OctoPrint binary sensor.""" - coordinator: OctoprintDataUpdateCoordinator - def __init__( self, coordinator: OctoprintDataUpdateCoordinator, diff --git a/homeassistant/components/octoprint/button.py b/homeassistant/components/octoprint/button.py index 97676592f47..e16f123a73a 100644 --- a/homeassistant/components/octoprint/button.py +++ b/homeassistant/components/octoprint/button.py @@ -34,10 +34,9 @@ async def async_setup_entry( ) -class OctoprintButton(CoordinatorEntity, ButtonEntity): +class OctoprintButton(CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonEntity): """Represent an OctoPrint binary sensor.""" - coordinator: OctoprintDataUpdateCoordinator client: OctoprintClient def __init__( diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 5a094c10987..4efc094c297 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -89,11 +89,11 @@ async def async_setup_entry( async_add_entities(entities) -class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): +class OctoPrintSensorBase( + CoordinatorEntity[OctoprintDataUpdateCoordinator], SensorEntity +): """Representation of an OctoPrint sensor.""" - coordinator: OctoprintDataUpdateCoordinator - def __init__( self, coordinator: OctoprintDataUpdateCoordinator, diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 78685036e06..4c92420972b 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -73,7 +73,7 @@ class OmniLogicUpdateCoordinator(DataUpdateCoordinator): return parsed_data -class OmniLogicEntity(CoordinatorEntity): +class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): """Defines the base OmniLogic entity.""" def __init__( diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index bb7170bb5da..40b52248a52 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -28,14 +28,18 @@ async def async_setup_entry( async_add_entities([OpenMeteoWeatherEntity(entry=entry, coordinator=coordinator)]) -class OpenMeteoWeatherEntity(CoordinatorEntity, WeatherEntity): +class OpenMeteoWeatherEntity( + CoordinatorEntity[DataUpdateCoordinator[OpenMeteoForecast]], WeatherEntity +): """Defines an Open-Meteo weather entity.""" _attr_temperature_unit = TEMP_CELSIUS - coordinator: DataUpdateCoordinator[OpenMeteoForecast] def __init__( - self, *, entry: ConfigEntry, coordinator: DataUpdateCoordinator + self, + *, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[OpenMeteoForecast], ) -> None: """Initialize Open-Meteo weather entity.""" super().__init__(coordinator=coordinator) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 72ef793c2b4..7c42f415a65 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -16,11 +16,9 @@ from .coordinator import OverkizDataUpdateCoordinator from .executor import OverkizExecutor -class OverkizEntity(CoordinatorEntity): +class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): """Representation of an Overkiz device entity.""" - coordinator: OverkizDataUpdateCoordinator - def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index edc076382ec..57f6b0ad99c 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -235,11 +235,11 @@ async def async_setup_entry( ) -class P1MonitorSensorEntity(CoordinatorEntity, SensorEntity): +class P1MonitorSensorEntity( + CoordinatorEntity[P1MonitorDataUpdateCoordinator], SensorEntity +): """Defines an P1 Monitor sensor.""" - coordinator: P1MonitorDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index fc26fc3ed6a..21bb7199269 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -123,7 +123,9 @@ def _average_pixels(data): return 0.0, 0.0, 0.0 -class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): +class PhilipsTVLightEntity( + CoordinatorEntity[PhilipsTVDataUpdateCoordinator], LightEntity +): """Representation of a Philips TV exposing the JointSpace API.""" def __init__( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 20fa2ced825..77a1d9dccd9 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -79,10 +79,11 @@ async def async_setup_entry( ) -class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): +class PhilipsTVMediaPlayer( + CoordinatorEntity[PhilipsTVDataUpdateCoordinator], MediaPlayerEntity +): """Representation of a Philips TV exposing the JointSpace API.""" - coordinator: PhilipsTVDataUpdateCoordinator _attr_device_class = MediaPlayerDeviceClass.TV def __init__( diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 09fe16215b6..38851964427 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -27,11 +27,9 @@ async def async_setup_entry( async_add_entities([PhilipsTVRemote(coordinator)]) -class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): +class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteEntity): """Device that sends commands.""" - coordinator: PhilipsTVDataUpdateCoordinator - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index 15f72a8aaff..a89c22f1850 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -27,11 +27,11 @@ async def async_setup_entry( async_add_entities([PhilipsTVScreenSwitch(coordinator)]) -class PhilipsTVScreenSwitch(CoordinatorEntity, SwitchEntity): +class PhilipsTVScreenSwitch( + CoordinatorEntity[PhilipsTVDataUpdateCoordinator], SwitchEntity +): """A Philips TV screen state switch.""" - coordinator: PhilipsTVDataUpdateCoordinator - def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 471f4483a47..de108329c45 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -133,10 +133,11 @@ async def async_setup_entry( ) -class PVOutputSensorEntity(CoordinatorEntity, SensorEntity): +class PVOutputSensorEntity( + CoordinatorEntity[PVOutputDataUpdateCoordinator], SensorEntity +): """Representation of a PVOutput sensor.""" - coordinator: PVOutputDataUpdateCoordinator entity_description: PVOutputSensorEntityDescription def __init__( diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 6d9ac9402e6..8cfae034bff 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -125,11 +125,9 @@ async def async_setup_entry( ) -class ElecPriceSensor(CoordinatorEntity, SensorEntity): +class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], SensorEntity): """Class to hold the prices of electricity as a sensor.""" - coordinator: ElecPricesDataUpdateCoordinator - def __init__( self, coordinator: ElecPricesDataUpdateCoordinator, From 539a469a8bcb51235210e3305ba7971a1ed559c3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:24:05 +0100 Subject: [PATCH 0581/1054] Update coordinator typing (5) [r-s] (#68465) --- homeassistant/components/rainforest_eagle/sensor.py | 4 +--- homeassistant/components/rituals_perfume_genie/entity.py | 4 +--- homeassistant/components/roku/entity.py | 4 +--- homeassistant/components/screenlogic/__init__.py | 4 +--- homeassistant/components/sensibo/entity.py | 4 +--- homeassistant/components/sharkiq/vacuum.py | 4 +--- homeassistant/components/solarlog/sensor.py | 4 ++-- homeassistant/components/speedtestdotnet/sensor.py | 5 +++-- homeassistant/components/steamist/entity.py | 4 +--- homeassistant/components/surepetcare/entity.py | 2 +- homeassistant/components/surepetcare/lock.py | 2 -- homeassistant/components/switchbot/binary_sensor.py | 2 -- homeassistant/components/switchbot/cover.py | 1 - homeassistant/components/switchbot/entity.py | 2 +- homeassistant/components/switchbot/sensor.py | 2 -- homeassistant/components/switchbot/switch.py | 1 - homeassistant/components/switcher_kis/sensor.py | 4 +++- homeassistant/components/switcher_kis/switch.py | 4 +++- homeassistant/components/synology_dsm/__init__.py | 4 +++- homeassistant/components/system_bridge/__init__.py | 2 +- homeassistant/components/system_bridge/binary_sensor.py | 1 - homeassistant/components/system_bridge/sensor.py | 1 - 22 files changed, 24 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 8f59888603c..a2c336bc5d0 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -67,11 +67,9 @@ async def async_setup_entry( async_add_entities(entities) -class EagleSensor(CoordinatorEntity, SensorEntity): +class EagleSensor(CoordinatorEntity[EagleDataCoordinator], SensorEntity): """Implementation of the Rainforest Eagle sensor.""" - coordinator: EagleDataCoordinator - def __init__(self, coordinator, entity_description): """Initialize the sensor.""" super().__init__(coordinator) diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 3ad71cdad67..e3bf1ef4e63 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -14,11 +14,9 @@ MODEL = "The Perfume Genie" MODEL2 = "The Perfume Genie 2.0" -class DiffuserEntity(CoordinatorEntity): +class DiffuserEntity(CoordinatorEntity[RitualsDataUpdateCoordinator]): """Representation of a diffuser entity.""" - coordinator: RitualsDataUpdateCoordinator - def __init__( self, diffuser: Diffuser, diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index ef969b846aa..39373c96c6a 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -9,11 +9,9 @@ from . import RokuDataUpdateCoordinator from .const import DOMAIN -class RokuEntity(CoordinatorEntity): +class RokuEntity(CoordinatorEntity[RokuDataUpdateCoordinator]): """Defines a base Roku entity.""" - coordinator: RokuDataUpdateCoordinator - def __init__( self, *, diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 8580ce8f8fc..fe7d8a79afd 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -167,11 +167,9 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(ex) from ex -class ScreenlogicEntity(CoordinatorEntity): +class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): """Base class for all ScreenLogic entities.""" - coordinator: ScreenlogicDataUpdateCoordinator - def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index a2a2bbf3a0e..bc0e2f49a2a 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -14,11 +14,9 @@ from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT from .coordinator import MotionSensor, SensiboDataUpdateCoordinator -class SensiboBaseEntity(CoordinatorEntity): +class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): """Representation of a Sensibo entity.""" - coordinator: SensiboDataUpdateCoordinator - def __init__( self, coordinator: SensiboDataUpdateCoordinator, diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index d9d22eb7a4d..351c0458754 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -84,11 +84,9 @@ async def async_setup_entry( async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) -class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): +class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuumEntity): """Shark IQ vacuum entity.""" - coordinator: SharkIqUpdateCoordinator - def __init__( self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator ) -> None: diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 6269210756c..4180d48cdef 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -2,9 +2,9 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import update_coordinator from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SolarlogData from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription @@ -20,7 +20,7 @@ async def async_setup_entry( ) -class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): +class SolarlogSensor(CoordinatorEntity[SolarlogData], SensorEntity): """Representation of a Sensor.""" entity_description: SolarLogSensorEntityDescription diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index aa4cd72f746..4f16c012fa6 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -43,10 +43,11 @@ async def async_setup_entry( ) -class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): +class SpeedtestSensor( + CoordinatorEntity[SpeedTestDataCoordinator], RestoreEntity, SensorEntity +): """Implementation of a speedtest.net sensor.""" - coordinator: SpeedTestDataCoordinator entity_description: SpeedtestSensorEntityDescription _attr_icon = ICON diff --git a/homeassistant/components/steamist/entity.py b/homeassistant/components/steamist/entity.py index 0a2bc239633..ab104a3074d 100644 --- a/homeassistant/components/steamist/entity.py +++ b/homeassistant/components/steamist/entity.py @@ -13,11 +13,9 @@ from .const import CONF_MODEL from .coordinator import SteamistDataUpdateCoordinator -class SteamistEntity(CoordinatorEntity, Entity): +class SteamistEntity(CoordinatorEntity[SteamistDataUpdateCoordinator], Entity): """Representation of an Steamist entity.""" - coordinator: SteamistDataUpdateCoordinator - def __init__( self, coordinator: SteamistDataUpdateCoordinator, diff --git a/homeassistant/components/surepetcare/entity.py b/homeassistant/components/surepetcare/entity.py index e1faaf07e26..301479c4b95 100644 --- a/homeassistant/components/surepetcare/entity.py +++ b/homeassistant/components/surepetcare/entity.py @@ -13,7 +13,7 @@ from . import SurePetcareDataCoordinator from .const import DOMAIN -class SurePetcareEntity(CoordinatorEntity): +class SurePetcareEntity(CoordinatorEntity[SurePetcareDataCoordinator]): """An implementation for Sure Petcare Entities.""" def __init__( diff --git a/homeassistant/components/surepetcare/lock.py b/homeassistant/components/surepetcare/lock.py index 8ebdd9958f0..3161fa6e0bc 100644 --- a/homeassistant/components/surepetcare/lock.py +++ b/homeassistant/components/surepetcare/lock.py @@ -46,8 +46,6 @@ async def async_setup_entry( class SurePetcareLock(SurePetcareEntity, LockEntity): """A lock implementation for Sure Petcare Entities.""" - coordinator: SurePetcareDataCoordinator - def __init__( self, surepetcare_id: int, diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index c8b3aec4573..c3f88e924ea 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -54,8 +54,6 @@ async def async_setup_entry( class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): """Representation of a Switchbot binary sensor.""" - coordinator: SwitchbotDataUpdateCoordinator - def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9814534ce6d..97ca1aa15bb 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -59,7 +59,6 @@ async def async_setup_entry( class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Representation of a Switchbot.""" - coordinator: SwitchbotDataUpdateCoordinator _attr_device_class = CoverDeviceClass.CURTAIN _attr_supported_features = ( SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 688cfea6a86..c27c40613c7 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -12,7 +12,7 @@ from .const import MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator -class SwitchbotEntity(CoordinatorEntity, Entity): +class SwitchbotEntity(CoordinatorEntity[SwitchbotDataUpdateCoordinator], Entity): """Generic entity encapsulating common features of Switchbot device.""" def __init__( diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index c3afff27654..1ee0276b7ee 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -74,8 +74,6 @@ async def async_setup_entry( class SwitchBotSensor(SwitchbotEntity, SensorEntity): """Representation of a Switchbot sensor.""" - coordinator: SwitchbotDataUpdateCoordinator - def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 845d27488ad..de66a437dee 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -104,7 +104,6 @@ async def async_setup_entry( class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): """Representation of a Switchbot.""" - coordinator: SwitchbotDataUpdateCoordinator _attr_device_class = SwitchDeviceClass.SWITCH def __init__( diff --git a/homeassistant/components/switcher_kis/sensor.py b/homeassistant/components/switcher_kis/sensor.py index 6b6bf1bec4a..c8dced8663c 100644 --- a/homeassistant/components/switcher_kis/sensor.py +++ b/homeassistant/components/switcher_kis/sensor.py @@ -92,7 +92,9 @@ async def async_setup_entry( ) -class SwitcherSensorEntity(CoordinatorEntity, SensorEntity): +class SwitcherSensorEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], SensorEntity +): """Representation of a Switcher sensor entity.""" def __init__( diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 6df841b1e4e..0065038954f 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -78,7 +78,9 @@ async def async_setup_entry( ) -class SwitcherBaseSwitchEntity(CoordinatorEntity, SwitchEntity): +class SwitcherBaseSwitchEntity( + CoordinatorEntity[SwitcherDataUpdateCoordinator], SwitchEntity +): """Representation of a Switcher switch entity.""" def __init__(self, coordinator: SwitcherDataUpdateCoordinator) -> None: diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e07df808198..0503094d1cd 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -227,7 +227,9 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) -class SynologyDSMBaseEntity(CoordinatorEntity): +class SynologyDSMBaseEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[str, dict[str, Any]]]] +): """Representation of a Synology NAS entry.""" entity_description: SynologyDSMEntityDescription diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 37beb997755..c6edf5b61ea 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -289,7 +289,7 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) -class SystemBridgeEntity(CoordinatorEntity): +class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): """Defines a base System Bridge entity.""" def __init__( diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index fca64dc6102..e592c8e82e4 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -67,7 +67,6 @@ async def async_setup_entry( class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity): """Define a System Bridge binary sensor.""" - coordinator: SystemBridgeDataUpdateCoordinator entity_description: SystemBridgeBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index c4969e2c14c..e66749820a7 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -484,7 +484,6 @@ async def async_setup_entry( class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): """Define a System Bridge sensor.""" - coordinator: SystemBridgeDataUpdateCoordinator entity_description: SystemBridgeSensorEntityDescription def __init__( From 129c9e42f11e935eff6a3d464b4a1cb7dc06b816 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:29:11 +0100 Subject: [PATCH 0582/1054] Update coordinator typing (7) [w-z] (#68467) --- homeassistant/components/wallbox/__init__.py | 4 +--- homeassistant/components/wallbox/number.py | 1 - homeassistant/components/wallbox/sensor.py | 1 - homeassistant/components/wemo/entity.py | 4 +--- homeassistant/components/wled/models.py | 4 +--- homeassistant/components/xbox/base_sensor.py | 2 +- homeassistant/components/xbox/media_player.py | 2 +- homeassistant/components/xbox/remote.py | 2 +- homeassistant/components/xiaomi_miio/device.py | 10 ++++++++-- homeassistant/components/xiaomi_miio/vacuum.py | 14 ++++++++++---- .../yale_smart_alarm/alarm_control_panel.py | 2 -- .../components/yale_smart_alarm/entity.py | 8 ++------ .../components/yamaha_musiccast/__init__.py | 4 +--- 13 files changed, 27 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index f1ce91a5bdf..2a3da958cb9 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -148,11 +148,9 @@ class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" -class WallboxEntity(CoordinatorEntity): +class WallboxEntity(CoordinatorEntity[WallboxCoordinator]): """Defines a base Wallbox entity.""" - coordinator: WallboxCoordinator - @property def device_info(self) -> DeviceInfo: """Return device information about this Wallbox device.""" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 2bea3b1ef70..5822e7d45d9 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -59,7 +59,6 @@ class WallboxNumber(WallboxEntity, NumberEntity): """Representation of the Wallbox portal.""" entity_description: WallboxNumberEntityDescription - coordinator: WallboxCoordinator def __init__( self, diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index d19ea7347ca..57f2176febb 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -152,7 +152,6 @@ class WallboxSensor(WallboxEntity, SensorEntity): """Representation of the Wallbox portal.""" entity_description: WallboxSensorEntityDescription - coordinator: WallboxCoordinator def __init__( self, diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 9884b5b340c..6d94e203932 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -16,11 +16,9 @@ from .wemo_device import DeviceCoordinator _LOGGER = logging.getLogger(__name__) -class WemoEntity(CoordinatorEntity): +class WemoEntity(CoordinatorEntity[DeviceCoordinator]): """Common methods for Wemo entities.""" - coordinator: DeviceCoordinator # Override CoordinatorEntity.coordinator type. - # Most pyWeMo devices are associated with a single Home Assistant entity. When # that is not the case, name_suffix & unique_id_suffix can be used to provide # names and unique ids for additional Home Assistant entities. diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index c6f10daeff4..b5fc0855e04 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -7,11 +7,9 @@ from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator -class WLEDEntity(CoordinatorEntity): +class WLEDEntity(CoordinatorEntity[WLEDDataUpdateCoordinator]): """Defines a base WLED entity.""" - coordinator: WLEDDataUpdateCoordinator - @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 100685b6c34..024feb294b5 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -11,7 +11,7 @@ from . import PresenceData, XboxUpdateCoordinator from .const import DOMAIN -class XboxBaseSensorEntity(CoordinatorEntity): +class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): """Base Sensor for the Xbox Integration.""" def __init__( diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index 4cc7ebda545..edf29c164ee 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -78,7 +78,7 @@ async def async_setup_entry( ) -class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity): +class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity): """Representation of an Xbox Media Player.""" def __init__( diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 897836e5c42..75595483608 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -45,7 +45,7 @@ async def async_setup_entry( ) -class XboxRemote(CoordinatorEntity, RemoteEntity): +class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity): """Representation of an Xbox remote.""" def __init__( diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index 5a186c23570..88f90ada919 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -3,6 +3,7 @@ import datetime from enum import Enum from functools import partial import logging +from typing import Any, TypeVar from construct.core import ChecksumError from miio import Device, DeviceException @@ -10,12 +11,17 @@ from miio import Device, DeviceException from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import CONF_MAC, CONF_MODEL, DOMAIN, AuthException, SetupException _LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T", bound=DataUpdateCoordinator[Any]) + class ConnectXiaomiDevice: """Class to async connect to a Xiaomi Device.""" @@ -101,7 +107,7 @@ class XiaomiMiioEntity(Entity): return device_info -class XiaomiCoordinatedMiioEntity(CoordinatorEntity): +class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): """Representation of a base a coordinated Xiaomi Miio Entity.""" def __init__(self, name, device, entry, unique_id, coordinator): diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index a6fa6c399eb..555a352a6ce 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -203,13 +203,19 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) -class MiroboVacuum(XiaomiCoordinatedMiioEntity, StateVacuumEntity): +class MiroboVacuum( + XiaomiCoordinatedMiioEntity[DataUpdateCoordinator[VacuumCoordinatorData]], + StateVacuumEntity, +): """Representation of a Xiaomi Vacuum cleaner robot.""" - coordinator: DataUpdateCoordinator[VacuumCoordinatorData] - def __init__( - self, name, device, entry, unique_id, coordinator: DataUpdateCoordinator + self, + name, + device, + entry, + unique_id, + coordinator: DataUpdateCoordinator[VacuumCoordinatorData], ): """Initialize the Xiaomi vacuum cleaner robot handler.""" super().__init__(name, device, entry, unique_id, coordinator) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 27d0ed8b631..7a74b675f0b 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -81,8 +81,6 @@ async def async_setup_entry( class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): """Represent a Yale Smart Alarm.""" - coordinator: YaleDataUpdateCoordinator - _attr_code_arm_required = False _attr_supported_features = SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index 065d1f4fecb..b9a832f88e8 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -9,11 +9,9 @@ from .const import DOMAIN, MANUFACTURER, MODEL from .coordinator import YaleDataUpdateCoordinator -class YaleEntity(CoordinatorEntity, Entity): +class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale device.""" - coordinator: YaleDataUpdateCoordinator - def __init__(self, coordinator: YaleDataUpdateCoordinator, data: dict) -> None: """Initialize an Yale device.""" super().__init__(coordinator) @@ -28,11 +26,9 @@ class YaleEntity(CoordinatorEntity, Entity): ) -class YaleAlarmEntity(CoordinatorEntity, Entity): +class YaleAlarmEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale Alarm device.""" - coordinator: YaleDataUpdateCoordinator - def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize an Yale device.""" super().__init__(coordinator) diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index d984aaceb96..e28712cdf21 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -120,11 +120,9 @@ class MusicCastDataUpdateCoordinator(DataUpdateCoordinator[MusicCastData]): return self.musiccast.data -class MusicCastEntity(CoordinatorEntity): +class MusicCastEntity(CoordinatorEntity[MusicCastDataUpdateCoordinator]): """Defines a base MusicCast entity.""" - coordinator: MusicCastDataUpdateCoordinator - def __init__( self, *, From 40d4495ed098624a1f0816357b260bd8a298d969 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Mar 2022 15:38:29 +0100 Subject: [PATCH 0583/1054] Add update platform to WLED (#68454) * Add update platform to WLED * Copy pasta fixes * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update tests/components/wled/test_update.py Co-authored-by: Martin Hjelmare * Fix tests Co-authored-by: Martin Hjelmare --- homeassistant/components/wled/__init__.py | 1 + .../components/wled/binary_sensor.py | 3 + homeassistant/components/wled/button.py | 10 +- homeassistant/components/wled/update.py | 93 +++++++ .../wled/fixtures/rgb_no_update.json | 218 ++++++++++++++++ tests/components/wled/test_binary_sensor.py | 27 +- tests/components/wled/test_button.py | 52 +++- tests/components/wled/test_update.py | 240 ++++++++++++++++++ 8 files changed, 635 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/wled/update.py create mode 100644 tests/components/wled/fixtures/rgb_no_update.json create mode 100644 tests/components/wled/test_update.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 9c32f9becb8..bcfb98b9916 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -16,6 +16,7 @@ PLATFORMS = ( Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, ) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index 1a8033701da..d2262798d50 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -35,6 +35,9 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.UPDATE + # Disabled by default, as this entity is deprecated. + _attr_entity_registry_enabled_default = False + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 5b2e13911bc..2967067ef44 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, LOGGER from .coordinator import WLEDDataUpdateCoordinator from .helpers import wled_exception_handler from .models import WLEDEntity @@ -52,6 +52,9 @@ class WLEDUpdateButton(WLEDEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.UPDATE _attr_entity_category = EntityCategory.CONFIG + # Disabled by default, as this entity is deprecated. + _attr_entity_registry_enabled_default = False + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) @@ -83,6 +86,11 @@ class WLEDUpdateButton(WLEDEntity, ButtonEntity): @wled_exception_handler async def async_press(self) -> None: """Send out a update command.""" + LOGGER.warning( + "The WLED update button '%s' is deprecated, please " + "use the new update entity as a replacement", + self.entity_id, + ) current = self.coordinator.data.info.version beta = self.coordinator.data.info.version_latest_beta stable = self.coordinator.data.info.version_latest_stable diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py new file mode 100644 index 00000000000..f9e380479a8 --- /dev/null +++ b/homeassistant/components/wled/update.py @@ -0,0 +1,93 @@ +"""Support for WLED updates.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up WLED update based on a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([WLEDUpdateEntity(coordinator)]) + + +class WLEDUpdateEntity(WLEDEntity, UpdateEntity): + """Defines a WLED update entity.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize the update entity.""" + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Firmware" + self._attr_unique_id = coordinator.data.info.mac_address + self._attr_title = "WLED" + + @property + def current_version(self) -> str | None: + """Version currently in use.""" + if (version := self.coordinator.data.info.version) is None: + return None + return str(version) + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + # If we already run a pre-release, we consider being on the beta channel. + # Offer beta version upgrade, unless stable is newer + if ( + (beta := self.coordinator.data.info.version_latest_beta) is not None + and (current := self.coordinator.data.info.version) is not None + and (current.alpha or current.beta or current.release_candidate) + and ( + (stable := self.coordinator.data.info.version_latest_stable) is None + or (stable is not None and stable < beta) + ) + ): + return str(beta) + + if (stable := self.coordinator.data.info.version_latest_stable) is not None: + return str(stable) + + return None + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + if (version := self.latest_version) is None: + return None + return f"https://github.com/Aircoookie/WLED/releases/tag/v{version}" + + @wled_exception_handler + async def async_install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update.""" + if version is None: + # We cast here, as we know that the latest_version is a string. + version = cast(str, self.latest_version) + await self.coordinator.wled.upgrade(version=version) + await self.coordinator.async_refresh() diff --git a/tests/components/wled/fixtures/rgb_no_update.json b/tests/components/wled/fixtures/rgb_no_update.json new file mode 100644 index 00000000000..96b8ada10a3 --- /dev/null +++ b/tests/components/wled/fixtures/rgb_no_update.json @@ -0,0 +1,218 @@ +{ + "state": { + "on": true, + "bri": 127, + "transition": 7, + "ps": -1, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "tbri": 0 + }, + "udpn": { + "send": false, + "recv": true + }, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 19, + "len": 20, + "col": [[255, 159, 0], [0, 0, 0], [0, 0, 0]], + "fx": 0, + "sx": 32, + "ix": 128, + "pal": 0, + "sel": true, + "rev": false, + "cln": -1 + }, + { + "id": 1, + "start": 20, + "stop": 30, + "len": 10, + "col": [[0, 255, 123], [0, 0, 0], [0, 0, 0]], + "fx": 1, + "sx": 16, + "ix": 64, + "pal": 1, + "sel": true, + "rev": true, + "cln": -1 + } + ] + }, + "info": { + "ver": null, + "version_latest_stable": null, + "version_latest_beta": null, + "vid": 1909122, + "leds": { + "count": 30, + "rgbw": false, + "pin": [2], + "pwr": 470, + "maxpwr": 850, + "maxseg": 10 + }, + "name": "WLED RGB Light", + "udpport": 21324, + "live": false, + "fxcount": 81, + "palcount": 50, + "wifi": { + "bssid": "AA:AA:AA:AA:AA:BB", + "rssi": -62, + "signal": 76, + "channel": 11 + }, + "arch": "esp8266", + "core": "2_4_2", + "freeheap": 14600, + "uptime": 32, + "opt": 119, + "brand": "WLED", + "product": "DIY light", + "btype": "bin", + "mac": "aabbccddeeff" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Dual Scan", + "Fade", + "Chase", + "Chase Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Dark Sparkle", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Mega Strobe", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Red & Blue", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Merry Christmas", + "Fire Flicker", + "Gradient", + "Loading", + "In Out", + "In In", + "Out Out", + "Out In", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Dual Scanner", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "BPM", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkle", + "Lake", + "Meteor", + "Smooth Meteor", + "Railway", + "Ripple", + "Twinklefox" + ], + "palettes": [ + "Default", + "Random Cycle", + "Primary Color", + "Based on Primary", + "Set Colors", + "Based on Set", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura" + ] +} diff --git a/tests/components/wled/test_binary_sensor.py b/tests/components/wled/test_binary_sensor.py index 311044d213d..8b755f63ffe 100644 --- a/tests/components/wled/test_binary_sensor.py +++ b/tests/components/wled/test_binary_sensor.py @@ -1,5 +1,5 @@ """Tests for the WLED binary sensor platform.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import pytest @@ -13,7 +13,10 @@ from tests.common import MockConfigEntry async def test_update_available( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the firmware update binary sensor.""" entity_registry = er.async_get(hass) @@ -32,7 +35,10 @@ async def test_update_available( @pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) async def test_no_update_available( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the update binary sensor. There is no update available.""" entity_registry = er.async_get(hass) @@ -47,3 +53,18 @@ async def test_no_update_available( assert entry assert entry.unique_id == "aabbccddeeff_update" assert entry.entity_category is EntityCategory.DIAGNOSTIC + + +async def test_disabled_by_default( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test that the binary update sensor is disabled by default.""" + registry = er.async_get(hass) + + state = hass.states.get("binary_sensor.wled_rgb_light_firmware") + assert state is None + + entry = registry.async_get("binary_sensor.wled_rgb_light_firmware") + assert entry + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index c9c8412a5b9..a09c7e2aaa3 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -1,5 +1,5 @@ """Tests for the WLED button platform.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock from freezegun import freeze_time import pytest @@ -98,7 +98,11 @@ async def test_button_connection_error( async def test_button_update_stay_stable( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the update button. @@ -127,11 +131,19 @@ async def test_button_update_stay_stable( await hass.async_block_till_done() assert mock_wled.upgrade.call_count == 1 mock_wled.upgrade.assert_called_with(version="0.12.0") + assert ( + "The WLED update button 'button.wled_rgb_light_update' is deprecated" + in caplog.text + ) @pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) async def test_button_update_beta_to_stable( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the update button. @@ -148,11 +160,19 @@ async def test_button_update_beta_to_stable( await hass.async_block_till_done() assert mock_wled.upgrade.call_count == 1 mock_wled.upgrade.assert_called_with(version="0.8.6") + assert ( + "The WLED update button 'button.wled_rgbw_light_update' is deprecated" + in caplog.text + ) @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) async def test_button_update_stay_beta( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, ) -> None: """Test the update button. @@ -168,13 +188,35 @@ async def test_button_update_stay_beta( await hass.async_block_till_done() assert mock_wled.upgrade.call_count == 1 mock_wled.upgrade.assert_called_with(version="0.8.6b2") + assert ( + "The WLED update button 'button.wled_rgb_light_update' is deprecated" + in caplog.text + ) @pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) async def test_button_no_update_available( - hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, ) -> None: """Test the update button. There is no update available.""" state = hass.states.get("button.wled_websocket_update") assert state assert state.state == STATE_UNAVAILABLE + + +async def test_disabled_by_default( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test that the update button is disabled by default.""" + registry = er.async_get(hass) + + state = hass.states.get("button.wled_rgb_light_update") + assert state is None + + entry = registry.async_get("button.wled_rgb_light_update") + assert entry + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION diff --git a/tests/components/wled/test_update.py b/tests/components/wled/test_update.py new file mode 100644 index 00000000000..fea6180048e --- /dev/null +++ b/tests/components/wled/test_update.py @@ -0,0 +1,240 @@ +"""Tests for the WLED update platform.""" +from unittest.mock import AsyncMock, MagicMock + +import pytest +from wled import WLEDError + +from homeassistant.components.update import ( + DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, + UpdateDeviceClass, + UpdateEntityFeature, +) +from homeassistant.components.update.const import ( + ATTR_CURRENT_VERSION, + ATTR_LATEST_VERSION, + ATTR_RELEASE_SUMMARY, + ATTR_RELEASE_URL, + ATTR_TITLE, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory + +from tests.common import MockConfigEntry + + +async def test_update_available( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the firmware update available.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("update.wled_rgb_light_firmware") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) == UpdateDeviceClass.FIRMWARE + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.5" + assert state.attributes[ATTR_LATEST_VERSION] == "0.12.0" + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + assert ( + state.attributes[ATTR_RELEASE_URL] + == "https://github.com/Aircoookie/WLED/releases/tag/v0.12.0" + ) + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + assert state.attributes[ATTR_TITLE] == "WLED" + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("update.wled_rgb_light_firmware") + assert entry + assert entry.unique_id == "aabbccddeeff" + assert entry.entity_category is EntityCategory.CONFIG + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_no_update.json"], indirect=True) +async def test_update_information_available( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test having no update information available at all.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("update.wled_rgb_light_firmware") + assert state + assert state.attributes.get(ATTR_DEVICE_CLASS) == UpdateDeviceClass.FIRMWARE + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_CURRENT_VERSION] is None + assert state.attributes[ATTR_LATEST_VERSION] is None + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + assert state.attributes[ATTR_RELEASE_URL] is None + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + assert state.attributes[ATTR_TITLE] == "WLED" + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("update.wled_rgb_light_firmware") + assert entry + assert entry.unique_id == "aabbccddeeff" + assert entry.entity_category is EntityCategory.CONFIG + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) +async def test_no_update_available( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test there is no update available.""" + entity_registry = er.async_get(hass) + + state = hass.states.get("update.wled_websocket_firmware") + assert state + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_DEVICE_CLASS) == UpdateDeviceClass.FIRMWARE + assert state.attributes[ATTR_CURRENT_VERSION] == "0.12.0-b2" + assert state.attributes[ATTR_LATEST_VERSION] == "0.12.0-b2" + assert state.attributes[ATTR_RELEASE_SUMMARY] is None + assert ( + state.attributes[ATTR_RELEASE_URL] + == "https://github.com/Aircoookie/WLED/releases/tag/v0.12.0-b2" + ) + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + assert state.attributes[ATTR_TITLE] == "WLED" + assert ATTR_ICON not in state.attributes + + assert ATTR_ICON not in state.attributes + + entry = entity_registry.async_get("update.wled_websocket_firmware") + assert entry + assert entry.unique_id == "aabbccddeeff" + assert entry.entity_category is EntityCategory.CONFIG + + +async def test_update_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED update.""" + mock_wled.update.side_effect = WLEDError + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.wled_rgb_light_firmware"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("update.wled_rgb_light_firmware") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Invalid response from API" in caplog.text + + +async def test_update_stay_stable( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the update entity staying on stable. + + There is both an update for beta and stable available, however, the device + is currently running a stable version. Therefore, the update entity should + update to the next stable (even though beta is newer). + """ + state = hass.states.get("update.wled_rgb_light_firmware") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.5" + assert state.attributes[ATTR_LATEST_VERSION] == "0.12.0" + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.wled_rgb_light_firmware"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.upgrade.call_count == 1 + mock_wled.upgrade.assert_called_with(version="0.12.0") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_update_beta_to_stable( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the update entity. + + There is both an update for beta and stable available and the device + is currently a beta, however, a newer stable is available. Therefore, the + update entity should update to the next stable. + """ + state = hass.states.get("update.wled_rgbw_light_firmware") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.6b4" + assert state.attributes[ATTR_LATEST_VERSION] == "0.8.6" + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.wled_rgbw_light_firmware"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.upgrade.call_count == 1 + mock_wled.upgrade.assert_called_with(version="0.8.6") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_update_stay_beta( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the update entity. + + There is an update for beta and the device is currently a beta. Therefore, + the update entity should update to the next beta. + """ + state = hass.states.get("update.wled_rgb_light_firmware") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_CURRENT_VERSION] == "0.8.6b1" + assert state.attributes[ATTR_LATEST_VERSION] == "0.8.6b2" + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.wled_rgb_light_firmware"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.upgrade.call_count == 1 + mock_wled.upgrade.assert_called_with(version="0.8.6b2") From 1072aff0172ae7d4127181af3ac2097c2e5c3398 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:49:39 +0100 Subject: [PATCH 0584/1054] Update coordinator typing (1) [a-c] (#68442) --- homeassistant/components/accuweather/sensor.py | 5 +++-- homeassistant/components/accuweather/weather.py | 6 +++--- homeassistant/components/aemet/sensor.py | 2 +- homeassistant/components/aemet/weather.py | 2 +- homeassistant/components/airly/sensor.py | 3 +-- homeassistant/components/airnow/sensor.py | 4 +--- homeassistant/components/airzone/__init__.py | 2 +- homeassistant/components/amberelectric/binary_sensor.py | 4 +++- homeassistant/components/amberelectric/sensor.py | 4 ++-- homeassistant/components/aseko_pool_live/entity.py | 4 +--- homeassistant/components/aurora/__init__.py | 2 +- homeassistant/components/awair/sensor.py | 2 +- homeassistant/components/azure_devops/__init__.py | 3 +-- homeassistant/components/braviatv/media_player.py | 3 +-- homeassistant/components/braviatv/remote.py | 4 +--- homeassistant/components/canary/alarm_control_panel.py | 5 +++-- homeassistant/components/canary/camera.py | 4 +--- homeassistant/components/canary/sensor.py | 4 +--- homeassistant/components/climacell/__init__.py | 2 +- 19 files changed, 28 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 448c00eb53f..220575541ad 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -58,11 +58,12 @@ async def async_setup_entry( async_add_entities(sensors) -class AccuWeatherSensor(CoordinatorEntity, SensorEntity): +class AccuWeatherSensor( + CoordinatorEntity[AccuWeatherDataUpdateCoordinator], SensorEntity +): """Define an AccuWeather entity.""" _attr_attribution = ATTRIBUTION - coordinator: AccuWeatherDataUpdateCoordinator entity_description: AccuWeatherSensorDescription def __init__( diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 4ab9342de62..536f66a3cb9 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -56,11 +56,11 @@ async def async_setup_entry( async_add_entities([AccuWeatherEntity(name, coordinator)]) -class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): +class AccuWeatherEntity( + CoordinatorEntity[AccuWeatherDataUpdateCoordinator], WeatherEntity +): """Define an AccuWeather entity.""" - coordinator: AccuWeatherDataUpdateCoordinator - def __init__( self, name: str, coordinator: AccuWeatherDataUpdateCoordinator ) -> None: diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f22c6d321b1..f98e3fff49e 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -64,7 +64,7 @@ async def async_setup_entry( async_add_entities(entities) -class AbstractAemetSensor(CoordinatorEntity, SensorEntity): +class AbstractAemetSensor(CoordinatorEntity[WeatherUpdateCoordinator], SensorEntity): """Abstract class for an AEMET OpenData sensor.""" _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index cf23d6f3643..2bbb4d0da55 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -43,7 +43,7 @@ async def async_setup_entry( async_add_entities(entities, False) -class AemetWeather(CoordinatorEntity, WeatherEntity): +class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 3dab90f5620..9b647b93afa 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -134,10 +134,9 @@ async def async_setup_entry( async_add_entities(sensors, False) -class AirlySensor(CoordinatorEntity, SensorEntity): +class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" - coordinator: AirlyDataUpdateCoordinator entity_description: AirlySensorEntityDescription def __init__( diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index fcd3cfd6f45..780e40ed2ba 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -70,11 +70,9 @@ async def async_setup_entry( async_add_entities(entities, False) -class AirNowSensor(CoordinatorEntity, SensorEntity): +class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity): """Define an AirNow sensor.""" - coordinator: AirNowDataUpdateCoordinator - def __init__( self, coordinator: AirNowDataUpdateCoordinator, diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 24a2978d88f..c1fd6deddf2 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -20,7 +20,7 @@ from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] -class AirzoneEntity(CoordinatorEntity): +class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): """Define an Airzone entity.""" def __init__( diff --git a/homeassistant/components/amberelectric/binary_sensor.py b/homeassistant/components/amberelectric/binary_sensor.py index 422ff66db59..1931bcbd32c 100644 --- a/homeassistant/components/amberelectric/binary_sensor.py +++ b/homeassistant/components/amberelectric/binary_sensor.py @@ -24,7 +24,9 @@ PRICE_SPIKE_ICONS = { } -class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity): +class AmberPriceGridSensor( + CoordinatorEntity[AmberUpdateCoordinator], BinarySensorEntity +): """Sensor to show single grid binary values.""" _attr_attribution = ATTRIBUTION diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 6d5fb105008..522cde2a95f 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -51,7 +51,7 @@ def friendly_channel_type(channel_type: str) -> str: return "General" -class AmberSensor(CoordinatorEntity, SensorEntity): +class AmberSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity): """Amber Base Sensor.""" _attr_attribution = ATTRIBUTION @@ -180,7 +180,7 @@ class AmberPriceDescriptorSensor(AmberSensor): return self.coordinator.data[self.entity_description.key][self.channel_type] -class AmberGridSensor(CoordinatorEntity, SensorEntity): +class AmberGridSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity): """Sensor to show single grid specific values.""" _attr_attribution = ATTRIBUTION diff --git a/homeassistant/components/aseko_pool_live/entity.py b/homeassistant/components/aseko_pool_live/entity.py index 963bb536671..58974bcc326 100644 --- a/homeassistant/components/aseko_pool_live/entity.py +++ b/homeassistant/components/aseko_pool_live/entity.py @@ -8,11 +8,9 @@ from . import AsekoDataUpdateCoordinator from .const import DOMAIN -class AsekoEntity(CoordinatorEntity): +class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]): """Representation of an aseko entity.""" - coordinator: AsekoDataUpdateCoordinator - def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None: """Initialize the aseko entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index a029f2cf61b..01d6092a4f2 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -118,7 +118,7 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Error updating from NOAA: {error}") from error -class AuroraEntity(CoordinatorEntity): +class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]): """Implementation of the base Aurora Entity.""" _attr_extra_state_attributes = {"attribution": ATTRIBUTION} diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 89c505bd4b9..1859ace1d4c 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -102,7 +102,7 @@ async def async_setup_entry( async_add_entities(entities) -class AwairSensor(CoordinatorEntity, SensorEntity): +class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): """Defines an Awair sensor entity.""" entity_description: AwairSensorEntityDescription diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 1b1c65ae6d1..c6de8515979 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -94,10 +94,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class AzureDevOpsEntity(CoordinatorEntity): +class AzureDevOpsEntity(CoordinatorEntity[DataUpdateCoordinator[list[DevOpsBuild]]]): """Defines a base Azure DevOps entity.""" - coordinator: DataUpdateCoordinator[list[DevOpsBuild]] entity_description: AzureDevOpsEntityDescription def __init__( diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 69df0b245b8..99a2e5a1cb1 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -67,10 +67,9 @@ async def async_setup_entry( ) -class BraviaTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): +class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" - coordinator: BraviaTVCoordinator _attr_device_class = MediaPlayerDeviceClass.TV _attr_supported_features = SUPPORT_BRAVIA diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 7e01f26d0a5..016f8363b09 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -37,11 +37,9 @@ async def async_setup_entry( ) -class BraviaTVRemote(CoordinatorEntity, RemoteEntity): +class BraviaTVRemote(CoordinatorEntity[BraviaTVCoordinator], RemoteEntity): """Representation of a Bravia TV Remote.""" - coordinator: BraviaTVCoordinator - def __init__( self, coordinator: BraviaTVCoordinator, diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 4d29d4893e7..165bae0b497 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -48,10 +48,11 @@ async def async_setup_entry( async_add_entities(alarms, True) -class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): +class CanaryAlarm( + CoordinatorEntity[CanaryDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of a Canary alarm control panel.""" - coordinator: CanaryDataUpdateCoordinator _attr_supported_features = ( SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT ) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index a4c5a5ac837..46826d80291 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -78,11 +78,9 @@ async def async_setup_entry( async_add_entities(cameras, True) -class CanaryCamera(CoordinatorEntity, Camera): +class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): """An implementation of a Canary security camera.""" - coordinator: CanaryDataUpdateCoordinator - def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index cf2b0970311..3de088016a9 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -76,11 +76,9 @@ async def async_setup_entry( async_add_entities(sensors, True) -class CanarySensor(CoordinatorEntity, SensorEntity): +class CanarySensor(CoordinatorEntity[CanaryDataUpdateCoordinator], SensorEntity): """Representation of a Canary sensor.""" - coordinator: CanaryDataUpdateCoordinator - def __init__( self, coordinator: CanaryDataUpdateCoordinator, diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index e408476aad3..cf80b83fc36 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -273,7 +273,7 @@ class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): return data -class ClimaCellEntity(CoordinatorEntity): +class ClimaCellEntity(CoordinatorEntity[ClimaCellDataUpdateCoordinator]): """Base ClimaCell Entity.""" def __init__( From c56b77f2b315e2b8742fd1db33233d60bd5a56eb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Mar 2022 15:51:46 +0100 Subject: [PATCH 0585/1054] Move WLED update title to class attribute (#68470) --- homeassistant/components/wled/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index f9e380479a8..197d5f4c580 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -35,13 +35,13 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity): _attr_supported_features = ( UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION ) + _attr_title = "WLED" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the update entity.""" super().__init__(coordinator=coordinator) self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = coordinator.data.info.mac_address - self._attr_title = "WLED" @property def current_version(self) -> str | None: From 7c71beaa610cc1a27f5a4db26ad0d5111894cd71 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:13:19 +0100 Subject: [PATCH 0586/1054] Trigger full ci run on label (#68469) --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8bf509a3787..b099f5fb9dd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -119,7 +119,8 @@ jobs: || [[ "${{ github.ref }}" == "refs/heads/master" ]] \ || [[ "${{ github.ref }}" == "refs/heads/rc" ]] \ || [[ "${{ steps.core.outputs.any }}" == "true" ]] \ - || [[ "${{ github.event.inputs.full }}" == "true" ]]; + || [[ "${{ github.event.inputs.full }}" == "true" ]] \ + || [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-full-run') }}" == "true" ]]; then test_groups="[1, 2, 3, 4, 5, 6]" test_group_count=6 From 28d3117a88e2fe0452c91ba882d546a914b1d739 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 21 Mar 2022 18:41:55 +0100 Subject: [PATCH 0587/1054] Track number of persons in a Zone (#68473) --- homeassistant/components/zone/__init__.py | 59 +++++++++++++++++++- tests/components/zone/test_init.py | 66 +++++++++++++++++++++-- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index ef2d21281d1..ebde3328c02 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,6 +1,7 @@ """Support for the definition of zones.""" from __future__ import annotations +from collections.abc import Callable import logging from typing import Any, cast @@ -9,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( ATTR_EDITABLE, + ATTR_ENTITY_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ICON, @@ -19,7 +21,10 @@ from homeassistant.const import ( CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, SERVICE_RELOAD, + STATE_HOME, + STATE_NOT_HOME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.helpers import ( @@ -27,6 +32,7 @@ from homeassistant.helpers import ( config_validation as cv, entity, entity_component, + event, service, storage, ) @@ -284,7 +290,10 @@ class Zone(entity.Entity): """Initialize the zone.""" self._config = config self.editable = True + self._attrs: dict | None = None + self._remove_listener: Callable[[], None] | None = None self._generate_attrs() + self._persons_in_zone: set[str] = set() @classmethod def from_yaml(cls, config: dict) -> Zone: @@ -295,9 +304,9 @@ class Zone(entity.Entity): return zone @property - def state(self) -> str: + def state(self) -> int: """Return the state property really does nothing for a zone.""" - return "zoning" + return len(self._persons_in_zone) @property def name(self) -> str: @@ -327,6 +336,35 @@ class Zone(entity.Entity): self._generate_attrs() self.async_write_ha_state() + @callback + def _person_state_change_listener(self, evt: Event) -> None: + person_entity_id = evt.data[ATTR_ENTITY_ID] + cur_count = len(self._persons_in_zone) + if self._state_is_in_zone(evt.data.get("new_state")): + self._persons_in_zone.add(person_entity_id) + elif person_entity_id in self._persons_in_zone: + self._persons_in_zone.remove(person_entity_id) + + if len(self._persons_in_zone) != cur_count: + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + person_domain = "person" # avoid circular import + persons = self.hass.states.async_entity_ids(person_domain) + for person in persons: + if self._state_is_in_zone(self.hass.states.get(person)): + self._persons_in_zone.add(person) + + self.async_on_remove( + event.async_track_state_change_filtered( + self.hass, + event.TrackStates(False, set(), {person_domain}), + self._person_state_change_listener, + ).async_remove + ) + @callback def _generate_attrs(self) -> None: """Generate new attrs based on config.""" @@ -337,3 +375,20 @@ class Zone(entity.Entity): ATTR_PASSIVE: self._config[CONF_PASSIVE], ATTR_EDITABLE: self.editable, } + + @callback + def _state_is_in_zone(self, state: State | None) -> bool: + """Return if given state is in zone.""" + return ( + state is not None + and state.state + not in ( + STATE_NOT_HOME, + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ) + and ( + state.state.casefold() == self.name.casefold() + or (state.state == STATE_HOME and self.entity_id == ENTITY_ID_HOME) + ) + ) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 8d0fddb921c..1e0c069e8ff 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -316,7 +316,7 @@ async def test_load_from_storage(hass, storage_setup): """Test set up from storage.""" assert await storage_setup() state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.state == "zoning" + assert state.state == "0" assert state.name == "from storage" assert state.attributes.get(ATTR_EDITABLE) @@ -328,12 +328,12 @@ async def test_editable_state_attribute(hass, storage_setup): ) state = hass.states.get(f"{DOMAIN}.from_storage") - assert state.state == "zoning" + assert state.state == "0" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" assert state.attributes.get(ATTR_EDITABLE) state = hass.states.get(f"{DOMAIN}.yaml_option") - assert state.state == "zoning" + assert state.state == "0" assert not state.attributes.get(ATTR_EDITABLE) @@ -457,7 +457,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): assert resp["success"] state = hass.states.get(input_entity_id) - assert state.state == "zoning" + assert state.state == "0" assert state.attributes["latitude"] == 3 assert state.attributes["longitude"] == 4 assert state.attributes["passive"] is True @@ -503,3 +503,61 @@ async def test_unavailable_zone(hass): assert zone.async_active_zone(hass, 0.0, 0.01) is None assert zone.in_zone(hass.states.get("zone.bla"), 0, 0) is False + + +async def test_state(hass): + """Test the state of a zone.""" + info = { + "name": "Test Zone", + "latitude": 32.880837, + "longitude": -117.237561, + "radius": 250, + "passive": False, + } + assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) + + assert len(hass.states.async_entity_ids("zone")) == 2 + state = hass.states.get("zone.test_zone") + assert state.state == "0" + + # Person entity enters zone + hass.states.async_set( + "person.person1", + "Test Zone", + ) + await hass.async_block_till_done() + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "0" + + # Person entity enters zone (case insensitive) + hass.states.async_set( + "person.person2", + "TEST zone", + ) + await hass.async_block_till_done() + assert hass.states.get("zone.test_zone").state == "2" + assert hass.states.get("zone.home").state == "0" + + # Person entity enters another zone + hass.states.async_set( + "person.person1", + "home", + ) + await hass.async_block_till_done() + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "1" + + # Person entity enters not_home + hass.states.async_set( + "person.person1", + "not_home", + ) + await hass.async_block_till_done() + assert hass.states.get("zone.test_zone").state == "1" + assert hass.states.get("zone.home").state == "0" + + # Person entity removed + hass.states.async_remove("person.person2") + await hass.async_block_till_done() + assert hass.states.get("zone.test_zone").state == "0" + assert hass.states.get("zone.home").state == "0" From d065475aac186368118e502d0b0a203944da42b6 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Mon, 21 Mar 2022 14:27:51 -0400 Subject: [PATCH 0588/1054] Bump asyncsleepiq to 1.2.0 (#68438) Co-authored-by: J. Nick Koston --- homeassistant/components/sleepiq/coordinator.py | 9 ++++----- homeassistant/components/sleepiq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index 47512d23ce9..8d51da5f47a 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -34,11 +34,10 @@ class SleepIQDataUpdateCoordinator(DataUpdateCoordinator[None]): self.client = client async def _async_update_data(self) -> None: - tasks = ( - [self.client.fetch_bed_statuses()] - + [bed.foundation.update_lights() for bed in self.client.beds.values()] - + [bed.foundation.update_actuators() for bed in self.client.beds.values()] - ) + tasks = [self.client.fetch_bed_statuses()] + [ + bed.foundation.update_foundation_status() + for bed in self.client.beds.values() + ] await asyncio.gather(*tasks) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index 24a3fc2f463..bb386f1cf42 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.1.2"], + "requirements": ["asyncsleepiq==1.2.0"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 7ba6a53525e..64332f81b7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -335,7 +335,7 @@ async-upnp-client==0.27.0 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.1.2 +asyncsleepiq==1.2.0 # homeassistant.components.aten_pe atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9d7239f024..4f5cf484ee0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ arcam-fmj==0.12.0 async-upnp-client==0.27.0 # homeassistant.components.sleepiq -asyncsleepiq==1.1.2 +asyncsleepiq==1.2.0 # homeassistant.components.aurora auroranoaa==0.0.2 From 1f135a20a0dd6e2f690bf8ecc2ee331b2729ad19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Mar 2022 09:54:57 -1000 Subject: [PATCH 0589/1054] Remove extra attributes from pvoutput sensors (#68481) --- homeassistant/components/pvoutput/const.py | 6 ----- homeassistant/components/pvoutput/sensor.py | 30 +-------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/homeassistant/components/pvoutput/const.py b/homeassistant/components/pvoutput/const.py index 084ff809b27..f01d10fd13d 100644 --- a/homeassistant/components/pvoutput/const.py +++ b/homeassistant/components/pvoutput/const.py @@ -14,10 +14,4 @@ LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(minutes=2) -ATTR_ENERGY_GENERATION = "energy_generation" -ATTR_POWER_GENERATION = "power_generation" -ATTR_ENERGY_CONSUMPTION = "energy_consumption" -ATTR_POWER_CONSUMPTION = "power_consumption" -ATTR_EFFICIENCY = "efficiency" - CONF_SYSTEM_ID = "system_id" diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index de108329c45..7bd4b8789eb 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -14,8 +14,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_TEMPERATURE, - ATTR_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, @@ -28,15 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - ATTR_EFFICIENCY, - ATTR_ENERGY_CONSUMPTION, - ATTR_ENERGY_GENERATION, - ATTR_POWER_CONSUMPTION, - ATTR_POWER_GENERATION, - CONF_SYSTEM_ID, - DOMAIN, -) +from .const import CONF_SYSTEM_ID, DOMAIN from .coordinator import PVOutputDataUpdateCoordinator @@ -164,21 +154,3 @@ class PVOutputSensorEntity( def native_value(self) -> int | float | None: """Return the state of the device.""" return self.entity_description.value_fn(self.coordinator.data) - - @property - def extra_state_attributes(self) -> dict[str, int | float | None] | None: - """Return the state attributes of the monitored installation.""" - - # Only add attributes to the original sensor - if self.entity_description.key != "energy_generation": - return None - - return { - ATTR_ENERGY_GENERATION: self.coordinator.data.energy_generation, - ATTR_POWER_GENERATION: self.coordinator.data.power_generation, - ATTR_ENERGY_CONSUMPTION: self.coordinator.data.energy_consumption, - ATTR_POWER_CONSUMPTION: self.coordinator.data.power_consumption, - ATTR_EFFICIENCY: self.coordinator.data.normalized_output, - ATTR_TEMPERATURE: self.coordinator.data.temperature, - ATTR_VOLTAGE: self.coordinator.data.voltage, - } From 574f4710aac2643ca616004ab6207290c5c17ec2 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 21 Mar 2022 21:13:03 +0100 Subject: [PATCH 0590/1054] Add select platform to Plugwise (#68303) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + homeassistant/components/plugwise/climate.py | 9 +-- homeassistant/components/plugwise/const.py | 8 +++ homeassistant/components/plugwise/select.py | 64 ++++++++++++++++++++ 4 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/plugwise/select.py diff --git a/.coveragerc b/.coveragerc index 47b07724421..3b99fac2c3b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -898,6 +898,7 @@ omit = homeassistant/components/plaato/sensor.py homeassistant/components/plex/media_player.py homeassistant/components/plex/view.py + homeassistant/components/plugwise/select.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/__init__.py diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index e22f12c3526..12a203b6896 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -21,18 +21,11 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN +from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, THERMOSTAT_CLASSES from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity from .util import plugwise_command -THERMOSTAT_CLASSES = [ - "thermostat", - "thermostatic_radiator_valve", - "zone_thermometer", - "zone_thermostat", -] - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index adcd68ed50e..3bcad0b88aa 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -24,6 +24,7 @@ PLATFORMS_GATEWAY = [ Platform.CLIMATE, Platform.SENSOR, Platform.SWITCH, + Platform.SELECT, ] ZEROCONF_MAP = { "smile": "P1", @@ -43,3 +44,10 @@ DEFAULT_SCAN_INTERVAL = { "thermostat": timedelta(seconds=60), } DEFAULT_USERNAME = "smile" + +THERMOSTAT_CLASSES = [ + "thermostat", + "thermostatic_radiator_valve", + "zone_thermometer", + "zone_thermostat", +] diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py new file mode 100644 index 00000000000..b3cfb366889 --- /dev/null +++ b/homeassistant/components/plugwise/select.py @@ -0,0 +1,64 @@ +"""Plugwise Select component for Home Assistant.""" +from __future__ import annotations + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, THERMOSTAT_CLASSES +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Smile selector from a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + PlugwiseSelectEntity(coordinator, device_id) + for device_id, device in coordinator.data.devices.items() + if device["class"] in THERMOSTAT_CLASSES + and len(device.get("available_schedules")) > 1 + ) + + +class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): + """Represent Smile selector.""" + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initialise the selector.""" + super().__init__(coordinator, device_id) + self._attr_unique_id = f"{device_id}-select_schedule" + self._attr_name = (f"{self.device.get('name', '')} Select Schedule").lstrip() + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return self.device.get("selected_schedule") + + @property + def options(self) -> list[str]: + """Return a set of selectable options.""" + return self.device.get("available_schedules", []) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + if not ( + await self.coordinator.api.set_schedule_state( + self.device.get("location"), + option, + STATE_ON, + ) + ): + raise HomeAssistantError(f"Failed to change to schedule {option}") + await self.coordinator.async_request_refresh() From 16655c4ccc79a2e51844acbfe3989f16e81f9223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Mar 2022 12:41:15 -1000 Subject: [PATCH 0591/1054] Fix tplink color temp conversion (#68484) --- homeassistant/components/tplink/light.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 30d7fbde40a..4519ead4d09 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -87,7 +87,10 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): # Handle turning to temp mode if ATTR_COLOR_TEMP in kwargs: - color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) + # Handle temp conversion mireds -> kelvin being slightly outside of valid range + kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) + kelvin_range = self.device.valid_temperature_range + color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin)) _LOGGER.debug("Changing color temp to %s", color_tmp) await self.device.set_color_temp( color_tmp, brightness=brightness, transition=transition From 653305b998dd033365576db303b32dd5df3a6c54 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 21 Mar 2022 17:48:44 -0500 Subject: [PATCH 0592/1054] Support multiple Plex servers in media browser (#68321) --- .coveragerc | 1 + homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/plex/cast.py | 12 +- .../components/plex/media_browser.py | 215 +++++++++++------- homeassistant/components/plex/media_player.py | 54 +---- homeassistant/components/plex/models.py | 33 +++ homeassistant/components/plex/services.py | 71 ++++-- .../components/sonos/media_player.py | 19 +- tests/components/plex/test_browse_media.py | 41 ++-- tests/components/plex/test_playback.py | 84 ++++++- tests/components/plex/test_services.py | 73 ++++-- tests/components/sonos/test_plex_playback.py | 52 ++++- 12 files changed, 436 insertions(+), 221 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3b99fac2c3b..1216a78370a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -896,6 +896,7 @@ omit = homeassistant/components/plaato/const.py homeassistant/components/plaato/entity.py homeassistant/components/plaato/sensor.py + homeassistant/components/plex/cast.py homeassistant/components/plex/media_player.py homeassistant/components/plex/view.py homeassistant/components/plugwise/select.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index df3a4b8cd11..e0a84ced16f 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -69,7 +69,7 @@ async def async_browse_media(hass, media_content_type, media_content_id, platfor return await hass.async_add_executor_job( partial( browse_media, - plex_server, + hass, is_internal, media_content_type, media_content_id, diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index 59f23a681f8..dc8d791f117 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -10,8 +10,7 @@ from homeassistant.components.media_player.const import MEDIA_CLASS_APP from homeassistant.core import HomeAssistant from . import async_browse_media as async_browse_plex_media, is_plex_media_id -from .const import PLEX_URI_SCHEME -from .services import lookup_plex_media +from .services import process_plex_payload async def async_get_media_browser_root_object( @@ -51,13 +50,10 @@ def _play_media( hass: HomeAssistant, chromecast: Chromecast, media_type: str, media_id: str ) -> None: """Play media.""" - media_id = media_id[len(PLEX_URI_SCHEME) :] - media = lookup_plex_media(hass, media_type, media_id) - if media is None: - return + result = process_plex_payload(hass, media_type, media_id) controller = PlexController() chromecast.register_handler(controller) - controller.play_media(media) + controller.play_media(result.media, offset=result.offset) async def async_play_media( @@ -68,7 +64,7 @@ async def async_play_media( media_id: str, ) -> bool: """Play media.""" - if media_id and media_id.startswith(PLEX_URI_SCHEME): + if is_plex_media_id(media_id): await hass.async_add_executor_job( _play_media, hass, chromecast, media_type, media_id ) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index c50c173ec8d..4b2bb502d69 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -1,7 +1,7 @@ """Support to interface with the Plex API.""" from __future__ import annotations -import logging +from yarl import URL from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -18,7 +18,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.components.media_player.errors import BrowseError -from .const import DOMAIN, PLEX_URI_SCHEME +from .const import DOMAIN, SERVERS from .errors import MediaNotFound from .helpers import pretty_title @@ -27,16 +27,7 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" -HUB_PREFIX = "hub:" EXPANDABLES = ["album", "artist", "playlist", "season", "show"] -PLAYLISTS_BROWSE_PAYLOAD = { - "title": "Playlists", - "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME + "all", - "media_content_type": "playlists", - "can_play": False, - "can_expand": True, -} ITEM_TYPE_MEDIA_CLASS = { "album": MEDIA_CLASS_ALBUM, "artist": MEDIA_CLASS_ARTIST, @@ -52,25 +43,39 @@ ITEM_TYPE_MEDIA_CLASS = { "video": MEDIA_CLASS_VIDEO, } -_LOGGER = logging.getLogger(__name__) - def browse_media( # noqa: C901 - plex_server, is_internal, media_content_type, media_content_id, *, platform=None + hass, is_internal, media_content_type, media_content_id, *, platform=None ): """Implement the websocket media browsing helper.""" + server_id = None + plex_server = None + special_folder = None + + if media_content_id: + url = URL(media_content_id) + server_id = url.host + plex_server = hass.data[DOMAIN][SERVERS][server_id] + if media_content_type == "hub": + _, hub_location, hub_identifier = url.parts + elif media_content_type in ["library", "server"] and len(url.parts) > 2: + _, media_content_id, special_folder = url.parts + else: + media_content_id = url.name + + if media_content_type in ("plex_root", None): + return root_payload(hass, is_internal, platform=platform) def item_payload(item, short_name=False): """Create response payload for a single media item.""" try: media_class = ITEM_TYPE_MEDIA_CLASS[item.type] except KeyError as err: - _LOGGER.debug("Unknown type received: %s", item.type) - raise UnknownMediaType from err + raise UnknownMediaType("Unknown type received: {item.type}") from err payload = { "title": pretty_title(item, short_name), "media_class": media_class, - "media_content_id": PLEX_URI_SCHEME + str(item.ratingKey), + "media_content_id": generate_plex_uri(server_id, item.ratingKey), "media_content_type": item.type, "can_play": True, "can_expand": item.type in EXPANDABLES, @@ -81,16 +86,41 @@ def browse_media( # noqa: C901 thumbnail = item.thumbUrl else: thumbnail = get_proxy_image_url( - plex_server.machine_identifier, + server_id, item.ratingKey, ) payload["thumbnail"] = thumbnail return BrowseMedia(**payload) - def library_payload(library_id): + def server_payload(): + """Create response payload to describe libraries of the Plex server.""" + server_info = BrowseMedia( + title=plex_server.friendly_name, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id=generate_plex_uri(server_id, "server"), + media_content_type="server", + can_play=False, + can_expand=True, + children=[], + children_media_class=MEDIA_CLASS_DIRECTORY, + thumbnail="https://brands.home-assistant.io/_/plex/logo.png", + ) + if platform != "sonos": + server_info.children.append( + special_library_payload(server_info, "Recommended") + ) + for library in plex_server.library.sections(): + if library.type == "photo": + continue + if library.type != "artist" and platform == "sonos": + continue + server_info.children.append(library_section_payload(library)) + server_info.children.append(playlists_payload()) + return server_info + + def library_contents(library): """Create response payload to describe contents of a specific library.""" - library = plex_server.library.sectionByID(library_id) library_info = library_section_payload(library) library_info.children = [special_library_payload(library_info, "Recommended")] for item in library.all(): @@ -100,9 +130,17 @@ def browse_media( # noqa: C901 continue return library_info - def playlists_payload(platform): + def playlists_payload(): """Create response payload for all available playlists.""" - playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} + playlists_info = { + "title": "Playlists", + "media_class": MEDIA_CLASS_DIRECTORY, + "media_content_id": generate_plex_uri(server_id, "all"), + "media_content_type": "playlists", + "can_play": False, + "can_expand": True, + "children": [], + } for playlist in plex_server.playlists(): if playlist.playlistType != "audio" and platform == "sonos": continue @@ -137,35 +175,29 @@ def browse_media( # noqa: C901 continue return media_info - if media_content_id: - assert media_content_id.startswith(PLEX_URI_SCHEME) - media_content_id = media_content_id[len(PLEX_URI_SCHEME) :] - - if media_content_id and media_content_id.startswith(HUB_PREFIX): - media_content_id = media_content_id[len(HUB_PREFIX) :] - location, hub_identifier = media_content_id.split(":") - if location == "server": + if media_content_type == "hub": + if hub_location == "server": hub = next( x for x in plex_server.library.hubs() if x.hubIdentifier == hub_identifier ) - media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}" + media_content_id = f"server/{hub.hubIdentifier}" else: - library_section = plex_server.library.sectionByID(int(location)) + library_section = plex_server.library.sectionByID(int(hub_location)) hub = next( x for x in library_section.hubs() if x.hubIdentifier == hub_identifier ) - media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}" + media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}" try: children_media_class = ITEM_TYPE_MEDIA_CLASS[hub.type] except KeyError as err: - raise BrowseError(f"Unknown type received: {hub.type}") from err + raise UnknownMediaType(f"Unknown type received: {hub.type}") from err payload = { "title": hub.title, "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME + media_content_id, - "media_content_type": hub.type, + "media_content_id": generate_plex_uri(server_id, media_content_id), + "media_content_type": "hub", "can_play": False, "can_expand": True, "children": [], @@ -180,11 +212,6 @@ def browse_media( # noqa: C901 payload["children"].append(item_payload(item)) return BrowseMedia(**payload) - if media_content_id and ":" in media_content_id: - media_content_id, special_folder = media_content_id.split(":") - else: - special_folder = None - if special_folder: if media_content_type == "server": library_or_section = plex_server.library @@ -196,7 +223,7 @@ def browse_media( # noqa: C901 try: children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE] except KeyError as err: - raise BrowseError( + raise UnknownMediaType( f"Unknown type received: {library_or_section.TYPE}" ) from err else: @@ -207,8 +234,9 @@ def browse_media( # noqa: C901 payload = { "title": title, "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME - + f"{media_content_id}:{special_folder}", + "media_content_id": generate_plex_uri( + server_id, f"{media_content_id}/{special_folder}" + ), "media_content_type": media_content_type, "can_play": False, "can_expand": True, @@ -225,11 +253,13 @@ def browse_media( # noqa: C901 return BrowseMedia(**payload) try: - if media_content_type in ("server", None): - return server_payload(plex_server, platform) + if media_content_type == "server": + return server_payload() if media_content_type == "library": - return library_payload(int(media_content_id)) + library_id = int(media_content_id) + library = plex_server.library.sectionByID(library_id) + return library_contents(library) except UnknownMediaType as err: raise BrowseError( @@ -237,7 +267,7 @@ def browse_media( # noqa: C901 ) from err if media_content_type == "playlists": - return playlists_payload(platform) + return playlists_payload() payload = { "media_type": DOMAIN, @@ -249,17 +279,61 @@ def browse_media( # noqa: C901 return response +def generate_plex_uri(server_id, media_id): + """Create a media_content_id URL for playable Plex media.""" + if isinstance(media_id, int): + media_id = str(media_id) + if isinstance(media_id, str) and not media_id.startswith("/"): + media_id = f"/{media_id}" + return str( + URL.build( + scheme=DOMAIN, + host=server_id, + path=media_id, + ) + ) + + +def root_payload(hass, is_internal, platform=None): + """Return root payload for Plex.""" + children = [] + + for server_id in hass.data[DOMAIN][SERVERS]: + children.append( + browse_media( + hass, + is_internal, + "server", + generate_plex_uri(server_id, ""), + platform=platform, + ) + ) + + if len(children) == 1: + return children[0] + + return BrowseMedia( + title="Plex", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="", + media_content_type="plex_root", + can_play=False, + can_expand=True, + children=children, + ) + + def library_section_payload(section): """Create response payload for a single library section.""" try: children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE] except KeyError as err: - _LOGGER.debug("Unknown type received: %s", section.TYPE) - raise UnknownMediaType from err + raise UnknownMediaType(f"Unknown type received: {section.TYPE}") from err + server_id = section._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=PLEX_URI_SCHEME + str(section.key), + media_content_id=generate_plex_uri(server_id, section.key), media_content_type="library", can_play=False, can_expand=True, @@ -270,10 +344,11 @@ def library_section_payload(section): def special_library_payload(parent_payload, special_type): """Create response payload for special library folders.""" title = f"{special_type} ({parent_payload.title})" + special_library_id = f"{parent_payload.media_content_id}/{special_type}" return BrowseMedia( title=title, media_class=parent_payload.media_class, - media_content_id=f"{parent_payload.media_content_id}:{special_type}", + media_content_id=special_library_id, media_content_type=parent_payload.media_content_type, can_play=False, can_expand=True, @@ -281,41 +356,18 @@ def special_library_payload(parent_payload, special_type): ) -def server_payload(plex_server, platform): - """Create response payload to describe libraries of the Plex server.""" - server_info = BrowseMedia( - title=plex_server.friendly_name, - media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=PLEX_URI_SCHEME + plex_server.machine_identifier, - media_content_type="server", - can_play=False, - can_expand=True, - children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, - ) - if platform != "sonos": - server_info.children.append(special_library_payload(server_info, "Recommended")) - for library in plex_server.library.sections(): - if library.type == "photo": - continue - if library.type != "artist" and platform == "sonos": - continue - server_info.children.append(library_section_payload(library)) - server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD)) - return server_info - - def hub_payload(hub): """Create response payload for a hub.""" if hasattr(hub, "librarySectionID"): - media_content_id = f"{HUB_PREFIX}{hub.librarySectionID}:{hub.hubIdentifier}" + media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}" else: - media_content_id = f"{HUB_PREFIX}server:{hub.hubIdentifier}" + media_content_id = f"server/{hub.hubIdentifier}" + server_id = hub._server.machineIdentifier # pylint: disable=protected-access payload = { "title": hub.title, "media_class": MEDIA_CLASS_DIRECTORY, - "media_content_id": PLEX_URI_SCHEME + media_content_id, - "media_content_type": hub.type, + "media_content_id": generate_plex_uri(server_id, media_content_id), + "media_content_type": "hub", "can_play": False, "can_expand": True, } @@ -324,10 +376,11 @@ def hub_payload(hub): def station_payload(station): """Create response payload for a music station.""" + server_id = station._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=station.title, media_class=ITEM_TYPE_MEDIA_CLASS[station.type], - media_content_id=PLEX_URI_SCHEME + station.key, + media_content_id=generate_plex_uri(server_id, station.key), media_content_type="station", can_play=True, can_expand=False, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index d652ead7dae..6e729618022 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,7 +2,6 @@ from __future__ import annotations from functools import wraps -import json import logging import plexapi.exceptions @@ -46,12 +45,11 @@ from .const import ( PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, - PLEX_URI_SCHEME, SERVERS, TRANSIENT_DEVICE_MODELS, ) -from .errors import MediaNotFound from .media_browser import browse_media +from .services import process_plex_payload _LOGGER = logging.getLogger(__name__) @@ -483,51 +481,13 @@ class PlexMediaPlayer(MediaPlayerEntity): f"Client is not currently accepting playback controls: {self.name}" ) - if not self.plex_server.has_token: - _LOGGER.warning( - "Plex integration configured without a token, playback may fail" - ) - - if media_id.startswith(PLEX_URI_SCHEME): - media_id = media_id[len(PLEX_URI_SCHEME) :] - - if media_type == "station": - playqueue = self.plex_server.create_station_playqueue(media_id) - try: - self.device.playMedia(playqueue) - except requests.exceptions.ConnectTimeout as exc: - raise HomeAssistantError( - f"Request failed when playing on {self.name}" - ) from exc - return - - src = json.loads(media_id) - if isinstance(src, int): - src = {"plex_key": src} - - offset = 0 - - if playqueue_id := src.pop("playqueue_id", None): - try: - playqueue = self.plex_server.get_playqueue(playqueue_id) - except plexapi.exceptions.NotFound as err: - raise MediaNotFound( - f"PlayQueue '{playqueue_id}' could not be found" - ) from err - else: - shuffle = src.pop("shuffle", 0) - offset = src.pop("offset", 0) * 1000 - resume = src.pop("resume", False) - media = self.plex_server.lookup_media(media_type, **src) - - if resume and not offset: - offset = media.viewOffset - - _LOGGER.debug("Attempting to play %s on %s", media, self.name) - playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + result = process_plex_payload( + self.hass, media_type, media_id, default_plex_server=self.plex_server + ) + _LOGGER.debug("Attempting to play %s on %s", result.media, self.name) try: - self.device.playMedia(playqueue, offset=offset) + self.device.playMedia(result.media, offset=result.offset) except requests.exceptions.ConnectTimeout as exc: raise HomeAssistantError( f"Request failed when playing on {self.name}" @@ -578,7 +538,7 @@ class PlexMediaPlayer(MediaPlayerEntity): is_internal = is_internal_request(self.hass) return await self.hass.async_add_executor_job( browse_media, - self.plex_server, + self.hass, is_internal, media_content_type, media_content_id, diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 406c263dbff..0b77f450e7e 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,4 +1,5 @@ """Models to represent various Plex objects used in the integration.""" +from distutils.util import strtobool import logging from homeassistant.components.media_player.const import ( @@ -141,3 +142,35 @@ class PlexSession: thumb_url = media.url(media.art) return thumb_url + + +class PlexMediaSearchResult: + """Represents results from a Plex media media_content_id search. + + Results are used by media_player.play_media implementations. + """ + + def __init__(self, media, params=None) -> None: + """Initialize the result.""" + self.media = media + self._params = params or {} + + @property + def offset(self) -> int: + """Provide the appropriate offset based on payload contents.""" + if offset := self._params.get("offset", 0): + return offset * 1000 + resume = self._params.get("resume", False) + if isinstance(resume, str): + resume = bool(strtobool(resume)) + if resume: + return self.media.viewOffset + return 0 + + @property + def shuffle(self) -> bool: + """Return value of shuffle parameter.""" + shuffle = self._params.get("shuffle", False) + if isinstance(shuffle, str): + shuffle = bool(strtobool(shuffle)) + return shuffle diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 280152b972f..5a6dd657942 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -4,6 +4,7 @@ import logging from plexapi.exceptions import NotFound import voluptuous as vol +from yarl import URL from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError @@ -12,11 +13,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( DOMAIN, PLEX_UPDATE_PLATFORMS_SIGNAL, + PLEX_URI_SCHEME, SERVERS, SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) from .errors import MediaNotFound +from .models import PlexMediaSearchResult REFRESH_LIBRARY_SCHEMA = vol.Schema( {vol.Optional("server_name"): str, vol.Required("library_name"): str} @@ -73,7 +76,7 @@ def refresh_library(hass: HomeAssistant, service_call: ServiceCall) -> None: library.update() -def get_plex_server(hass, plex_server_name=None): +def get_plex_server(hass, plex_server_name=None, plex_server_id=None): """Retrieve a configured Plex server by name.""" if DOMAIN not in hass.data: raise HomeAssistantError("Plex integration not configured") @@ -81,6 +84,9 @@ def get_plex_server(hass, plex_server_name=None): if not plex_servers: raise HomeAssistantError("No Plex servers available") + if plex_server_id: + return hass.data[DOMAIN][SERVERS][plex_server_id] + if plex_server_name: plex_server = next( (x for x in plex_servers if x.friendly_name == plex_server_name), None @@ -101,30 +107,69 @@ def get_plex_server(hass, plex_server_name=None): ) -def lookup_plex_media(hass, content_type, content_id): - """Look up Plex media for other integrations using media_player.play_media service payloads.""" - content = json.loads(content_id) +def process_plex_payload( + hass, content_type, content_id, default_plex_server=None, supports_playqueues=True +) -> PlexMediaSearchResult: + """Look up Plex media using media_player.play_media service payloads.""" + plex_server = default_plex_server + + if content_id.startswith(PLEX_URI_SCHEME + "{"): + # Handle the special payload of 'plex://{}' + content_id = content_id[len(PLEX_URI_SCHEME) :] + content = json.loads(content_id) + elif content_id.startswith(PLEX_URI_SCHEME): + # Handle standard media_browser payloads + plex_url = URL(content_id) + if plex_url.name: + if len(plex_url.parts) == 2: + # The path contains a single item, will always be a ratingKey + content = int(plex_url.name) + else: + # For "special" items like radio stations + content = plex_url.path + server_id = plex_url.host + plex_server = get_plex_server(hass, plex_server_id=server_id) + else: + # Handle legacy payloads without server_id in URL host position + content = int(plex_url.host) # type: ignore[arg-type] + else: + content = json.loads(content_id) + + if isinstance(content, dict): + if plex_server_name := content.pop("plex_server", None): + plex_server = get_plex_server(hass, plex_server_name) + + if not plex_server: + plex_server = get_plex_server(hass) + + if content_type == "station": + if not supports_playqueues: + raise HomeAssistantError("Plex stations are not supported on this device") + playqueue = plex_server.create_station_playqueue(content) + return PlexMediaSearchResult(playqueue) if isinstance(content, int): content = {"plex_key": content} content_type = DOMAIN - plex_server_name = content.pop("plex_server", None) - plex_server = get_plex_server(hass, plex_server_name) - if playqueue_id := content.pop("playqueue_id", None): + if not supports_playqueues: + raise HomeAssistantError("Plex playqueues are not supported on this device") try: playqueue = plex_server.get_playqueue(playqueue_id) except NotFound as err: raise MediaNotFound( f"PlayQueue '{playqueue_id}' could not be found" ) from err - return playqueue + return PlexMediaSearchResult(playqueue, content) - shuffle = content.pop("shuffle", 0) - media = plex_server.lookup_media(content_type, **content) + search_query = content.copy() + shuffle = search_query.pop("shuffle", 0) - if shuffle: - return plex_server.create_playqueue(media, shuffle=shuffle) + media = plex_server.lookup_media(content_type, **search_query) - return media + if supports_playqueues and (isinstance(media, list) or shuffle): + playqueue = plex_server.create_playqueue(media, shuffle=shuffle) + return PlexMediaSearchResult(playqueue, content) + + return PlexMediaSearchResult(media, content) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4b1c9b82448..16d805a578a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations from asyncio import run_coroutine_threadsafe import datetime -import json import logging from typing import Any @@ -50,7 +49,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.components.plex.const import PLEX_URI_SCHEME -from homeassistant.components.plex.services import lookup_plex_media +from homeassistant.components.plex.services import process_plex_payload from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -567,20 +566,16 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco = self.coordinator.soco if media_id and media_id.startswith(PLEX_URI_SCHEME): plex_plugin = self.speaker.plex_plugin - media_id = media_id[len(PLEX_URI_SCHEME) :] - payload = json.loads(media_id) - if isinstance(payload, dict): - shuffle = payload.pop("shuffle", False) - else: - shuffle = False - media = lookup_plex_media(self.hass, media_type, json.dumps(payload)) - if shuffle: + result = process_plex_payload( + self.hass, media_type, media_id, supports_playqueues=False + ) + if result.shuffle: self.set_shuffle(True) if kwargs.get(ATTR_MEDIA_ENQUEUE): - plex_plugin.add_to_queue(media) + plex_plugin.add_to_queue(result.media) else: soco.clear_queue() - plex_plugin.add_to_queue(media) + plex_plugin.add_to_queue(result.media) soco.play_from_queue(0) return diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 502084c2090..316c095160c 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -1,6 +1,8 @@ """Tests for Plex media browser.""" from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import Mock, patch + +from yarl import URL from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -104,6 +106,7 @@ class MockPlexStation: title = "Radio Station" radio = True type = "playlist" + _server = Mock(machineIdentifier="unique_id_123") async def test_browse_media( @@ -138,7 +141,7 @@ async def test_browse_media( assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" assert ( result[ATTR_MEDIA_CONTENT_ID] - == PLEX_URI_SCHEME + DEFAULT_DATA[CONF_SERVER_IDENTIFIER] + == PLEX_URI_SCHEME + DEFAULT_DATA[CONF_SERVER_IDENTIFIER] + "/server" ) # Library Sections + Recommended + Playlists assert len(result["children"]) == len(mock_plex_server.library.sections()) + 2 @@ -162,7 +165,7 @@ async def test_browse_media( "entity_id": media_players[0], ATTR_MEDIA_CONTENT_TYPE: "server", ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME - + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}", + + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/server/{special_keys[0]}", } ) @@ -174,13 +177,14 @@ async def test_browse_media( assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" assert ( result[ATTR_MEDIA_CONTENT_ID] - == PLEX_URI_SCHEME + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}" + == PLEX_URI_SCHEME + + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/server/{special_keys[0]}" ) assert len(result["children"]) == 4 # Hardcoded in fixture - assert result["children"][0]["media_content_type"] == "mixed" - assert result["children"][1]["media_content_type"] == "album" - assert result["children"][2]["media_content_type"] == "clip" - assert result["children"][3]["media_content_type"] == "playlist" + assert result["children"][0]["media_content_type"] == "hub" + assert result["children"][1]["media_content_type"] == "hub" + assert result["children"][2]["media_content_type"] == "hub" + assert result["children"][3]["media_content_type"] == "hub" # Browse into a special folder (server): Continue Watching msg_id += 1 @@ -199,7 +203,7 @@ async def test_browse_media( assert msg["type"] == TYPE_RESULT assert msg["success"] result = msg["result"] - assert result[ATTR_MEDIA_CONTENT_TYPE] == "mixed" + assert result[ATTR_MEDIA_CONTENT_TYPE] == "hub" requests_mock.get( f"{mock_plex_server.url_in_use}/hubs/sections/3?includeStations=1", @@ -216,7 +220,7 @@ async def test_browse_media( "entity_id": media_players[0], ATTR_MEDIA_CONTENT_TYPE: "library", ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME - + f"{library_section_id}:{special_keys[0]}", + + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/{library_section_id}/{special_keys[0]}", } ) @@ -228,7 +232,8 @@ async def test_browse_media( assert result[ATTR_MEDIA_CONTENT_TYPE] == "library" assert ( result[ATTR_MEDIA_CONTENT_ID] - == PLEX_URI_SCHEME + f"{library_section_id}:{special_keys[0]}" + == PLEX_URI_SCHEME + + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/{library_section_id}/{special_keys[0]}" ) assert len(result["children"]) == 1 @@ -249,7 +254,7 @@ async def test_browse_media( assert msg["type"] == TYPE_RESULT assert msg["success"] result = msg["result"] - assert result[ATTR_MEDIA_CONTENT_TYPE] == "station" + assert result[ATTR_MEDIA_CONTENT_TYPE] == "hub" assert len(result["children"]) == 3 assert result["children"][0]["title"] == "Library Radio" @@ -271,7 +276,7 @@ async def test_browse_media( assert msg["success"] result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "library" - result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :]) + result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name) # All items in section + Hubs assert ( len(result["children"]) @@ -305,7 +310,7 @@ async def test_browse_media( assert msg["success"] result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "show" - result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :]) + result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name) assert result["title"] == mock_plex_server.fetch_item(result_id).title assert result["children"][0]["title"] == f"{mock_season.title} ({mock_season.year})" @@ -335,7 +340,7 @@ async def test_browse_media( assert msg["success"] result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "season" - result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :]) + result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name) assert ( result["title"] == f"{mock_season.parentTitle} - {mock_season.title} ({mock_season.year})" @@ -360,7 +365,7 @@ async def test_browse_media( assert msg["success"] result = msg["result"] - result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :]) + result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name) assert result[ATTR_MEDIA_CONTENT_TYPE] == "library" assert result["title"] == "Music" @@ -390,7 +395,7 @@ async def test_browse_media( assert mock_fetch.called assert msg["success"] result = msg["result"] - result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :]) + result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name) assert result[ATTR_MEDIA_CONTENT_TYPE] == "artist" assert result["title"] == mock_artist.title assert result["children"][0]["title"] == "Radio Station" @@ -411,7 +416,7 @@ async def test_browse_media( assert msg["success"] result = msg["result"] - result_id = int(result[ATTR_MEDIA_CONTENT_ID][len(PLEX_URI_SCHEME) :]) + result_id = int(URL(result[ATTR_MEDIA_CONTENT_ID]).name) assert result[ATTR_MEDIA_CONTENT_TYPE] == "album" assert ( result["title"] diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index e67249ea375..e90a64a5fe1 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -1,6 +1,6 @@ """Tests for Plex player playback methods/services.""" from http import HTTPStatus -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -11,14 +11,19 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, SERVICE_PLAY_MEDIA, ) +from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, PLEX_URI_SCHEME from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError +from .const import DEFAULT_DATA, PLEX_DIRECT_URL + class MockPlexMedia: """Minimal mock of plexapi media object.""" key = "key" + viewOffset = 333 + _server = Mock(_baseurl=PLEX_DIRECT_URL) def __init__(self, title, mediatype): """Initialize the instance.""" @@ -51,7 +56,9 @@ async def test_media_player_playback( media_player = "media_player.plex_plex_web_chrome" requests_mock.post("/playqueues", text=playqueue_created) - requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK) + playmedia_mock = requests_mock.get( + "/player/playback/playMedia", status_code=HTTPStatus.OK + ) # Test media lookup failure payload = '{"library_name": "Movies", "title": "Movie 1" }' @@ -67,6 +74,7 @@ async def test_media_player_playback( }, True, ) + assert not playmedia_mock.called assert f"No {MEDIA_TYPE_MOVIE} results in 'Movies' for" in str(excinfo.value) movie1 = MockPlexMedia("Movie", "movie") @@ -86,12 +94,57 @@ async def test_media_player_playback( }, True, ) + assert playmedia_mock.called + + # Test movie success with resume + playmedia_mock.reset() + with patch("plexapi.library.LibrarySection.search", return_value=movies): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1", "resume": true}', + }, + True, + ) + assert playmedia_mock.called + assert playmedia_mock.last_request.qs["offset"][0] == str(movie1.viewOffset) + + # Test movie success with media browser URL + playmedia_mock.reset() + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME + + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/1", + }, + True, + ) + assert playmedia_mock.called + + # Test movie success with legacy media browser URL + playmedia_mock.reset() + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME + "1", + }, + True, + ) + assert playmedia_mock.called # Test multiple choices with exact match + playmedia_mock.reset() movies = [movie1, movie2] - with patch("plexapi.library.LibrarySection.search", return_value=movies), patch( - "homeassistant.components.plex.server.PlexServer.create_playqueue" - ) as mock_create_playqueue: + with patch("plexapi.library.LibrarySection.search", return_value=movies): assert await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -102,9 +155,10 @@ async def test_media_player_playback( }, True, ) - assert mock_create_playqueue.call_args.args == (movie1,) + assert playmedia_mock.called # Test multiple choices without exact match + playmedia_mock.reset() movies = [movie2, movie3] with pytest.raises(HomeAssistantError) as excinfo: payload = '{"library_name": "Movies", "title": "Movie" }' @@ -119,6 +173,7 @@ async def test_media_player_playback( }, True, ) + assert not playmedia_mock.called assert "Multiple matches, make content_id more specific" in str(excinfo.value) # Test multiple choices with allow_multiple @@ -137,3 +192,20 @@ async def test_media_player_playback( True, ) assert mock_create_playqueue.call_args.args == (movies,) + assert playmedia_mock.called + + # Test radio station + playmedia_mock.reset() + radio_id = "/library/sections/3/stations/1" + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: "station", + ATTR_MEDIA_CONTENT_ID: PLEX_URI_SCHEME + + f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}/{radio_id}", + }, + True, + ) + assert playmedia_mock.called diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index ddc4ed58ba8..f579fd90f90 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -13,10 +13,11 @@ from homeassistant.components.plex.const import ( CONF_SERVER_IDENTIFIER, DOMAIN, PLEX_SERVER_CONFIG, + PLEX_URI_SCHEME, SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) -from homeassistant.components.plex.services import lookup_plex_media +from homeassistant.components.plex.services import process_plex_payload from homeassistant.const import CONF_URL from homeassistant.exceptions import HomeAssistantError @@ -121,19 +122,25 @@ async def test_lookup_media_for_other_integrations( playqueue_created, ): """Test media lookup for media_player.play_media calls from cast/sonos.""" - CONTENT_ID = '{"library_name": "Music", "artist_name": "Artist"}' - CONTENT_ID_KEY = "100" - CONTENT_ID_BAD_MEDIA = '{"library_name": "Music", "artist_name": "Not an Artist"}' - CONTENT_ID_PLAYQUEUE = '{"playqueue_id": 1234}' - CONTENT_ID_BAD_PLAYQUEUE = '{"playqueue_id": 1235}' - CONTENT_ID_SERVER = '{"plex_server": "Plex Server 1", "library_name": "Music", "artist_name": "Artist"}' + CONTENT_ID = PLEX_URI_SCHEME + '{"library_name": "Music", "artist_name": "Artist"}' + CONTENT_ID_KEY = PLEX_URI_SCHEME + "100" + CONTENT_ID_BAD_MEDIA = ( + PLEX_URI_SCHEME + '{"library_name": "Music", "artist_name": "Not an Artist"}' + ) + CONTENT_ID_PLAYQUEUE = PLEX_URI_SCHEME + '{"playqueue_id": 1234}' + CONTENT_ID_BAD_PLAYQUEUE = PLEX_URI_SCHEME + '{"playqueue_id": 1235}' + CONTENT_ID_SERVER = ( + PLEX_URI_SCHEME + + '{"plex_server": "Plex Server 1", "library_name": "Music", "artist_name": "Artist"}' + ) CONTENT_ID_SHUFFLE = ( - '{"library_name": "Music", "artist_name": "Artist", "shuffle": 1}' + PLEX_URI_SCHEME + + '{"library_name": "Music", "artist_name": "Artist", "shuffle": 1}' ) # Test with no Plex integration available with pytest.raises(HomeAssistantError) as excinfo: - lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) + process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) assert "Plex integration not configured" in str(excinfo.value) with patch( @@ -145,45 +152,61 @@ async def test_lookup_media_for_other_integrations( # Test with no Plex servers available with pytest.raises(HomeAssistantError) as excinfo: - lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) + process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) assert "No Plex servers available" in str(excinfo.value) # Complete setup of a Plex server await hass.config_entries.async_unload(entry.entry_id) await setup_plex_server() - # Test lookup success - result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID) - assert isinstance(result, plexapi.audio.Artist) + # Test lookup success without playqueue + result = process_plex_payload( + hass, MEDIA_TYPE_MUSIC, CONTENT_ID, supports_playqueues=False + ) + assert isinstance(result.media, plexapi.audio.Artist) + assert not result.shuffle - # Test media key payload - result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_KEY) - assert isinstance(result, plexapi.audio.Track) + # Test media key payload without playqueue + result = process_plex_payload( + hass, MEDIA_TYPE_MUSIC, CONTENT_ID_KEY, supports_playqueues=False + ) + assert isinstance(result.media, plexapi.audio.Track) + assert not result.shuffle - # Test with specified server - result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SERVER) - assert isinstance(result, plexapi.audio.Artist) + # Test with specified server without playqueue + result = process_plex_payload( + hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SERVER, supports_playqueues=False + ) + assert isinstance(result.media, plexapi.audio.Artist) + assert not result.shuffle + + # Test shuffle without playqueue + result = process_plex_payload( + hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE, supports_playqueues=False + ) + assert isinstance(result.media, plexapi.audio.Artist) + assert result.shuffle # Test with media not found with patch("plexapi.library.LibrarySection.search", return_value=None): with pytest.raises(HomeAssistantError) as excinfo: - lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA) + process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_MEDIA) assert f"No {MEDIA_TYPE_MUSIC} results in 'Music' for" in str(excinfo.value) # Test with playqueue requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) - result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_PLAYQUEUE) - assert isinstance(result, plexapi.playqueue.PlayQueue) + result = process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_PLAYQUEUE) + assert isinstance(result.media, plexapi.playqueue.PlayQueue) # Test with invalid playqueue requests_mock.get( "https://1.2.3.4:32400/playQueues/1235", status_code=HTTPStatus.NOT_FOUND ) with pytest.raises(HomeAssistantError) as excinfo: - lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_PLAYQUEUE) + process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_BAD_PLAYQUEUE) assert "PlayQueue '1235' could not be found" in str(excinfo.value) # Test playqueue is created with shuffle requests_mock.post("/playqueues", text=playqueue_created) - result = lookup_plex_media(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE) - assert isinstance(result, plexapi.playqueue.PlayQueue) + result = process_plex_payload(hass, MEDIA_TYPE_MUSIC, CONTENT_ID_SHUFFLE) + assert isinstance(result.media, plexapi.playqueue.PlayQueue) diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py index 4c7c6f4f94a..27a9f652c95 100644 --- a/tests/components/sonos/test_plex_playback.py +++ b/tests/components/sonos/test_plex_playback.py @@ -1,5 +1,6 @@ """Tests for the Sonos Media Player platform.""" -from unittest.mock import patch +import json +from unittest.mock import Mock, patch import pytest @@ -10,23 +11,25 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.plex.const import PLEX_URI_SCHEME +from homeassistant.components.plex.const import DOMAIN as PLEX_DOMAIN, PLEX_URI_SCHEME from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError async def test_plex_play_media(hass, async_autosetup_sonos): """Test playing media via the Plex integration.""" + mock_plex_server = Mock() + mock_lookup = mock_plex_server.lookup_media + media_player = "media_player.zone_a" media_content_id = ( '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}' ) with patch( - "homeassistant.components.sonos.media_player.lookup_plex_media" - ) as mock_lookup, patch( - "soco.plugins.plex.PlexPlugin.add_to_queue" - ) as mock_add_to_queue, patch( + "homeassistant.components.plex.services.get_plex_server", + return_value=mock_plex_server, + ), patch("soco.plugins.plex.PlexPlugin.add_to_queue") as mock_add_to_queue, patch( "homeassistant.components.sonos.media_player.SonosMediaPlayerEntity.set_shuffle" ) as mock_shuffle: # Test successful Plex service call @@ -44,8 +47,8 @@ async def test_plex_play_media(hass, async_autosetup_sonos): assert len(mock_lookup.mock_calls) == 1 assert len(mock_add_to_queue.mock_calls) == 1 assert not mock_shuffle.called - assert mock_lookup.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC - assert mock_lookup.mock_calls[0][1][2] == media_content_id + assert mock_lookup.mock_calls[0][1][0] == MEDIA_TYPE_MUSIC + assert mock_lookup.mock_calls[0][2] == json.loads(media_content_id) # Test handling shuffle in payload mock_lookup.reset_mock() @@ -66,8 +69,8 @@ async def test_plex_play_media(hass, async_autosetup_sonos): assert mock_shuffle.called assert len(mock_lookup.mock_calls) == 1 assert len(mock_add_to_queue.mock_calls) == 1 - assert mock_lookup.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC - assert mock_lookup.mock_calls[0][1][2] == media_content_id + assert mock_lookup.mock_calls[0][1][0] == MEDIA_TYPE_MUSIC + assert mock_lookup.mock_calls[0][2] == json.loads(media_content_id) # Test failed Plex service call mock_lookup.reset_mock() @@ -87,3 +90,32 @@ async def test_plex_play_media(hass, async_autosetup_sonos): ) assert mock_lookup.called assert not mock_add_to_queue.called + + # Test new media browser payload format + mock_lookup.reset_mock() + mock_lookup.side_effect = None + mock_add_to_queue.reset_mock() + + server_id = "unique_id_123" + plex_item_key = 300 + + with patch( + "homeassistant.components.plex.services.get_plex_server", + return_value=mock_plex_server, + ): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{server_id}/{plex_item_key}?shuffle=1", + }, + blocking=True, + ) + + assert len(mock_lookup.mock_calls) == 1 + assert len(mock_add_to_queue.mock_calls) == 1 + assert mock_shuffle.called + assert mock_lookup.mock_calls[0][1][0] == PLEX_DOMAIN + assert mock_lookup.mock_calls[0][2] == {"plex_key": plex_item_key} From 247af2e74fec9da1634f1010149fbd46bc428783 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 21 Mar 2022 23:49:18 +0100 Subject: [PATCH 0593/1054] Improve recorder setup in tests (#68333) Co-authored-by: J. Nick Koston --- tests/components/demo/test_init.py | 21 +++++++++++---------- tests/conftest.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index a446856de7b..f25d1a57b34 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -7,11 +7,12 @@ import pytest from homeassistant.components.demo import DOMAIN from homeassistant.components.device_tracker.legacy import YAML_DEVICES +from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import list_statistic_ids from homeassistant.helpers.json import JSONEncoder -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component -from tests.components.recorder.common import wait_recording_done +from tests.components.recorder.common import async_wait_recording_done_without_instance @pytest.fixture(autouse=True) @@ -45,16 +46,16 @@ async def test_setting_up_demo(hass): ) -def test_demo_statistics(hass_recorder): +async def test_demo_statistics(hass, recorder_mock): """Test that the demo components makes some statistics available.""" - hass = hass_recorder() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done_without_instance(hass) - assert setup_component(hass, DOMAIN, {DOMAIN: {}}) - hass.block_till_done() - hass.start() - wait_recording_done(hass) - - statistic_ids = list_statistic_ids(hass) + statistic_ids = await get_instance(hass).async_add_executor_job( + list_statistic_ids, hass + ) assert { "name": None, "source": "demo", diff --git a/tests/conftest.py b/tests/conftest.py index a7dcef31591..9f19415cafa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,6 +41,7 @@ from tests.common import ( # noqa: E402, isort:skip MockConfigEntry, MockUser, async_fire_mqtt_message, + async_init_recorder_component, async_test_home_assistant, get_test_home_assistant, init_recorder_component, @@ -821,6 +822,28 @@ def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage): hass.stop() +@pytest.fixture +async def recorder_mock(enable_nightly_purge, enable_statistics, hass): + """Fixture with in-memory recorder.""" + stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None + with patch( + "homeassistant.components.recorder.Recorder.async_periodic_statistics", + side_effect=stats, + autospec=True, + ), patch( + "homeassistant.components.recorder.Recorder.async_nightly_tasks", + side_effect=nightly, + autospec=True, + ): + await async_init_recorder_component(hass) + await hass.async_start() + await hass.async_block_till_done() + await hass.async_add_executor_job( + hass.data[recorder.DATA_INSTANCE].block_till_done + ) + + @pytest.fixture def mock_integration_frame(): """Mock as if we're calling code from inside an integration.""" From a43505a0a37c2a48ee15ff95a95567cdd2db0524 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Mon, 21 Mar 2022 18:56:53 -0400 Subject: [PATCH 0594/1054] Add PECO power outage counter integration (#65194) * Create a new NWS Alerts integration * Create a new NWS Alerts integration * Create new PECO integration * Remove empty keys * Revert "Create a new NWS Alerts integration" This reverts commit 38309c5a878d78f26df56a62e56cb602d9dc9a9e. * Revert "Create a new NWS Alerts integration" This reverts commit aeabdd37b86c370bdb8009e885806bdac6e464d8. * Fix test with new mock data * Add init and sensor to .coveragerc and more tests for config flow * Small fixes and replacing patch with pytest.raises in testing invalid county * Add type defs and fix test_config_flow to use MultipleValid instead * Fix issues with 'typing.Dict' * Move API communication to seperate PyPI library * Switch PyPI library from httpx to aiohttp to allow for passing in websessions * Commit file changes requested by farmio as listed here: https://github.com/home-assistant/core/pull/65194/files/d267e4300a4d359d88ef33e43b66d0e961ac154d * Add suggestions requested by farmio as listed here: https://github.com/home-assistant/core/pull/65194/files/586d8ffa42d7860d91e25fb82b2d6eace6645a82 * Move native_unit_of_measurement from prop to attr * Update PLATFORMS constant type annotation Co-authored-by: Matthias Alphart * Add peco to .strict-typing I am from school so I can't run mypy atm * Forgot to import Final * Do as requested [here](https://github.com/home-assistant/core/runs/5070634928?check_suite_focus=true) * Updated mypy.ini, checks should pass now * Fix to conform to mypy restrictions https://github.com/home-assistant/core/runs/5072861837\?check_suite_focus\=true * Fix type annotations * Fix tests * Use cast in async_update_data * Add data type to CoordinatorEntity and DataUpdateCoordinator * More cleanup from suggestions here: https://github.com/home-assistant/core/pull/65194\#pullrequestreview-908183493 * Fix tests for new code * Cleaning up a speck of dust * Remove unused variable from the peco sensor * Add rounding to percentage, and extra clean-up * Final suggestions from @farmio * Update SCAN_INTERVAL to be a little bit faster * Change the SCAN_INTERVAL to be somewhat near the update interval of the outage map, as noted by farmio * New UpdateCoordinator typing --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/peco/__init__.py | 31 +++ homeassistant/components/peco/config_flow.py | 43 +++ homeassistant/components/peco/const.py | 17 ++ homeassistant/components/peco/manifest.json | 13 + homeassistant/components/peco/sensor.py | 113 ++++++++ homeassistant/components/peco/strings.json | 16 ++ .../components/peco/translations/en.json | 16 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/peco/__init__.py | 1 + tests/components/peco/test_config_flow.py | 57 ++++ tests/components/peco/test_init.py | 38 +++ tests/components/peco/test_sensor.py | 256 ++++++++++++++++++ 17 files changed, 622 insertions(+) create mode 100644 homeassistant/components/peco/__init__.py create mode 100644 homeassistant/components/peco/config_flow.py create mode 100644 homeassistant/components/peco/const.py create mode 100644 homeassistant/components/peco/manifest.json create mode 100644 homeassistant/components/peco/sensor.py create mode 100644 homeassistant/components/peco/strings.json create mode 100644 homeassistant/components/peco/translations/en.json create mode 100644 tests/components/peco/__init__.py create mode 100644 tests/components/peco/test_config_flow.py create mode 100644 tests/components/peco/test_init.py create mode 100644 tests/components/peco/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 7b1fc47b3a3..cf77894f77f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -151,6 +151,7 @@ homeassistant.components.oncue.* homeassistant.components.onewire.* homeassistant.components.open_meteo.* homeassistant.components.openuv.* +homeassistant.components.peco.* homeassistant.components.overkiz.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* diff --git a/CODEOWNERS b/CODEOWNERS index 24a7793f6a6..6ce57254f5a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -741,6 +741,8 @@ homeassistant/components/panel_custom/* @home-assistant/frontend tests/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend tests/components/panel_iframe/* @home-assistant/frontend +homeassistant/components/peco/* @IceBotYT +tests/components/peco/* @IceBotYT homeassistant/components/persistent_notification/* @home-assistant/core tests/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py new file mode 100644 index 00000000000..86fe8c82d90 --- /dev/null +++ b/homeassistant/components/peco/__init__.py @@ -0,0 +1,31 @@ +"""The PECO Outage Counter integration.""" +from __future__ import annotations + +from typing import Final + +import peco + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: Final = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up PECO Outage Counter from a config entry.""" + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = peco.PecoOutageApi() + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/peco/config_flow.py b/homeassistant/components/peco/config_flow.py new file mode 100644 index 00000000000..1d3ab9cb5d3 --- /dev/null +++ b/homeassistant/components/peco/config_flow.py @@ -0,0 +1,43 @@ +"""Config flow for PECO Outage Counter integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_COUNTY, COUNTY_LIST, DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_COUNTY): vol.In(COUNTY_LIST), + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for PECO Outage Counter.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + county = user_input[ + CONF_COUNTY + ] # Voluptuous automatically detects if the county is invalid + + await self.async_set_unique_id(county) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"{county.capitalize()} Outage Count", data=user_input + ) diff --git a/homeassistant/components/peco/const.py b/homeassistant/components/peco/const.py new file mode 100644 index 00000000000..5f49ecf2898 --- /dev/null +++ b/homeassistant/components/peco/const.py @@ -0,0 +1,17 @@ +"""Constants for the PECO Outage Counter integration.""" +import logging +from typing import Final + +DOMAIN: Final = "peco" +LOGGER: Final = logging.getLogger(__package__) +COUNTY_LIST: Final = [ + "BUCKS", + "CHESTER", + "DELAWARE", + "MONTGOMERY", + "PHILADELPHIA", + "YORK", + "TOTAL", +] +SCAN_INTERVAL: Final = 9 +CONF_COUNTY: Final = "county" diff --git a/homeassistant/components/peco/manifest.json b/homeassistant/components/peco/manifest.json new file mode 100644 index 00000000000..2a577faddbf --- /dev/null +++ b/homeassistant/components/peco/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "peco", + "name": "PECO Outage Counter", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/peco", + "codeowners": [ + "@IceBotYT" + ], + "iot_class": "cloud_polling", + "requirements": [ + "peco==0.0.21" + ] +} \ No newline at end of file diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py new file mode 100644 index 00000000000..d4490cd5a9b --- /dev/null +++ b/homeassistant/components/peco/sensor.py @@ -0,0 +1,113 @@ +"""Sensor component for PECO outage counter.""" +import asyncio +from datetime import timedelta +from typing import Final, cast + +from peco import BadJSONError, HttpError + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import CONF_COUNTY, DOMAIN, LOGGER, SCAN_INTERVAL + +PARALLEL_UPDATES: Final = 0 +SENSOR_LIST = ( + SensorEntityDescription(key="customers_out", name="Customers Out"), + SensorEntityDescription( + key="percent_customers_out", + name="Percent Customers Out", + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription(key="outage_count", name="Outage Count"), + SensorEntityDescription(key="customers_served", name="Customers Served"), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + api = hass.data[DOMAIN][config_entry.entry_id] + websession = async_get_clientsession(hass) + county: str = config_entry.data[CONF_COUNTY] + + async def async_update_data() -> dict[str, float]: + """Fetch data from API.""" + try: + data = ( + cast(dict[str, float], await api.get_outage_totals(websession)) + if county == "TOTAL" + else cast( + dict[str, float], + await api.get_outage_count(county, websession), + ) + ) + except HttpError as err: + raise UpdateFailed(f"Error fetching data: {err}") from err + except BadJSONError as err: + raise UpdateFailed(f"Error parsing data: {err}") from err + except asyncio.TimeoutError as err: + raise UpdateFailed(f"Timeout fetching data: {err}") from err + if data["percent_customers_out"] < 5: + percent_out = round( + data["customers_out"] / data["customers_served"] * 100, 3 + ) + data["percent_customers_out"] = percent_out + return data + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="PECO Outage Count", + update_method=async_update_data, + update_interval=timedelta(minutes=SCAN_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + async_add_entities( + [PecoSensor(sensor, county, coordinator) for sensor in SENSOR_LIST], + True, + ) + return + + +class PecoSensor( + CoordinatorEntity[DataUpdateCoordinator[dict[str, float]]], SensorEntity +): + """PECO outage counter sensor.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_icon: str = "mdi:power-plug-off" + + def __init__( + self, + description: SensorEntityDescription, + county: str, + coordinator: DataUpdateCoordinator[dict[str, float]], + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._attr_name = f"{county.capitalize()} {description.name}" + self._attr_unique_id = f"{county}-{description.key}" + self.entity_description = description + + @property + def native_value(self) -> float: + """Return the value of the sensor.""" + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/peco/strings.json b/homeassistant/components/peco/strings.json new file mode 100644 index 00000000000..2d195ed5d22 --- /dev/null +++ b/homeassistant/components/peco/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "title": "PECO Outage Counter", + "description": "Please choose your county below.", + "data": { + "county": "County" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/en.json b/homeassistant/components/peco/translations/en.json new file mode 100644 index 00000000000..60483a1d65c --- /dev/null +++ b/homeassistant/components/peco/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "step": { + "user": { + "data": { + "county": "County" + }, + "description": "Please choose your county below.", + "title": "PECO Outage Counter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6a668b0d666..058bb936c7b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -249,6 +249,7 @@ FLOWS = { "owntracks", "p1_monitor", "panasonic_viera", + "peco", "philips_js", "pi_hole", "picnic", diff --git a/mypy.ini b/mypy.ini index b4d51d7f4e1..2f1792438a5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1463,6 +1463,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.peco.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.overkiz.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 64332f81b7e..013576a33db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1177,6 +1177,9 @@ panasonic_viera==0.3.6 # homeassistant.components.dunehd pdunehd==1.3.2 +# homeassistant.components.peco +peco==0.0.21 + # homeassistant.components.pencom pencompy==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f5cf484ee0..148384e0fdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -768,6 +768,9 @@ panasonic_viera==0.3.6 # homeassistant.components.dunehd pdunehd==1.3.2 +# homeassistant.components.peco +peco==0.0.21 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora diff --git a/tests/components/peco/__init__.py b/tests/components/peco/__init__.py new file mode 100644 index 00000000000..090a46cdf7b --- /dev/null +++ b/tests/components/peco/__init__.py @@ -0,0 +1 @@ +"""Tests for the PECO Outage Counter integration.""" diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py new file mode 100644 index 00000000000..bd209b54b1d --- /dev/null +++ b/tests/components/peco/test_config_flow.py @@ -0,0 +1,57 @@ +"""Test the PECO Outage Counter config flow.""" +from unittest.mock import patch + +from pytest import raises +from voluptuous.error import MultipleInvalid + +from homeassistant import config_entries +from homeassistant.components.peco.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.peco.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "county": "PHILADELPHIA", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Philadelphia Outage Count" + assert result2["data"] == { + "county": "PHILADELPHIA", + } + + +async def test_invalid_county(hass: HomeAssistant) -> None: + """Test if the InvalidCounty error works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with raises(MultipleInvalid): + await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "county": "INVALID_COUNTY_THAT_SHOULD_NOT_EXIST", + }, + ) + await hass.async_block_till_done() + + # it should have errored, instead of returning an errors dict, since this error should never happen diff --git a/tests/components/peco/test_init.py b/tests/components/peco/test_init.py new file mode 100644 index 00000000000..e7db50e653c --- /dev/null +++ b/tests/components/peco/test_init.py @@ -0,0 +1,38 @@ +"""Test the PECO Outage Counter init file.""" +from peco import PecoOutageApi + +from homeassistant.components.peco.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_ENTRY_DATA = {"county": "TOTAL"} +INVALID_COUNTY_DATA = {"county": "INVALID_COUNTY_THAT_SHOULD_NOT_EXIST", "test": True} + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test the unload entry.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state == ConfigEntryState.NOT_LOADED + + +async def test_data(hass: HomeAssistant) -> None: + """Test the data.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + assert hass.data[DOMAIN] + assert isinstance(hass.data[DOMAIN][config_entry.entry_id], PecoOutageApi) diff --git a/tests/components/peco/test_sensor.py b/tests/components/peco/test_sensor.py new file mode 100644 index 00000000000..ea879c8f07a --- /dev/null +++ b/tests/components/peco/test_sensor.py @@ -0,0 +1,256 @@ +"""Test the PECO Outage Counter sensors.""" +import asyncio + +from homeassistant.components.peco.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +MOCK_ENTRY_DATA = {"county": "TOTAL"} +COUNTY_ENTRY_DATA = {"county": "BUCKS"} +INVALID_COUNTY_DATA = {"county": "INVALID"} + + +async def test_sensor_available( + aioclient_mock: AiohttpClientMocker, hass: HomeAssistant +) -> None: + """Test that the sensors are working.""" + # Totals Test + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + json={"data": {"interval_generation_data": "data/TEST"}}, + ) + aioclient_mock.get( + "https://kubra.io/data/TEST/public/reports/a36a6292-1c55-44de-a6a9-44fedf9482ee_report.json", + json={ + "file_data": { + "totals": { + "cust_a": { + "val": 123, + }, + "percent_cust_a": { + "val": 1.23, + }, + "n_out": 456, + "cust_s": 789, + } + } + }, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert config_entry.state == ConfigEntryState.LOADED + + sensors_to_get = [ + "total_customers_out", + "total_percent_customers_out", + "total_outage_count", + "total_customers_served", + ] + + for sensor in sensors_to_get: + sensor_entity = hass.states.get(f"sensor.{sensor}") + assert sensor_entity is not None + assert sensor_entity.state != "unavailable" + + if sensor == "total_customers_out": + assert sensor_entity.state == "123" + elif sensor == "total_percent_customers_out": + assert sensor_entity.state == "15.589" + elif sensor == "total_outage_count": + assert sensor_entity.state == "456" + elif sensor == "total_customers_served": + assert sensor_entity.state == "789" + + # County Test + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + json={"data": {"interval_generation_data": "data/TEST"}}, + ) + aioclient_mock.get( + "https://kubra.io/data/TEST/public/reports/a36a6292-1c55-44de-a6a9-44fedf9482ee_report.json", + json={ + "file_data": { + "areas": [ + { + "name": "BUCKS", + "cust_a": { + "val": 123, + }, + "percent_cust_a": { + "val": 1.23, + }, + "n_out": 456, + "cust_s": 789, + } + ] + } + }, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + assert config_entry.state == ConfigEntryState.LOADED + + sensors_to_get = [ + "bucks_customers_out", + "bucks_percent_customers_out", + "bucks_outage_count", + "bucks_customers_served", + ] + + for sensor in sensors_to_get: + sensor_entity = hass.states.get(f"sensor.{sensor}") + assert sensor_entity is not None + assert sensor_entity.state != "unavailable" + + if sensor == "bucks_customers_out": + assert sensor_entity.state == "123" + elif sensor == "bucks_percent_customers_out": + assert sensor_entity.state == "15.589" + elif sensor == "bucks_outage_count": + assert sensor_entity.state == "456" + elif sensor == "bucks_customers_served": + assert sensor_entity.state == "789" + + +async def test_http_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +): + """Test if it raises an error when there is an HTTP error.""" + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + json={"data": {"interval_generation_data": "data/TEST"}}, + ) + aioclient_mock.get( + "https://kubra.io/data/TEST/public/reports/a36a6292-1c55-44de-a6a9-44fedf9482ee_report.json", + status=500, + json={"error": "Internal Server Error"}, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + assert "Error getting PECO outage counter data" in caplog.text + + +async def test_bad_json( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +): + """Test if it raises an error when there is bad JSON.""" + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + json={"data": {}}, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + assert "ConfigEntryNotReady" in caplog.text + + +async def test_total_http_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +): + """Test if it raises an error when there is an HTTP error.""" + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + json={"data": {"interval_generation_data": "data/TEST"}}, + ) + aioclient_mock.get( + "https://kubra.io/data/TEST/public/reports/a36a6292-1c55-44de-a6a9-44fedf9482ee_report.json", + status=500, + json={"error": "Internal Server Error"}, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + assert "Error getting PECO outage counter data" in caplog.text + + +async def test_total_bad_json( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +): + """Test if it raises an error when there is bad JSON.""" + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + json={"data": {}}, + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + assert "ConfigEntryNotReady" in caplog.text + + +async def test_update_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +): + """Test if it raises an error when there is a timeout.""" + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + exc=asyncio.TimeoutError(), + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=COUNTY_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + assert "Timeout fetching data" in caplog.text + + +async def test_total_update_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +): + """Test if it raises an error when there is a timeout.""" + aioclient_mock.get( + "https://kubra.io/stormcenter/api/v1/stormcenters/39e6d9f3-fdea-4539-848f-b8631945da6f/views/74de8a50-3f45-4f6a-9483-fd618bb9165d/currentState?preview=false", + exc=asyncio.TimeoutError(), + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + assert "Timeout fetching data" in caplog.text From 70a771b6ed00795ffe860f0c8754f2b7a345aa2d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 22 Mar 2022 04:40:33 +0100 Subject: [PATCH 0595/1054] Respect disable_new_entities for new device_tracker entities (#68148) --- .../components/device_tracker/config_entry.py | 12 ++++++- .../device_tracker/test_config_entry.py | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index adabd297c55..c9b8534c2bc 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -149,9 +149,19 @@ def _async_register_mac( return # Make sure entity has a config entry and was disabled by the - # default disable logic in the integration. + # default disable logic in the integration and new entities + # are allowed to be added. if ( entity_entry.config_entry_id is None + or ( + ( + config_entry := hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + ) + is not None + and config_entry.pref_disable_new_entities + ) or entity_entry.disabled_by != er.RegistryEntryDisabler.INTEGRATION ): return diff --git a/tests/components/device_tracker/test_config_entry.py b/tests/components/device_tracker/test_config_entry.py index 5134123074e..73b07d31026 100644 --- a/tests/components/device_tracker/test_config_entry.py +++ b/tests/components/device_tracker/test_config_entry.py @@ -137,6 +137,39 @@ async def test_register_mac(hass): assert entity_entry_1.disabled_by is None +async def test_register_mac_ignored(hass): + """Test ignoring registering a mac.""" + dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + + config_entry = MockConfigEntry(domain="test", pref_disable_new_entities=True) + config_entry.add_to_hass(hass) + + mac1 = "12:34:56:AB:CD:EF" + + entity_entry_1 = ent_reg.async_get_or_create( + "device_tracker", + "test", + mac1 + "yo1", + original_name="name 1", + config_entry=config_entry, + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + ) + + ce._async_register_mac(hass, "test", mac1, mac1 + "yo1") + + dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, mac1)}, + ) + + await hass.async_block_till_done() + + entity_entry_1 = ent_reg.async_get(entity_entry_1.entity_id) + + assert entity_entry_1.disabled_by == er.RegistryEntryDisabler.INTEGRATION + + async def test_connected_device_registered(hass): """Test dispatch on connected device being registered.""" From bcb8c7ec3cd38ce6d5627b4147aebd2329421c19 Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 22 Mar 2022 05:14:47 +0100 Subject: [PATCH 0596/1054] Add API endpoint get_statistics_metadata (#68471) Co-authored-by: Erik Montnemery --- homeassistant/components/history/__init__.py | 1 + .../components/recorder/statistics.py | 17 ++- .../components/recorder/websocket_api.py | 20 ++- homeassistant/components/sensor/recorder.py | 19 ++- .../components/recorder/test_websocket_api.py | 133 ++++++++++++++++++ 5 files changed, 176 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 10b50f40fd3..2bf285d25e6 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -171,6 +171,7 @@ async def ws_get_list_statistic_ids( statistic_ids = await get_instance(hass).async_add_executor_job( list_statistic_ids, hass, + None, msg.get("statistic_type"), ) connection.send_result(msg["id"], statistic_ids) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4154ae83055..df53bd55307 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -718,21 +718,22 @@ def update_statistics_metadata( def list_statistic_ids( hass: HomeAssistant, + statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, ) -> list[dict | None]: - """Return all statistic_ids and unit of measurement. + """Return all statistic_ids (or filtered one) and unit of measurement. Queries the database for existing statistic_ids, as well as integrations with a recorder platform for statistic_ids which will be added in the next statistics period. """ units = hass.config.units - statistic_ids = {} + result = {} # Query the database with session_scope(hass=hass) as session: metadata = get_metadata_with_session( - hass, session, statistic_type=statistic_type + hass, session, statistic_type=statistic_type, statistic_ids=statistic_ids ) for _, meta in metadata.values(): @@ -741,7 +742,7 @@ def list_statistic_ids( unit = _configured_unit(unit, units) meta["unit_of_measurement"] = unit - statistic_ids = { + result = { meta["statistic_id"]: { "name": meta["name"], "source": meta["source"], @@ -754,7 +755,9 @@ def list_statistic_ids( for platform in hass.data[DOMAIN].values(): if not hasattr(platform, "list_statistic_ids"): continue - platform_statistic_ids = platform.list_statistic_ids(hass, statistic_type) + platform_statistic_ids = platform.list_statistic_ids( + hass, statistic_ids=statistic_ids, statistic_type=statistic_type + ) for statistic_id, info in platform_statistic_ids.items(): if (unit := info["unit_of_measurement"]) is not None: @@ -763,7 +766,7 @@ def list_statistic_ids( platform_statistic_ids[statistic_id]["unit_of_measurement"] = unit for key, value in platform_statistic_ids.items(): - statistic_ids.setdefault(key, value) + result.setdefault(key, value) # Return a list of statistic_id + metadata return [ @@ -773,7 +776,7 @@ def list_statistic_ids( "source": info["source"], "unit_of_measurement": info["unit_of_measurement"], } - for _id, info in statistic_ids.items() + for _id, info in result.items() ] diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index ee5081b3c1c..a480439eaac 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -10,7 +10,7 @@ from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG -from .statistics import validate_statistics +from .statistics import list_statistic_ids, validate_statistics from .util import async_migration_in_progress if TYPE_CHECKING: @@ -24,6 +24,7 @@ def async_setup(hass: HomeAssistant) -> None: """Set up the recorder websocket API.""" websocket_api.async_register_command(hass, ws_validate_statistics) websocket_api.async_register_command(hass, ws_clear_statistics) + websocket_api.async_register_command(hass, ws_get_statistics_metadata) websocket_api.async_register_command(hass, ws_update_statistics_metadata) websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_backup_start) @@ -68,6 +69,23 @@ def ws_clear_statistics( connection.send_result(msg["id"]) +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/get_statistics_metadata", + vol.Optional("statistic_ids"): [str], + } +) +@websocket_api.async_response +async def ws_get_statistics_metadata( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Get metadata for a list of statistic_ids.""" + statistic_ids = await hass.async_add_executor_job( + list_statistic_ids, hass, msg.get("statistic_ids") + ) + connection.send_result(msg["id"], statistic_ids) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 635c5af6242..75e117d9834 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -596,11 +596,15 @@ def _compile_statistics( # noqa: C901 return result -def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) -> dict: - """Return statistic_ids and meta data.""" +def list_statistic_ids( + hass: HomeAssistant, + statistic_ids: list[str] | tuple[str] | None = None, + statistic_type: str | None = None, +) -> dict: + """Return all or filtered statistic_ids and meta data.""" entities = _get_sensor_states(hass) - statistic_ids = {} + result = {} for state in entities: state_class = state.attributes[ATTR_STATE_CLASS] @@ -611,6 +615,9 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - if statistic_type is not None and statistic_type not in provided_statistics: continue + if statistic_ids is not None and state.entity_id not in statistic_ids: + continue + if ( "sum" in provided_statistics and ATTR_LAST_RESET not in state.attributes @@ -619,7 +626,7 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - continue if device_class not in UNIT_CONVERSIONS: - statistic_ids[state.entity_id] = { + result[state.entity_id] = { "source": RECORDER_DOMAIN, "unit_of_measurement": native_unit, } @@ -629,12 +636,12 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - continue statistics_unit = DEVICE_CLASS_UNITS[device_class] - statistic_ids[state.entity_id] = { + result[state.entity_id] = { "source": RECORDER_DOMAIN, "unit_of_measurement": statistics_unit, } - return statistic_ids + return result def validate_statistics( diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 2a9f737e9a5..33478b76bcb 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -9,6 +9,7 @@ from pytest import approx from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -35,6 +36,16 @@ TEMPERATURE_SENSOR_ATTRIBUTES = { "state_class": "measurement", "unit_of_measurement": "°C", } +ENERGY_SENSOR_ATTRIBUTES = { + "device_class": "energy", + "state_class": "total", + "unit_of_measurement": "kWh", +} +GAS_SENSOR_ATTRIBUTES = { + "device_class": "gas", + "state_class": "total", + "unit_of_measurement": "m³", +} async def test_validate_statistics(hass, hass_ws_client): @@ -421,3 +432,125 @@ async def test_backup_end_without_start( response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "database_unlock_failed" + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (METRIC_SYSTEM, GAS_SENSOR_ATTRIBUTES, "m³"), + (METRIC_SYSTEM, ENERGY_SENSOR_ATTRIBUTES, "kWh"), + ], +) +async def test_get_statistics_metadata(hass, hass_ws_client, units, attributes, unit): + """Test get_statistics_metadata.""" + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {"history": {}}) + await async_setup_component(hass, "sensor", {}) + await async_init_recorder_component(hass) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json({"id": 1, "type": "recorder/get_statistics_metadata"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 8, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_gas", + "unit_of_measurement": unit, + } + + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test2", 10, attributes=attributes) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + + await client.send_json( + { + "id": 2, + "type": "recorder/get_statistics_metadata", + "statistic_ids": ["sensor.test"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": unit, + } + ] + + hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + # Remove the state, statistics will now be fetched from the database + hass.states.async_remove("sensor.test") + await hass.async_block_till_done() + + await client.send_json( + { + "id": 3, + "type": "recorder/get_statistics_metadata", + "statistic_ids": ["sensor.test"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": unit, + } + ] From cb011570e81c2e9fcbf39e30b76dd9f441ce5264 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Mar 2022 18:20:17 -1000 Subject: [PATCH 0597/1054] Seperate emonitor extra_state_attributes into their own sensors (#68479) --- homeassistant/components/emonitor/sensor.py | 91 ++++++++++++--------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index b99dd35093e..39a8c90f283 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -1,9 +1,12 @@ """Support for a Emonitor channel sensor.""" -from aioemonitor.monitor import EmonitorChannel +from __future__ import annotations + +from aioemonitor.monitor import EmonitorChannel, EmonitorStatus from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -21,6 +24,12 @@ from homeassistant.helpers.update_coordinator import ( from . import name_short_mac from .const import DOMAIN +SENSORS = ( + SensorEntityDescription(key="inst_power"), + SensorEntityDescription(key="avg_power", name="Average"), + SensorEntityDescription(key="max_power", name="Max"), +) + async def async_setup_entry( hass: HomeAssistant, @@ -30,7 +39,7 @@ async def async_setup_entry( """Set up entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] channels = coordinator.data.channels - entities = [] + entities: list[EmonitorPowerSensor] = [] seen_channels = set() for channel_number, channel in channels.items(): seen_channels.add(channel_number) @@ -39,7 +48,10 @@ async def async_setup_entry( if channel.paired_with_channel in seen_channels: continue - entities.append(EmonitorPowerSensor(coordinator, channel_number)) + entities.extend( + EmonitorPowerSensor(coordinator, description, channel_number) + for description in SENSORS + ) async_add_entities(entities) @@ -51,59 +63,60 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): _attr_native_unit_of_measurement = POWER_WATT _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, coordinator: DataUpdateCoordinator, channel_number: int) -> None: + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: SensorEntityDescription, + channel_number: int, + ) -> None: """Initialize the channel sensor.""" + self.entity_description = description self.channel_number = channel_number super().__init__(coordinator) - self._attr_unique_id = f"{self.mac_address}_{channel_number}" + mac_address = self.emonitor_status.network.mac_address + if description.name: + self._attr_name = f"{self.channel_data.label} {description.name}" + self._attr_unique_id = f"{mac_address}_{channel_number}_{description.key}" + else: + self._attr_name = self.channel_data.label + self._attr_unique_id = f"{mac_address}_{channel_number}" + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, mac_address)}, + manufacturer="Powerhouse Dynamics, Inc.", + name=name_short_mac(mac_address[-6:]), + sw_version=self.emonitor_status.hardware.firmware_version, + ) + + @property + def channels(self) -> dict[int, EmonitorChannel]: + """Return the channels dict.""" + channels: dict[int, EmonitorChannel] = self.emonitor_status.channels + return channels @property def channel_data(self) -> EmonitorChannel: - """Channel data.""" - return self.coordinator.data.channels[self.channel_number] + """Return the channel data.""" + return self.channels[self.channel_number] @property - def paired_channel_data(self) -> EmonitorChannel: - """Channel data.""" - return self.coordinator.data.channels[self.channel_data.paired_with_channel] - - @property - def name(self) -> str: - """Name of the sensor.""" - return self.channel_data.label + def emonitor_status(self) -> EmonitorStatus: + """Return the EmonitorStatus.""" + return self.coordinator.data def _paired_attr(self, attr_name: str) -> float: """Cumulative attributes for channel and paired channel.""" - attr_val = getattr(self.channel_data, attr_name) - if self.channel_data.paired_with_channel: - attr_val += getattr(self.paired_channel_data, attr_name) + channel_data = self.channels[self.channel_number] + attr_val = getattr(channel_data, attr_name) + if paired_channel := channel_data.paired_with_channel: + attr_val += getattr(self.channels[paired_channel], attr_name) return attr_val @property def native_value(self) -> StateType: """State of the sensor.""" - return self._paired_attr("inst_power") + return self._paired_attr(self.entity_description.key) @property def extra_state_attributes(self) -> dict: """Return the device specific state attributes.""" - return { - "channel": self.channel_number, - "avg_power": self._paired_attr("avg_power"), - "max_power": self._paired_attr("max_power"), - } - - @property - def mac_address(self) -> str: - """Return the mac address of the device.""" - return self.coordinator.data.network.mac_address - - @property - def device_info(self) -> DeviceInfo: - """Return info about the emonitor device.""" - return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, - manufacturer="Powerhouse Dynamics, Inc.", - name=name_short_mac(self.mac_address[-6:]), - sw_version=self.coordinator.data.hardware.firmware_version, - ) + return {"channel": self.channel_number} From 06ebb0b8b3a3025161c34687eee79dd01432b787 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 21 Mar 2022 20:20:40 -1000 Subject: [PATCH 0598/1054] Add support for effects to tplink light strips (#65166) --- homeassistant/components/tplink/__init__.py | 2 +- homeassistant/components/tplink/light.py | 42 +++++++++++-- homeassistant/components/tplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tplink/__init__.py | 57 ++++++++++++++++- tests/components/tplink/test_light.py | 61 ++++++++++++++++++- 7 files changed, 156 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 33b03109cd8..10e621e6668 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device: SmartDevice = hass_data[entry.entry_id].device if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass_data.pop(entry.entry_id) - await device.protocol.close() # type: ignore[no-untyped-call] + await device.protocol.close() return unload_ok diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 4519ead4d09..3f8179f42f2 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -4,17 +4,19 @@ from __future__ import annotations import logging from typing import Any, cast -from kasa import SmartBulb +from kasa import SmartBulb, SmartLightStrip from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_ONOFF, + SUPPORT_EFFECT, SUPPORT_TRANSITION, LightEntity, ) @@ -42,7 +44,9 @@ async def async_setup_entry( """Set up switches.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] device = cast(SmartBulb, coordinator.device) - if device.is_bulb or device.is_light_strip or device.is_dimmer: + if device.is_light_strip: + async_add_entities([TPLinkSmartLightStrip(device, coordinator)]) + elif device.is_bulb or device.is_dimmer: async_add_entities([TPLinkSmartBulb(device, coordinator)]) @@ -59,14 +63,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Initialize the switch.""" super().__init__(device, coordinator) # For backwards compat with pyHS100 - if self.device.is_dimmer: + if device.is_dimmer: # Dimmers used to use the switch format since # pyHS100 treated them as SmartPlug but the old code # created them as lights # https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 self._attr_unique_id = legacy_device_id(device) else: - self._attr_unique_id = self.device.mac.replace(":", "").upper() + self._attr_unique_id = device.mac.replace(":", "").upper() @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: @@ -104,6 +108,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): await self.device.set_hsv(hue, sat, brightness, transition=transition) return + if ATTR_EFFECT in kwargs: + assert isinstance(self.device, SmartLightStrip) + await self.device.set_effect(kwargs[ATTR_EFFECT]) + return + # Fallback to adjusting brightness or turning the bulb on if brightness is not None: await self.device.set_brightness(brightness, transition=transition) @@ -175,3 +184,28 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): return COLOR_MODE_COLOR_TEMP return COLOR_MODE_BRIGHTNESS + + +class TPLinkSmartLightStrip(TPLinkSmartBulb): + """Representation of a TPLink Smart Light Strip.""" + + device: SmartLightStrip + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return super().supported_features | SUPPORT_EFFECT + + @property + def effect_list(self) -> list[str] | None: + """Return the list of available effects.""" + if effect_list := self.device.effect_list: + return cast(list[str], effect_list) + return None + + @property + def effect(self) -> str | None: + """Return the current effect.""" + if (effect := self.device.effect) and effect["enable"]: + return cast(str, effect["name"]) + return None diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 20f2e9dc171..ec72a3d7941 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -3,7 +3,7 @@ "name": "TP-Link Kasa Smart", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", - "requirements": ["python-kasa==0.4.1"], + "requirements": ["python-kasa==0.4.2"], "codeowners": ["@rytilahti", "@thegardenmonkey"], "dependencies": ["network"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 013576a33db..0cb4a718af6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1891,7 +1891,7 @@ python-join-api==0.0.6 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa==0.4.1 +python-kasa==0.4.2 # homeassistant.components.lirc # python-lirc==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148384e0fdd..2e905d2a194 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1221,7 +1221,7 @@ python-izone==1.2.3 python-juicenet==1.1.0 # homeassistant.components.tplink -python-kasa==0.4.1 +python-kasa==0.4.2 # homeassistant.components.xiaomi_miio python-miio==0.5.11 diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index f9422d60669..4232d3e6909 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -2,7 +2,14 @@ from unittest.mock import AsyncMock, MagicMock, patch -from kasa import SmartBulb, SmartDevice, SmartDimmer, SmartPlug, SmartStrip +from kasa import ( + SmartBulb, + SmartDevice, + SmartDimmer, + SmartLightStrip, + SmartPlug, + SmartStrip, +) from kasa.exceptions import SmartDeviceException from kasa.protocol import TPLinkSmartHomeProtocol @@ -40,6 +47,10 @@ def _mocked_bulb() -> SmartBulb: bulb.is_strip = False bulb.is_plug = False bulb.is_dimmer = False + bulb.is_light_strip = False + bulb.has_effects = False + bulb.effect = None + bulb.effect_list = None bulb.hsv = (10, 30, 5) bulb.device_id = MAC_ADDRESS bulb.valid_temperature_range.min = 4000 @@ -54,6 +65,47 @@ def _mocked_bulb() -> SmartBulb: return bulb +class MockedSmartLightStrip(SmartLightStrip): + """Mock a SmartLightStrip.""" + + def __new__(cls, *args, **kwargs): + """Mock a SmartLightStrip that will pass an isinstance check.""" + return MagicMock(spec=cls) + + +def _mocked_smart_light_strip() -> SmartLightStrip: + strip = MockedSmartLightStrip() + strip.update = AsyncMock() + strip.mac = MAC_ADDRESS + strip.alias = ALIAS + strip.model = MODEL + strip.host = IP_ADDRESS + strip.brightness = 50 + strip.color_temp = 4000 + strip.is_color = True + strip.is_strip = False + strip.is_plug = False + strip.is_dimmer = False + strip.is_light_strip = True + strip.has_effects = True + strip.effect = {"name": "Effect1", "enable": 1} + strip.effect_list = ["Effect1", "Effect2"] + strip.hsv = (10, 30, 5) + strip.device_id = MAC_ADDRESS + strip.valid_temperature_range.min = 4000 + strip.valid_temperature_range.max = 9000 + strip.hw_info = {"sw_ver": "1.0.0", "hw_ver": "1.0.0"} + strip.turn_off = AsyncMock() + strip.turn_on = AsyncMock() + strip.set_brightness = AsyncMock() + strip.set_hsv = AsyncMock() + strip.set_color_temp = AsyncMock() + strip.set_effect = AsyncMock() + strip.set_custom_effect = AsyncMock() + strip.protocol = _mock_protocol() + return strip + + def _mocked_dimmer() -> SmartDimmer: dimmer = MagicMock(auto_spec=SmartDimmer, name="Mocked dimmer") dimmer.update = AsyncMock() @@ -67,6 +119,9 @@ def _mocked_dimmer() -> SmartDimmer: dimmer.is_strip = False dimmer.is_plug = False dimmer.is_dimmer = True + dimmer.is_light_strip = False + dimmer.effect = None + dimmer.effect_list = None dimmer.hsv = (10, 30, 5) dimmer.device_id = MAC_ADDRESS dimmer.valid_temperature_range.min = 4000 diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index c3b82045ef0..32394be790b 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,6 +1,7 @@ """Tests for light platform.""" from __future__ import annotations +from datetime import timedelta from unittest.mock import PropertyMock import pytest @@ -10,6 +11,8 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_EFFECT_LIST, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, @@ -20,14 +23,21 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from . import MAC_ADDRESS, _mocked_bulb, _patch_discovery, _patch_single_discovery +from . import ( + MAC_ADDRESS, + _mocked_bulb, + _mocked_smart_light_strip, + _patch_discovery, + _patch_single_discovery, +) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_light_unique_id(hass: HomeAssistant) -> None: @@ -375,3 +385,48 @@ async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: ) bulb.turn_on.assert_called_once_with(transition=1) bulb.turn_on.reset_mock() + + +async def test_smart_strip_effects(hass: HomeAssistant) -> None: + """Test smart strip effects.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + strip = _mocked_smart_light_strip() + + with _patch_discovery(device=strip), _patch_single_discovery(device=strip): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT] == "Effect1" + assert state.attributes[ATTR_EFFECT_LIST] == ["Effect1", "Effect2"] + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Effect2"}, + blocking=True, + ) + strip.set_effect.assert_called_once_with("Effect2") + strip.set_effect.reset_mock() + + strip.effect = {"name": "Effect1", "enable": 0} + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert ATTR_EFFECT not in state.attributes + + strip.effect_list = None + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT_LIST] is None From 9180243a542c0edd51f28ee1d66803993b362d58 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 22 Mar 2022 10:40:48 +0100 Subject: [PATCH 0599/1054] Deprecate mysensors config YAML (#68504) --- .../components/mysensors/__init__.py | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index dc4462b8d0e..d392624bbe4 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -124,27 +124,30 @@ GATEWAY_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - vol.All( - deprecated(CONF_DEBUG), - deprecated(CONF_OPTIMISTIC), - deprecated(CONF_PERSISTENCE), - { - vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, - set_default_persistence_file, - has_all_unique_files, - [GATEWAY_SCHEMA], - ), - vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + vol.All( + deprecated(CONF_DEBUG), + deprecated(CONF_OPTIMISTIC), + deprecated(CONF_PERSISTENCE), + { + vol.Required(CONF_GATEWAYS): vol.All( + cv.ensure_list, + set_default_persistence_file, + has_all_unique_files, + [GATEWAY_SCHEMA], + ), + vol.Optional(CONF_RETAIN, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, + }, + ) ) - ) - }, + }, + ), extra=vol.ALLOW_EXTRA, ) From 87378016c14f681a97df4e212832d29f14efd460 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 22 Mar 2022 11:11:41 +0100 Subject: [PATCH 0600/1054] Add basic support for SamsungTV encrypted models (#68500) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 28 ++- homeassistant/components/samsungtv/bridge.py | 212 +++++++++++++++++- .../components/samsungtv/config_flow.py | 58 ++++- homeassistant/components/samsungtv/const.py | 4 + .../components/samsungtv/diagnostics.py | 4 +- .../components/samsungtv/media_player.py | 19 +- .../components/samsungtv/strings.json | 8 +- .../components/samsungtv/translations/en.json | 8 +- tests/components/samsungtv/__init__.py | 22 ++ tests/components/samsungtv/conftest.py | 27 +++ tests/components/samsungtv/const.py | 54 +++++ .../components/samsungtv/test_config_flow.py | 126 ++++++++++- .../components/samsungtv/test_diagnostics.py | 102 +++++++-- tests/components/samsungtv/test_init.py | 3 + .../components/samsungtv/test_media_player.py | 149 +++++++++++- 15 files changed, 766 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 4ce2b2350d4..470d7e80584 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -17,11 +17,12 @@ from homeassistant.const import ( CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_TOKEN, EVENT_HOMEASSISTANT_STOP, Platform, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -29,10 +30,12 @@ from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_MODEL, CONF_ON_ACTION, + CONF_SESSION_ID, DEFAULT_NAME, DOMAIN, LEGACY_PORT, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, ) @@ -110,6 +113,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Samsung TV platform.""" # Initialize bridge + if entry.data.get(CONF_METHOD) == METHOD_ENCRYPTED_WEBSOCKET: + if not entry.data.get(CONF_TOKEN) or not entry.data.get(CONF_SESSION_ID): + raise ConfigEntryAuthFailed( + "Token and session id are required in encrypted mode" + ) bridge = await _async_create_bridge_with_updated_data(hass, entry) # Ensure updates get saved against the config_entry @@ -120,6 +128,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bridge.register_update_config_entry_callback(_update_config_entry) + # Allow bridge to force the reload of the config_entry + @callback + def _reload_config_entry() -> None: + """Update config entry with the new token.""" + hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) + + bridge.register_reload_callback(_reload_config_entry) + async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) @@ -134,6 +150,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +def _model_requires_encryption(model: str | None) -> bool: + """H and J models need pairing with PIN.""" + return model is not None and len(model) > 4 and model[4] in ("H", "J") + + async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry ) -> SamsungTVBridge: @@ -194,12 +215,13 @@ async def _async_create_bridge_with_updated_data( LOGGER.info("Updated model to %s for %s", model, host) updated_data[CONF_MODEL] = model - if model and len(model) > 4 and model[4] in ("H", "J"): + if _model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: LOGGER.info( "Detected model %s for %s. Some televisions from H and J series use " - "an encrypted protocol that may not be supported in this integration", + "an encrypted protocol but you are using %s which may not be supported", model, host, + method, ) if updated_data: diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index e9036d1aa59..b6f9aebd86b 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import asyncio from asyncio.exceptions import TimeoutError as AsyncioTimeoutError -from collections.abc import Callable, Mapping +from collections.abc import Callable, Iterable, Mapping import contextlib from typing import Any, cast @@ -13,6 +13,7 @@ from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledRespo from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.command import SamsungTVCommand +from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote from samsungtvws.event import MS_ERROR_EVENT from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey @@ -28,13 +29,17 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_DESCRIPTION, + CONF_SESSION_ID, + ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, @@ -64,19 +69,31 @@ async def async_get_device_info( host: str, ) -> tuple[int | None, str | None, dict[str, Any] | None]: """Fetch the port, method, and device info.""" + # Bridge is defined if bridge and bridge.port: return bridge.port, bridge.method, await bridge.async_device_info() + # Try websocket ports for port in WEBSOCKET_PORTS: bridge = SamsungTVBridge.get_bridge(hass, METHOD_WEBSOCKET, host, port) if info := await bridge.async_device_info(): return port, METHOD_WEBSOCKET, info + # Try encrypted websocket port + bridge = SamsungTVBridge.get_bridge( + hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT + ) + result = await bridge.async_try_connect() + if result == RESULT_SUCCESS: + return port, METHOD_ENCRYPTED_WEBSOCKET, await bridge.async_device_info() + + # Try legacy port bridge = SamsungTVBridge.get_bridge(hass, METHOD_LEGACY, host, LEGACY_PORT) result = await bridge.async_try_connect() if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING): return LEGACY_PORT, METHOD_LEGACY, await bridge.async_device_info() + # Failed to get info return None, None, None @@ -94,6 +111,8 @@ class SamsungTVBridge(ABC): """Get Bridge instance.""" if method == METHOD_LEGACY or port == LEGACY_PORT: return SamsungTVLegacyBridge(hass, method, host, port) + if method == METHOD_ENCRYPTED_WEBSOCKET or port == ENCRYPTED_WEBSOCKET_PORT: + return SamsungTVEncryptedBridge(hass, method, host, port, entry_data) return SamsungTVWSBridge(hass, method, host, port, entry_data) def __init__( @@ -105,13 +124,19 @@ class SamsungTVBridge(ABC): self.method = method self.host = host self.token: str | None = None + self.session_id: str | None = None self._reauth_callback: CALLBACK_TYPE | None = None + self._reload_callback: CALLBACK_TYPE | None = None self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None def register_reauth_callback(self, func: CALLBACK_TYPE) -> None: """Register a callback function.""" self._reauth_callback = func + def register_reload_callback(self, func: CALLBACK_TYPE) -> None: + """Register a callback function.""" + self._reload_callback = func + def register_update_config_entry_callback( self, func: Callable[[Mapping[str, Any]], None] ) -> None: @@ -140,7 +165,7 @@ class SamsungTVBridge(ABC): @abstractmethod async def async_power_off(self) -> None: - """Send power off command to remote.""" + """Send power off command to remote and close.""" @abstractmethod async def async_close_remote(self) -> None: @@ -151,6 +176,11 @@ class SamsungTVBridge(ABC): if self._reauth_callback is not None: self._reauth_callback() + def _notify_reload_callback(self) -> None: + """Notify reload callback.""" + if self._reload_callback is not None: + self._reload_callback() + def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None: """Notify update config callback.""" if self._update_config_entry is not None: @@ -348,6 +378,7 @@ class SamsungTVWSBridge(SamsungTVBridge): # Ensure we get an updated value info = await self.async_device_info() return info is not None and info["device"]["PowerState"] == "on" + LOGGER.debug("Checking if TV %s is on using websocket", self.host) if remote := await self._async_get_remote(): return remote.is_alive() @@ -406,7 +437,7 @@ class SamsungTVWSBridge(SamsungTVBridge): """Try to gather infos of this TV.""" if self._rest_api is None: assert self.port - self._rest_api = SamsungTVAsyncRest( + rest_api = SamsungTVAsyncRest( host=self.host, session=async_get_clientsession(self.hass), port=self.port, @@ -414,7 +445,7 @@ class SamsungTVWSBridge(SamsungTVBridge): ) with contextlib.suppress(HttpApiError, AsyncioTimeoutError): - device_info: dict[str, Any] = await self._rest_api.rest_device_info() + device_info: dict[str, Any] = await rest_api.rest_device_info() LOGGER.debug("Device info on %s is: %s", self.host, device_info) self._device_info = device_info return device_info @@ -513,8 +544,7 @@ class SamsungTVWSBridge(SamsungTVBridge): self._notify_update_config_entry({CONF_TOKEN: self.token}) return self._remote - @staticmethod - def _remote_event(event: str, response: Any) -> None: + def _remote_event(self, event: str, response: Any) -> None: """Received event from remote websocket.""" if event == MS_ERROR_EVENT: # { 'event': 'ms.error', @@ -523,10 +553,17 @@ class SamsungTVWSBridge(SamsungTVBridge): message := data.get("message") ) == "unrecognized method value : ms.remote.control": LOGGER.error( - "Your TV seems to be unsupported by " - "SamsungTVWSBridge and may need a PIN: '%s'", + "Your TV seems to be unsupported by SamsungTVWSBridge" + " and needs a PIN: '%s'. Reloading", message, ) + self._notify_update_config_entry( + { + CONF_METHOD: METHOD_ENCRYPTED_WEBSOCKET, + CONF_PORT: ENCRYPTED_WEBSOCKET_PORT, + } + ) + self._notify_reload_callback() async def async_power_off(self) -> None: """Send power off command to remote.""" @@ -548,3 +585,162 @@ class SamsungTVWSBridge(SamsungTVBridge): LOGGER.debug( "Error closing connection to %s: %s", self.host, err.__repr__() ) + + +class SamsungTVEncryptedBridge(SamsungTVBridge): + """The Bridge for Encrypted WebSocket TVs (J/H models).""" + + def __init__( + self, + hass: HomeAssistant, + method: str, + host: str, + port: int | None = None, + entry_data: Mapping[str, Any] | None = None, + ) -> None: + """Initialize Bridge.""" + super().__init__(hass, method, host, port) + if entry_data: + self.token = entry_data.get(CONF_TOKEN) + self.session_id = entry_data.get(CONF_SESSION_ID) + self._rest_api_port: int | None = None + self._device_info: dict[str, Any] | None = None + self._remote: SamsungTVEncryptedWSAsyncRemote | None = None + self._remote_lock = asyncio.Lock() + + async def async_get_app_list(self) -> dict[str, str]: + """Get installed app list.""" + return {} + + async def async_is_on(self) -> bool: + """Tells if the TV is on.""" + LOGGER.debug("Checking if TV %s is on using websocket", self.host) + if remote := await self._async_get_remote(): + return remote.is_alive() + return False + + async def async_try_connect(self) -> str: + """Try to connect to the Websocket TV.""" + self.port = ENCRYPTED_WEBSOCKET_PORT + config = { + CONF_NAME: VALUE_CONF_NAME, + CONF_HOST: self.host, + CONF_METHOD: self.method, + CONF_PORT: self.port, + # We need this high timeout because waiting for auth popup is just an open socket + CONF_TIMEOUT: TIMEOUT_REQUEST, + } + + try: + LOGGER.debug("Try config: %s", config) + async with SamsungTVEncryptedWSAsyncRemote( + host=self.host, + port=self.port, + web_session=async_get_clientsession(self.hass), + token=self.token or "", + session_id=self.session_id or "", + timeout=TIMEOUT_REQUEST, + ) as remote: + await remote.start_listening() + LOGGER.debug("Working config: %s", config) + return RESULT_SUCCESS + except WebSocketException as err: + LOGGER.debug("Working but unsupported config: %s, error: %s", config, err) + return RESULT_NOT_SUPPORTED + except (OSError, AsyncioTimeoutError, ConnectionFailure) as err: + LOGGER.debug("Failing config: %s, error: %s", config, err) + + return RESULT_CANNOT_CONNECT + + async def async_device_info(self) -> dict[str, Any] | None: + """Try to gather infos of this TV.""" + # Default to try all ports + rest_api_ports: Iterable[int] = WEBSOCKET_PORTS + if self._rest_api_port: + # We have already made a successful call to the REST api + rest_api_ports = (self._rest_api_port,) + + for rest_api_port in rest_api_ports: + assert self.port + rest_api = SamsungTVAsyncRest( + host=self.host, + session=async_get_clientsession(self.hass), + port=self.port, + timeout=TIMEOUT_WEBSOCKET, + ) + + with contextlib.suppress(HttpApiError, AsyncioTimeoutError): + device_info: dict[str, Any] = await rest_api.rest_device_info() + LOGGER.debug("Device info on %s is: %s", self.host, device_info) + self._device_info = device_info + self._rest_api_port = rest_api_port + return device_info + + return None + + async def async_send_keys(self, keys: list[str]) -> None: + """Send a list of keys using websocket protocol.""" + raise HomeAssistantError( + "Sending commands to encrypted TVs is not yet supported" + ) + + async def _async_get_remote(self) -> SamsungTVEncryptedWSAsyncRemote | None: + """Create or return a remote control instance.""" + if (remote := self._remote) and remote.is_alive(): + # If we have one then try to use it + return remote + + async with self._remote_lock: + # If we don't have one make sure we do it under the lock + # so we don't make two do due a race to get the remote + return await self._async_get_remote_under_lock() + + async def _async_get_remote_under_lock( + self, + ) -> SamsungTVEncryptedWSAsyncRemote | None: + """Create or return a remote control instance.""" + if self._remote is None or not self._remote.is_alive(): + # We need to create a new instance to reconnect. + LOGGER.debug("Create SamsungTVEncryptedBridge for %s", self.host) + assert self.port + self._remote = SamsungTVEncryptedWSAsyncRemote( + host=self.host, + port=self.port, + web_session=async_get_clientsession(self.hass), + token=self.token or "", + session_id=self.session_id or "", + timeout=TIMEOUT_WEBSOCKET, + ) + try: + # pylint:disable=[fixme] + # TODO: remove secondary timeout when library is bumped + # See https://github.com/xchwarze/samsung-tv-ws-api/pull/82 + await asyncio.wait_for( + self._remote.start_listening(), TIMEOUT_WEBSOCKET + ) + except (WebSocketException, AsyncioTimeoutError, OSError) as err: + LOGGER.debug( + "Failed to get remote for %s: %s", self.host, err.__repr__() + ) + self._remote = None + else: + LOGGER.debug("Created SamsungTVEncryptedBridge for %s", self.host) + return self._remote + + async def async_power_off(self) -> None: + """Send power off command to remote.""" + raise HomeAssistantError( + "Sending commands to encrypted TVs is not yet supported" + ) + + async def async_close_remote(self) -> None: + """Close remote object.""" + try: + if self._remote is not None: + # Close the current remote connection + await self._remote.close() + self._remote = None + except OSError as err: + LOGGER.debug( + "Error closing connection to %s: %s", self.host, err.__repr__() + ) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 812916a4654..29f46c1cd33 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -8,6 +8,7 @@ from typing import Any from urllib.parse import urlparse import getmac +from samsungtvws.encrypted.authenticator import SamsungTVEncryptedWSAsyncAuthenticator import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -21,20 +22,25 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_MANUFACTURER, CONF_MODEL, + CONF_SESSION_ID, DEFAULT_MANUFACTURER, DOMAIN, + ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, RESULT_CANNOT_CONNECT, + RESULT_INVALID_PIN, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, RESULT_UNKNOWN_HOST, @@ -73,6 +79,9 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._id: int | None = None self._bridge: SamsungTVBridge | None = None self._device_info: dict[str, Any] | None = None + self._encrypted_authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = ( + None + ) def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: """Get device entry.""" @@ -174,6 +183,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): port = user_input.get(CONF_PORT) if port in WEBSOCKET_PORTS: user_input[CONF_METHOD] = METHOD_WEBSOCKET + elif port == ENCRYPTED_WEBSOCKET_PORT: + user_input[CONF_METHOD] = METHOD_ENCRYPTED_WEBSOCKET elif port == LEGACY_PORT: user_input[CONF_METHOD] = METHOD_LEGACY user_input[CONF_MANUFACTURER] = DEFAULT_MANUFACTURER @@ -365,10 +376,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Confirm reauth.""" errors = {} assert self._reauth_entry + method = self._reauth_entry.data[CONF_METHOD] if user_input is not None: bridge = SamsungTVBridge.get_bridge( self.hass, - self._reauth_entry.data[CONF_METHOD], + method, self._reauth_entry.data[CONF_HOST], ) result = await bridge.async_try_connect() @@ -387,8 +399,50 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {"base": RESULT_AUTH_MISSING} self.context["title_placeholders"] = {"device": self._title} + step_id = "reauth_confirm" + if method == METHOD_ENCRYPTED_WEBSOCKET: + step_id = "reauth_confirm_encrypted" return self.async_show_form( - step_id="reauth_confirm", + step_id=step_id, errors=errors, description_placeholders={"device": self._title}, ) + + async def async_step_reauth_confirm_encrypted( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Confirm reauth (encrypted method).""" + errors = {} + assert self._reauth_entry + if self._encrypted_authenticator is None: + self._encrypted_authenticator = SamsungTVEncryptedWSAsyncAuthenticator( + self._reauth_entry.data[CONF_HOST], + web_session=async_get_clientsession(self.hass), + ) + await self._encrypted_authenticator.start_pairing() + + if user_input is not None and (pin := user_input.get("pin")): + if token := await self._encrypted_authenticator.try_pin(pin): + session_id = ( + await self._encrypted_authenticator.get_session_id_and_close() + ) + new_data = { + **self._reauth_entry.data, + CONF_TOKEN: token, + CONF_SESSION_ID: session_id, + } + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=new_data + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + errors = {"base": RESULT_INVALID_PIN} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="reauth_confirm_encrypted", + errors=errors, + description_placeholders={"device": self._title}, + data_schema=vol.Schema({vol.Required("pin"): str}), + ) diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index f2571372b1f..498b83a0539 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -16,18 +16,22 @@ CONF_DESCRIPTION = "description" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" CONF_ON_ACTION = "turn_on_action" +CONF_SESSION_ID = "session_id" RESULT_AUTH_MISSING = "auth_missing" +RESULT_INVALID_PIN = "invalid_pin" RESULT_SUCCESS = "success" RESULT_CANNOT_CONNECT = "cannot_connect" RESULT_NOT_SUPPORTED = "not_supported" RESULT_UNKNOWN_HOST = "unknown" METHOD_LEGACY = "legacy" +METHOD_ENCRYPTED_WEBSOCKET = "encrypted" METHOD_WEBSOCKET = "websocket" TIMEOUT_REQUEST = 31 TIMEOUT_WEBSOCKET = 5 LEGACY_PORT = 55000 +ENCRYPTED_WEBSOCKET_PORT = 8000 WEBSOCKET_PORTS = (8002, 8001) diff --git a/homeassistant/components/samsungtv/diagnostics.py b/homeassistant/components/samsungtv/diagnostics.py index ff792fff3e3..319e08827cf 100644 --- a/homeassistant/components/samsungtv/diagnostics.py +++ b/homeassistant/components/samsungtv/diagnostics.py @@ -9,9 +9,9 @@ from homeassistant.const import CONF_TOKEN from homeassistant.core import HomeAssistant from .bridge import SamsungTVBridge -from .const import DOMAIN +from .const import CONF_SESSION_ID, DOMAIN -TO_REDACT = {CONF_TOKEN} +TO_REDACT = {CONF_TOKEN, CONF_SESSION_ID} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index baea046c3dc..a293321d311 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -26,7 +26,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_METHOD, + CONF_NAME, + STATE_OFF, + STATE_ON, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component import homeassistant.helpers.config_validation as cv @@ -44,6 +51,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, + METHOD_ENCRYPTED_WEBSOCKET, ) SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} @@ -112,11 +120,14 @@ class SamsungTVDevice(MediaPlayerEntity): self._attr_source_list = list(SOURCES) self._app_list: dict[str, str] | None = None - if self._on_script or self._mac: - self._attr_supported_features = SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON - else: + if config_entry.data.get(CONF_METHOD) != METHOD_ENCRYPTED_WEBSOCKET: + # Encrypted websockets currently only support ON/OFF status self._attr_supported_features = SUPPORT_SAMSUNGTV + if self._on_script or self._mac: + # Add turn-on if on_script or mac is available + self._attr_supported_features |= SUPPORT_TURN_ON + self._attr_device_info = DeviceInfo( name=self.name, manufacturer=config_entry.data.get(CONF_MANUFACTURER), diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index f413a7f1219..f64620638bf 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -13,11 +13,15 @@ "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." }, "reauth_confirm": { - "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds or input PIN." + }, + "reauth_confirm_encrypted": { + "description": "Please enter the PIN displayed on {device}." } }, "error": { - "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]" + "auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]", + "invalid_pin": "PIN is invalid, please try again." }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 4648f930e9b..2e3dd88bec7 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -12,7 +12,8 @@ "unknown": "Unexpected error" }, "error": { - "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant." + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", + "invalid_pin": "PIN is invalid, please try again." }, "flow_title": "{device}", "step": { @@ -21,7 +22,10 @@ "title": "Samsung TV" }, "reauth_confirm": { - "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." + "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds or input PIN." + }, + "reauth_confirm_encrypted": { + "description": "Please enter the PIN displayed on {device}." }, "user": { "data": { diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 4ad1622c6ca..7cace81f7a6 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1 +1,23 @@ """Tests for the samsungtv component.""" + + +from homeassistant.components.samsungtv.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry: + """Set up mock Samsung TV from config entry data.""" + entry = MockConfigEntry( + domain=DOMAIN, data=data, entry_id="123456", unique_id="any" + ) + entry.add_to_hass(hass) + + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index c7733a3652a..3d9ea74e346 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from samsungctl import Remote from samsungtvws.async_remote import SamsungTVWSAsyncRemote +from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote import homeassistant.util.dt as dt_util @@ -77,6 +78,32 @@ def remotews_fixture() -> Mock: yield remotews +@pytest.fixture(name="remoteencws") +def remoteencws_fixture() -> Mock: + """Patch the samsungtvws SamsungTVEncryptedWSAsyncRemote.""" + remoteencws = Mock(SamsungTVEncryptedWSAsyncRemote) + remoteencws.__aenter__ = AsyncMock(return_value=remoteencws) + remoteencws.__aexit__ = AsyncMock() + + def _start_listening( + ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None + ): + remoteencws.ws_event_callback = ws_event_callback + + def _mock_ws_event_callback(event: str, response: Any): + if remoteencws.ws_event_callback: + remoteencws.ws_event_callback(event, response) + + remoteencws.start_listening.side_effect = _start_listening + remoteencws.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback) + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote", + ) as remotews_class: + remotews_class.return_value = remoteencws + yield remoteencws + + @pytest.fixture(name="delay") def delay_fixture() -> Mock: """Patch the delay script function.""" diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 7f9f175c171..11e92481f21 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -1,4 +1,29 @@ """Constants for the samsungtv tests.""" +from homeassistant.components.samsungtv.const import CONF_SESSION_ID +from homeassistant.const import ( + CONF_HOST, + CONF_IP_ADDRESS, + CONF_MAC, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TOKEN, +) + +MOCK_CONFIG_ENCRYPTED_WS = { + CONF_HOST: "fake_host", + CONF_NAME: "fake", + CONF_PORT: 8000, +} +MOCK_ENTRYDATA_ENCRYPTED_WS = { + **MOCK_CONFIG_ENCRYPTED_WS, + CONF_IP_ADDRESS: "test", + CONF_METHOD: "encrypted", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_TOKEN: "037739871315caef138547b03e348b72", + CONF_SESSION_ID: "2", +} + SAMPLE_APP_LIST = [ { "appId": "111299001912", @@ -73,3 +98,32 @@ SAMPLE_DEVICE_INFO_FRAME = { "uri": "https://1.2.3.4:8002/api/v2/", "version": "2.0.25", } + +SAMPLE_DEVICE_INFO_UE48JU6400 = { + "id": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "name": "[TV] TV-UE48JU6470", + "version": "2.0.25", + "device": { + "type": "Samsung SmartTV", + "duid": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "model": "15_HAWKM_UHD_2D", + "modelName": "UE48JU6400", + "description": "Samsung DTV RCR", + "networkType": "wired", + "ssid": "", + "ip": "1.2.3.4", + "firmwareVersion": "Unknown", + "name": "[TV] TV-UE48JU6470", + "id": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "udn": "uuid:223da676-497a-4e06-9507-5e27ec4f0fb3", + "resolution": "1920x1080", + "countryCode": "AT", + "msfVersion": "2.0.25", + "smartHubAgreement": "true", + "wifiMac": "aa:bb:ww:ii:ff:ii", + "developerMode": "0", + "developerIP": "", + }, + "type": "Samsung SmartTV", + "uri": "https://1.2.3.4:8002/api/v2/", +} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 8f1a22395fb..72f23b01350 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -18,9 +18,11 @@ from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_MODEL, + CONF_SESSION_ID, DEFAULT_MANUFACTURER, DOMAIN, LEGACY_PORT, + METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, METHOD_WEBSOCKET, RESULT_AUTH_MISSING, @@ -49,7 +51,13 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_FRAME +from . import setup_samsungtv_entry +from .const import ( + MOCK_CONFIG_ENCRYPTED_WS, + MOCK_ENTRYDATA_ENCRYPTED_WS, + SAMPLE_APP_LIST, + SAMPLE_DEVICE_INFO_FRAME, +) from tests.common import MockConfigEntry @@ -484,6 +492,9 @@ async def test_ssdp_websocket_not_supported( with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=WebSocketProtocolError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", ) as remotews, patch.object( @@ -645,12 +656,16 @@ async def test_import_legacy(hass: HomeAssistant) -> None: async def test_import_legacy_without_name(hass: HomeAssistant, rest_api: Mock) -> None: """Test importing from yaml without a name.""" rest_api.rest_device_info.return_value = None - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_IMPORT_DATA_WITHOUT_NAME, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=WebSocketProtocolError("Boom"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA_WITHOUT_NAME, + ) + await hass.async_block_till_done() assert result["type"] == "create_entry" assert result["title"] == "fake_host" assert result["data"][CONF_HOST] == "fake_host" @@ -682,6 +697,26 @@ async def test_import_websocket(hass: HomeAssistant): assert result["result"].unique_id is None +@pytest.mark.usefixtures("remoteencws") +async def test_import_websocket_encrypted(hass: HomeAssistant): + """Test importing from yaml with hostname.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_CONFIG_ENCRYPTED_WS, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "fake" + assert result["data"][CONF_METHOD] == METHOD_ENCRYPTED_WEBSOCKET + assert result["data"][CONF_PORT] == 8000 + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + @pytest.mark.usefixtures("remotews") async def test_import_websocket_without_port(hass: HomeAssistant): """Test importing from yaml with hostname by no port.""" @@ -826,7 +861,7 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> assert result["reason"] == "not_supported" -@pytest.mark.usefixtures("remote", "remotews") +@pytest.mark.usefixtures("remote", "remotews", "remoteencws") async def test_zeroconf_no_device_info(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from zeroconf where device_info returns None.""" rest_api.rest_device_info.return_value = None @@ -1234,6 +1269,9 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( with patch( "homeassistant.components.samsungtv.bridge.Remote.__enter__", return_value=True, + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=WebSocketProtocolError("Boom"), ), patch( "homeassistant.components.samsungtv.async_setup", return_value=True, @@ -1364,6 +1402,78 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: assert result2["reason"] == "not_supported" +@pytest.mark.usefixtures("remoteencws") +async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: + """Test reauth flow for encrypted TVs.""" + encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS} + del encrypted_entry_data[CONF_TOKEN] + del encrypted_entry_data[CONF_SESSION_ID] + + entry = await setup_samsungtv_entry(hass, encrypted_entry_data) + assert entry.state == config_entries.ConfigEntryState.SETUP_ERROR + flows_in_progress = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"]["source"] == "reauth" + ] + assert len(flows_in_progress) == 1 + result = flows_in_progress[0] + + with patch( + "homeassistant.components.samsungtv.config_flow.SamsungTVEncryptedWSAsyncAuthenticator", + autospec=True, + ) as authenticator_mock: + authenticator_mock.return_value.try_pin.side_effect = [ + None, + "037739871315caef138547b03e348b72", + ] + authenticator_mock.return_value.get_session_id_and_close.return_value = "1" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm_encrypted" + assert result["errors"] == {} + + # First time on reauth_confirm_encrypted + # creates the authenticator, start pairing and requests PIN + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=None + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm_encrypted" + + # Invalid PIN + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pin": "invalid"} + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm_encrypted" + + # Valid PIN + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pin": "1234"} + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert entry.state == config_entries.ConfigEntryState.LOADED + + authenticator_mock.assert_called_once() + assert authenticator_mock.call_args[0] == ("fake_host",) + + authenticator_mock.return_value.start_pairing.assert_called_once() + assert authenticator_mock.return_value.try_pin.call_count == 2 + assert authenticator_mock.return_value.try_pin.call_args_list == [ + call("invalid"), + call("1234"), + ] + authenticator_mock.return_value.get_session_id_and_close.assert_called_once() + + assert entry.data[CONF_TOKEN] == "037739871315caef138547b03e348b72" + assert entry.data[CONF_SESSION_ID] == "1" + + @pytest.mark.usefixtures("remotews") async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( hass: HomeAssistant, diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index ba9e050d9ce..88fb98a66db 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -1,39 +1,30 @@ """Test samsungtv diagnostics.""" +from unittest.mock import Mock + from aiohttp import ClientSession import pytest +from samsungtvws.exceptions import HttpApiError from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.samsungtv import DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import SAMPLE_DEVICE_INFO_WIFI +from . import setup_samsungtv_entry +from .const import ( + MOCK_ENTRYDATA_ENCRYPTED_WS, + SAMPLE_DEVICE_INFO_UE48JU6400, + SAMPLE_DEVICE_INFO_WIFI, +) from .test_media_player import MOCK_ENTRY_WS_WITH_MAC -from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry -@pytest.fixture(name="config_entry") -def get_config_entry(hass: HomeAssistant) -> ConfigEntry: - """Create and register mock config entry.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=MOCK_ENTRY_WS_WITH_MAC, - entry_id="123456", - unique_id="any", - ) - config_entry.add_to_hass(hass) - return config_entry - - @pytest.mark.usefixtures("remotews") async def test_entry_diagnostics( - hass: HomeAssistant, config_entry: ConfigEntry, hass_client: ClientSession + hass: HomeAssistant, hass_client: ClientSession ) -> None: """Test config entry diagnostics.""" - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS_WITH_MAC) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { @@ -60,3 +51,74 @@ async def test_entry_diagnostics( }, "device_info": SAMPLE_DEVICE_INFO_WIFI, } + + +@pytest.mark.usefixtures("remoteencws") +async def test_entry_diagnostics_encrypted( + hass: HomeAssistant, rest_api: Mock, hass_client: ClientSession +) -> None: + """Test config entry diagnostics.""" + rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_UE48JU6400 + config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "data": { + "host": "fake_host", + "ip_address": "test", + "mac": "aa:bb:cc:dd:ee:ff", + "method": "encrypted", + "model": "UE48JU6400", + "name": "fake", + "port": 8000, + "token": REDACTED, + "session_id": REDACTED, + }, + "disabled_by": None, + "domain": "samsungtv", + "entry_id": "123456", + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "title": "Mock Title", + "unique_id": "any", + "version": 2, + }, + "device_info": SAMPLE_DEVICE_INFO_UE48JU6400, + } + + +@pytest.mark.usefixtures("remoteencws") +async def test_entry_diagnostics_encrypte_offline( + hass: HomeAssistant, rest_api: Mock, hass_client: ClientSession +) -> None: + """Test config entry diagnostics.""" + rest_api.rest_device_info.side_effect = HttpApiError + config_entry = await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "data": { + "host": "fake_host", + "ip_address": "test", + "mac": "aa:bb:cc:dd:ee:ff", + "method": "encrypted", + "name": "fake", + "port": 8000, + "token": REDACTED, + "session_id": REDACTED, + }, + "disabled_by": None, + "domain": "samsungtv", + "entry_id": "123456", + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "title": "Mock Title", + "unique_id": "any", + "version": 2, + }, + "device_info": None, + } diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 14462b1aaf3..6fec07fab84 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -80,6 +80,9 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant) """Test import from yaml when the device is offline.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=OSError, ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", side_effect=OSError, diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 74849fb9fee..dac0c43bb1b 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -8,6 +8,7 @@ import pytest from samsungctl import exceptions from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.command import SamsungTVSleepCommand +from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException @@ -29,6 +30,9 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, DOMAIN as SAMSUNGTV_DOMAIN, + ENCRYPTED_WEBSOCKET_PORT, + METHOD_ENCRYPTED_WEBSOCKET, + METHOD_WEBSOCKET, TIMEOUT_WEBSOCKET, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV @@ -65,7 +69,12 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .const import SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_FRAME +from . import setup_samsungtv_entry +from .const import ( + MOCK_ENTRYDATA_ENCRYPTED_WS, + SAMPLE_APP_LIST, + SAMPLE_DEVICE_INFO_FRAME, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -119,6 +128,7 @@ MOCK_ENTRY_WS_WITH_MAC = { CONF_TOKEN: "123456789", } + ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon" MOCK_CONFIG_NOTURNON = { SAMSUNGTV_DOMAIN: [ @@ -221,6 +231,30 @@ async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> Non remote_class.assert_called_once_with(**MOCK_CALLS_WS) +async def test_setup_encrypted_websocket( + hass: HomeAssistant, mock_now: datetime +) -> None: + """Test setup of platform from config entry.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote" + ) as remote_class: + remote = Mock(SamsungTVEncryptedWSAsyncRemote) + remote.__aenter__ = AsyncMock(return_value=remote) + remote.__aexit__ = AsyncMock() + remote_class.return_value = remote + + await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + remote_class.assert_called_once() + + @pytest.mark.usefixtures("remote") async def test_update_on(hass: HomeAssistant, mock_now: datetime) -> None: """Testing update tv on.""" @@ -343,6 +377,30 @@ async def test_update_off_ws_with_power_state( remotews.start_listening.assert_not_called() +async def test_update_off_encryptedws( + hass: HomeAssistant, remoteencws: Mock, rest_api: Mock, mock_now: datetime +) -> None: + """Testing update tv off.""" + await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + + rest_api.rest_device_info.assert_called_once() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + remoteencws.start_listening = Mock(side_effect=WebSocketException("Boom")) + remoteencws.is_alive.return_value = False + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + rest_api.rest_device_info.assert_called_once() + + @pytest.mark.usefixtures("remote") async def test_update_access_denied(hass: HomeAssistant, mock_now: datetime) -> None: """Testing update tv access denied exception.""" @@ -543,6 +601,21 @@ async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) assert state.state == STATE_ON +async def test_send_key_websocketexception_encrypted( + hass: HomeAssistant, remoteencws: Mock +) -> None: + """Testing unhandled response exception.""" + await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + remoteencws.send_commands = Mock(side_effect=WebSocketException("Boom")) + with pytest.raises(HomeAssistantError) as exc_info: + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert exc_info.match("media_player.fake does not support this service.") + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -554,6 +627,21 @@ async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None assert state.state == STATE_ON +async def test_send_key_os_error_ws_encrypted( + hass: HomeAssistant, remoteencws: Mock +) -> None: + """Testing unhandled response exception.""" + await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + remoteencws.send_commands = Mock(side_effect=OSError("Boom")) + with pytest.raises(HomeAssistantError) as exc_info: + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert exc_info.match("media_player.fake does not support this service.") + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + async def test_send_key_os_error(hass: HomeAssistant, remote: Mock) -> None: """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -714,6 +802,28 @@ async def test_turn_off_websocket_frame( assert commands[2].params["DataOfCmd"] == "KEY_POWER" +async def test_turn_off_encrypted_websocket( + hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test for turn_off.""" + await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + + remoteencws.send_commands.reset_mock() + + with pytest.raises(HomeAssistantError) as exc_info: + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert exc_info.match("media_player.fake does not support this service.") + + # commands not sent : power off in progress + with pytest.raises(HomeAssistantError) as exc_info: + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert exc_info.match("media_player.fake does not support this service.") + + async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: """Test for turn_off.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) @@ -751,6 +861,20 @@ async def test_turn_off_ws_os_error( assert "Error closing connection" in caplog.text +async def test_turn_off_encryptedws_os_error( + hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test for turn_off with OSError.""" + caplog.set_level(logging.DEBUG) + await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + remoteencws.close = Mock(side_effect=OSError("BOOM")) + with pytest.raises(HomeAssistantError) as exc_info: + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert exc_info.match("media_player.fake does not support this service.") + + async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: """Test for volume_up.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -1074,11 +1198,10 @@ async def test_websocket_unsupported_remote_control( hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), DEFAULT_MOCK], - ): - await setup_samsungtv(hass, MOCK_CONFIGWS) + entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + assert entry.data[CONF_METHOD] == METHOD_WEBSOCKET + assert entry.data[CONF_PORT] == 8001 remotews.send_command.reset_mock() @@ -1102,6 +1225,18 @@ async def test_websocket_unsupported_remote_control( # error logged assert ( - "Your TV seems to be unsupported by SamsungTVWSBridge and may need a PIN: " + "Your TV seems to be unsupported by SamsungTVWSBridge and needs a PIN: " "'unrecognized method value : ms.remote.control'" in caplog.text ) + + # ensure reauth triggered, and method/port updated + await hass.async_block_till_done() + assert [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"]["source"] == "reauth" + ] + assert entry.data[CONF_METHOD] == METHOD_ENCRYPTED_WEBSOCKET + assert entry.data[CONF_PORT] == ENCRYPTED_WEBSOCKET_PORT + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE From b5d2c6e43aa42a39d10b65014222a793612aef89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Mar 2022 11:32:19 +0100 Subject: [PATCH 0601/1054] Add config flow for threshold binary sensor (#68238) Co-authored-by: Franck Nijhof --- .../components/threshold/__init__.py | 25 +++ .../components/threshold/binary_sensor.py | 54 ++++-- .../components/threshold/config_flow.py | 63 +++++++ homeassistant/components/threshold/const.py | 9 + .../components/threshold/manifest.json | 3 +- .../components/threshold/strings.json | 39 +++++ .../components/threshold/translations/en.json | 39 +++++ homeassistant/generated/config_flows.py | 1 + .../helpers/helper_config_entry_flow.py | 47 ++++- .../components/threshold/test_config_flow.py | 162 ++++++++++++++++++ tests/components/threshold/test_init.py | 61 +++++++ 11 files changed, 483 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/threshold/config_flow.py create mode 100644 homeassistant/components/threshold/const.py create mode 100644 homeassistant/components/threshold/strings.json create mode 100644 homeassistant/components/threshold/translations/en.json create mode 100644 tests/components/threshold/test_config_flow.py create mode 100644 tests/components/threshold/test_init.py diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index 98ebdcd8418..75f4c5d1abc 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -1 +1,26 @@ """The threshold component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Min/Max from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (Platform.BINARY_SENSOR,) + ) diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index ccb998c7586..49a97d84f0e 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, @@ -19,11 +20,13 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER + _LOGGER = logging.getLogger(__name__) ATTR_HYSTERESIS = "hysteresis" @@ -33,10 +36,6 @@ ATTR_SENSOR_VALUE = "sensor_value" ATTR_TYPE = "type" ATTR_UPPER = "upper" -CONF_HYSTERESIS = "hysteresis" -CONF_LOWER = "lower" -CONF_UPPER = "upper" - DEFAULT_NAME = "Threshold" DEFAULT_HYSTERESIS = 0.0 @@ -61,6 +60,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize threshold config entry.""" + registry = er.async_get(hass) + device_class = None + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + hysteresis = config_entry.options[CONF_HYSTERESIS] + lower = config_entry.options[CONF_LOWER] + name = config_entry.title + unique_id = config_entry.entry_id + upper = config_entry.options[CONF_UPPER] + + async_add_entities( + [ + ThresholdSensor( + hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id + ) + ] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -78,7 +103,7 @@ async def async_setup_platform( async_add_entities( [ ThresholdSensor( - hass, entity_id, name, lower, upper, hysteresis, device_class + hass, entity_id, name, lower, upper, hysteresis, device_class, None ) ], ) @@ -87,9 +112,11 @@ async def async_setup_platform( class ThresholdSensor(BinarySensorEntity): """Representation of a Threshold sensor.""" - def __init__(self, hass, entity_id, name, lower, upper, hysteresis, device_class): + def __init__( + self, hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id + ): """Initialize the Threshold sensor.""" - self._hass = hass + self._attr_unique_id = unique_id self._entity_id = entity_id self._name = name self._threshold_lower = lower @@ -101,10 +128,9 @@ class ThresholdSensor(BinarySensorEntity): self._state = None self.sensor_value = None - @callback - def async_threshold_sensor_state_listener(event): + def _update_sensor_state(): """Handle sensor state changes.""" - if (new_state := event.data.get("new_state")) is None: + if (new_state := hass.states.get(self._entity_id)) is None: return try: @@ -118,11 +144,17 @@ class ThresholdSensor(BinarySensorEntity): _LOGGER.warning("State is not numerical") self._update_state() + + @callback + def async_threshold_sensor_state_listener(event): + """Handle sensor state changes.""" + _update_sensor_state() self.async_write_ha_state() async_track_state_change_event( hass, [entity_id], async_threshold_sensor_state_listener ) + _update_sensor_state() @property def name(self): diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py new file mode 100644 index 00000000000..01e5364284a --- /dev/null +++ b/homeassistant/components/threshold/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for Threshold integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol + +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowError, + HelperFlowStep, +) + +from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN + + +def _validate_mode(data: Any) -> Any: + """Validate the threshold mode, and set limits to None if not set.""" + if CONF_LOWER not in data and CONF_UPPER not in data: + raise HelperFlowError("need_lower_upper") + return {CONF_LOWER: None, CONF_UPPER: None, **data} + + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): selector.selector( + {"number": {"mode": "box"}} + ), + vol.Optional(CONF_LOWER): selector.selector({"number": {"mode": "box"}}), + vol.Optional(CONF_UPPER): selector.selector({"number": {"mode": "box"}}), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + vol.Required(CONF_ENTITY_ID): selector.selector( + {"entity": {"domain": "sensor"}} + ), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW = { + "user": HelperFlowStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) +} + +OPTIONS_FLOW = { + "init": HelperFlowStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) +} + + +class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Threshold.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return options[CONF_NAME] diff --git a/homeassistant/components/threshold/const.py b/homeassistant/components/threshold/const.py new file mode 100644 index 00000000000..2cb9dc88f0f --- /dev/null +++ b/homeassistant/components/threshold/const.py @@ -0,0 +1,9 @@ +"""Constants for the Threshold integration.""" + +DOMAIN = "threshold" + +CONF_HYSTERESIS = "hysteresis" +CONF_LOWER = "lower" +CONF_UPPER = "upper" + +DEFAULT_HYSTERESIS = 0.0 diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index c4eabcfe6a5..84656ad4360 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/threshold", "codeowners": ["@fabaff"], "quality_scale": "internal", - "iot_class": "local_polling" + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/threshold/strings.json b/homeassistant/components/threshold/strings.json new file mode 100644 index 00000000000..dbe8bf39b1e --- /dev/null +++ b/homeassistant/components/threshold/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "step": { + "user": { + "title": "New Threshold Sensor", + "description": "Configure when the sensor should turn on and off.\n\nOnly lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit].", + "data": { + "entity_id": "Input sensor", + "hysteresis": "Hysteresis", + "lower": "Lower limit", + "mode": "Threshold mode", + "name": "Name", + "upper": "Upper limit" + } + } + }, + "error": { + "need_lower_upper": "Lower and upper limits can't both be empty" + } + }, + "options": { + "step": { + "init": { + "description": "[%key:component::threshold::config::step::user::description%]", + "data": { + "entity_id": "[%key:component::threshold::config::step::user::data::entity_id%]", + "hysteresis": "[%key:component::threshold::config::step::user::data::hysteresis%]", + "lower": "[%key:component::threshold::config::step::user::data::lower%]", + "mode": "[%key:component::threshold::config::step::user::data::mode%]", + "name": "[%key:component::threshold::config::step::user::data::name%]", + "upper": "[%key:component::threshold::config::step::user::data::upper%]" + } + } + }, + "error": { + "need_lower_upper": "[%key:component::threshold::config::error::need_lower_upper%]" + } + } +} diff --git a/homeassistant/components/threshold/translations/en.json b/homeassistant/components/threshold/translations/en.json new file mode 100644 index 00000000000..ace0621ab02 --- /dev/null +++ b/homeassistant/components/threshold/translations/en.json @@ -0,0 +1,39 @@ +{ + "config": { + "error": { + "need_lower_upper": "Lower and upper limits can't both be empty" + }, + "step": { + "user": { + "data": { + "entity_id": "Input sensor", + "hysteresis": "Hysteresis", + "lower": "Lower limit", + "mode": "Threshold mode", + "name": "Name", + "upper": "Upper limit" + }, + "description": "Configure when the sensor should turn on and off.\n\nOnly lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit].", + "title": "New Threshold Sensor" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Lower and upper limits can't both be empty" + }, + "step": { + "init": { + "data": { + "entity_id": "Input sensor", + "hysteresis": "Hysteresis", + "lower": "Lower limit", + "mode": "Threshold mode", + "name": "Name", + "upper": "Upper limit" + }, + "description": "Configure when the sensor should turn on and off.\n\nOnly lower limit configured - Turn on when the input sensor's value is less than the lower limit.\nOnly upper limit configured - Turn on when the input sensor's value is greater than the upper limit.\nBoth lower and upper limit configured - Turn on when the input sensor's value is in the range [lower limit .. upper limit]." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 058bb936c7b..25d6ec2c807 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -337,6 +337,7 @@ FLOWS = { "tasmota", "tellduslive", "tesla_wall_connector", + "threshold", "tibber", "tile", "tolo", diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 2e9c55baedd..2611841e1cf 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -16,6 +16,10 @@ from homeassistant.data_entry_flow import FlowResult, UnknownHandler from . import entity_registry as er +class HelperFlowError(Exception): + """Validation failed.""" + + @dataclass class HelperFlowStep: """Define a helper config or options flow step.""" @@ -24,6 +28,12 @@ class HelperFlowStep: # fails, the step will be retried. If the schema is None, no user input is requested. schema: vol.Schema | None + # Optional function to validate user input. + # The validate_user_input function is called if the schema validates successfully. + # The validate_user_input function is passed the user input from the current step. + # The validate_user_input should raise HelperFlowError is user input is invalid. + validate_user_input: Callable[[dict[str, Any]], dict[str, Any]] = lambda x: x + # Optional function to identify next step. # The next_step function is called if the schema validates successfully or if no # schema is defined. The next_step function is passed the union of config entry @@ -52,6 +62,13 @@ class HelperCommonFlowHandler: """Handle a step.""" next_step_id: str = step_id + if user_input is not None and self._flow[next_step_id].schema is not None: + # Do extra validation of user input + try: + user_input = self._flow[next_step_id].validate_user_input(user_input) + except HelperFlowError as exc: + return self._show_next_step(next_step_id, exc, user_input) + if user_input is not None: # User input was validated successfully, update options self._options.update(user_input) @@ -67,21 +84,35 @@ class HelperCommonFlowHandler: next_step_id = next_step_id_or_end_flow + return self._show_next_step(next_step_id) + + def _show_next_step( + self, + next_step_id: str, + error: HelperFlowError | None = None, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Show step for next step.""" + options = dict(self._options) + if user_input: + options.update(user_input) if (data_schema := self._flow[next_step_id].schema) and data_schema.schema: - # Copy the schema, then set suggested field values to saved options - schema = dict(data_schema.schema) - for key in list(schema): - if key in self._options and isinstance(key, vol.Marker): + # Make a copy of the schema with suggested values set to saved options + schema = {} + for key, val in data_schema.schema.items(): + new_key = key + if key in options and isinstance(key, vol.Marker): # Copy the marker to not modify the flow schema new_key = copy.copy(key) - new_key.description = {"suggested_value": self._options[key]} - val = schema.pop(key) - schema[new_key] = val + new_key.description = {"suggested_value": options[key]} + schema[new_key] = val data_schema = vol.Schema(schema) + errors = {"base": str(error)} if error else None + # Show form for next step return self._handler.async_show_form( - step_id=next_step_id, data_schema=data_schema + step_id=next_step_id, data_schema=data_schema, errors=errors ) diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py new file mode 100644 index 00000000000..0a10c24a2f6 --- /dev/null +++ b/tests/components/threshold/test_config_flow.py @@ -0,0 +1,162 @@ +"""Test the Threshold config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.threshold.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + input_sensor = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.threshold.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "entity_id": input_sensor, + "lower": -2, + "upper": 0.0, + "name": "My threshold sensor", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My threshold sensor" + assert result["data"] == {} + assert result["options"] == { + "entity_id": input_sensor, + "hysteresis": 0.0, + "lower": -2.0, + "name": "My threshold sensor", + "upper": 0.0, + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "entity_id": input_sensor, + "hysteresis": 0.0, + "lower": -2.0, + "name": "My threshold sensor", + "upper": 0.0, + } + assert config_entry.title == "My threshold sensor" + + +@pytest.mark.parametrize("extra_input_data,error", (({}, "need_lower_upper"),)) +async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: + """Test not providing lower or upper limit fails.""" + input_sensor = "sensor.input" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "entity_id": input_sensor, + "name": "My threshold sensor", + **extra_input_data, + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": error} + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +async def test_options(hass: HomeAssistant) -> None: + """Test reconfiguring.""" + input_sensor = "sensor.input" + hass.states.async_set(input_sensor, "10") + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": input_sensor, + "hysteresis": 0.0, + "lower": -2.0, + "name": "My threshold", + "upper": None, + }, + title="My threshold", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "hysteresis") == 0.0 + assert get_suggested(schema, "lower") == -2.0 + assert get_suggested(schema, "upper") is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "hysteresis": 0.0, + "upper": 20.0, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entity_id": input_sensor, + "hysteresis": 0.0, + "lower": None, + "name": "My threshold", + "upper": 20.0, + } + assert config_entry.data == {} + assert config_entry.options == { + "entity_id": input_sensor, + "hysteresis": 0.0, + "lower": None, + "name": "My threshold", + "upper": 20.0, + } + assert config_entry.title == "My threshold" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 2 + + # Check the state of the entity has changed as expected + state = hass.states.get("binary_sensor.my_threshold") + assert state.state == "off" + assert state.attributes["type"] == "upper" diff --git a/tests/components/threshold/test_init.py b/tests/components/threshold/test_init.py new file mode 100644 index 00000000000..82d183ad380 --- /dev/null +++ b/tests/components/threshold/test_init.py @@ -0,0 +1,61 @@ +"""Test the Min/Max integration.""" +import pytest + +from homeassistant.components.threshold.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("binary_sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + hass.states.async_set("sensor.input", "-10") + + input_sensor = "sensor.input" + + registry = er.async_get(hass) + threshold_entity_id = f"{platform}.input_threshold" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_id": input_sensor, + "hysteresis": 0.0, + "lower": -2.0, + "name": "Input threshold", + "upper": None, + }, + title="Input threshold", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(threshold_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(threshold_entity_id) + assert state.state == "on" + assert state.attributes["entity_id"] == input_sensor + assert state.attributes["hysteresis"] == 0.0 + assert state.attributes["lower"] == -2.0 + assert state.attributes["position"] == "below" + assert state.attributes["sensor_value"] == -10.0 + assert state.attributes["type"] == "lower" + assert state.attributes["upper"] is None + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(threshold_entity_id) is None + assert registry.async_get(threshold_entity_id) is None From 1b955970f8bc8af3c9a2d34c35fcc03f155b015c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Mar 2022 12:14:34 +0100 Subject: [PATCH 0602/1054] Allow hiding and unhiding group members (#68192) --- homeassistant/components/group/__init__.py | 24 ++- homeassistant/components/group/config_flow.py | 41 ++++- homeassistant/components/group/const.py | 3 + homeassistant/helpers/entity_registry.py | 16 ++ .../helpers/helper_config_entry_flow.py | 31 +++- tests/components/group/test_config_flow.py | 164 +++++++++++++++++- tests/components/group/test_init.py | 94 ++++++++++ 7 files changed, 359 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/group/const.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 16958be9663..5d7b12bb595 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -28,8 +28,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id -from homeassistant.helpers import start -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er, start from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event @@ -40,6 +39,8 @@ from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass +from .const import CONF_HIDE_MEMBERS + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs DOMAIN = "group" @@ -238,6 +239,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + # Unhide the group members + registry = er.async_get(hass) + + if not entry.options[CONF_HIDE_MEMBERS]: + return + + for member in entry.options[CONF_ENTITIES]: + if not (entity_id := er.async_resolve_entity_id(registry, member)): + continue + if (entity_entry := registry.async_get(entity_id)) is None: + continue + if entity_entry.hidden_by != er.RegistryEntryHider.INTEGRATION: + continue + + registry.async_update_entity(entity_id, hidden_by=None) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all groups found defined in the configuration.""" if DOMAIN not in hass.data: diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index d1cd258b7b8..38c08692fe4 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -7,8 +7,8 @@ from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_ENTITIES -from homeassistant.core import callback -from homeassistant.helpers import selector +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, HelperFlowStep, @@ -16,6 +16,7 @@ from homeassistant.helpers.helper_config_entry_flow import ( from . import DOMAIN from .binary_sensor import CONF_ALL +from .const import CONF_HIDE_MEMBERS def basic_group_options_schema(domain: str) -> vol.Schema: @@ -25,6 +26,9 @@ def basic_group_options_schema(domain: str) -> vol.Schema: vol.Required(CONF_ENTITIES): selector.selector( {"entity": {"domain": domain, "multiple": True}} ), + vol.Required(CONF_HIDE_MEMBERS, default=False): selector.selector( + {"boolean": {}} + ), } ) @@ -98,6 +102,39 @@ class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" return cast(str, options["name"]) if "name" in options else "" + + @callback + def async_config_flow_finished(self, options: Mapping[str, Any]) -> None: + """Hide the group members if requested.""" + if options[CONF_HIDE_MEMBERS]: + _async_hide_members( + self.hass, options[CONF_ENTITIES], er.RegistryEntryHider.INTEGRATION + ) + + @callback + @staticmethod + def async_options_flow_finished( + hass: HomeAssistant, options: Mapping[str, Any] + ) -> None: + """Hide or unhide the group members as requested.""" + hidden_by = ( + er.RegistryEntryHider.INTEGRATION if options[CONF_HIDE_MEMBERS] else None + ) + _async_hide_members(hass, options[CONF_ENTITIES], hidden_by) + + +def _async_hide_members( + hass: HomeAssistant, members: list[str], hidden_by: er.RegistryEntryHider | None +) -> None: + """Hide or unhide group members.""" + registry = er.async_get(hass) + for member in members: + if not (entity_id := er.async_resolve_entity_id(registry, member)): + continue + if entity_id not in registry.entities: + continue + registry.async_update_entity(entity_id, hidden_by=hidden_by) diff --git a/homeassistant/components/group/const.py b/homeassistant/components/group/const.py new file mode 100644 index 00000000000..82817e71add --- /dev/null +++ b/homeassistant/components/group/const.py @@ -0,0 +1,3 @@ +"""Constants for the Group integration.""" + +CONF_HIDE_MEMBERS = "hide_members" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5f91911044e..90ca1e55cf2 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -961,6 +961,22 @@ def async_validate_entity_id(registry: EntityRegistry, entity_id_or_uuid: str) - return entry.entity_id +@callback +def async_resolve_entity_id( + registry: EntityRegistry, entity_id_or_uuid: str +) -> str | None: + """Validate and resolve an entity id or UUID to an entity id. + + Returns None if the entity or UUID is invalid, or if the UUID is not + associated with an entity registry item. + """ + if valid_entity_id(entity_id_or_uuid): + return entity_id_or_uuid + if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None: + return None + return entry.entity_id + + @callback def async_validate_entity_ids( registry: EntityRegistry, entity_ids_or_uuids: list[str] diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 2611841e1cf..b9e35d9d336 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -137,7 +137,9 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): if cls.options_flow is None: raise UnknownHandler - return HelperOptionsFlowHandler(config_entry, cls.options_flow) + return HelperOptionsFlowHandler( + config_entry, cls.options_flow, cls.async_options_flow_finished + ) # Create an async_get_options_flow method cls.async_get_options_flow = _async_get_options_flow # type: ignore[assignment] @@ -167,6 +169,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): # pylint: disable-next=no-self-use @abstractmethod + @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title. @@ -174,6 +177,25 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): input from the config flow steps. """ + @callback + def async_config_flow_finished(self, options: Mapping[str, Any]) -> None: + """Take necessary actions after the config flow is finished, if needed. + + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ + + @callback + @staticmethod + def async_options_flow_finished( + hass: HomeAssistant, options: Mapping[str, Any] + ) -> None: + """Take necessary actions after the options flow is finished, if needed. + + The options parameter contains config entry options, which is the union of stored + options and user input from the options flow steps. + """ + @callback def async_create_entry( # pylint: disable=arguments-differ self, @@ -181,6 +203,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): **kwargs: Any, ) -> FlowResult: """Finish config flow and create a config entry.""" + self.async_config_flow_finished(data) return super().async_create_entry( data={}, options=data, title=self.async_config_entry_title(data), **kwargs ) @@ -193,10 +216,12 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): self, config_entry: config_entries.ConfigEntry, options_flow: dict[str, vol.Schema], + async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None], ) -> None: """Initialize options flow.""" self._common_handler = HelperCommonFlowHandler(self, options_flow, config_entry) self._config_entry = config_entry + self._async_options_flow_finished = async_options_flow_finished for step in options_flow: setattr(self, f"async_step_{step}", self._async_step) @@ -210,10 +235,12 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): @callback def async_create_entry( # pylint: disable=arguments-differ self, + data: Mapping[str, Any], **kwargs: Any, ) -> FlowResult: """Finish config flow and create a config entry.""" - return super().async_create_entry(title="", **kwargs) + self._async_options_flow_finished(self.hass, data) + return super().async_create_entry(title="", data=data, **kwargs) @callback diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index cf0254768a3..16fa2ad6933 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -68,8 +69,9 @@ async def test_config_flow( assert result["title"] == "Living Room" assert result["data"] == {} assert result["options"] == { - "group_type": group_type, "entities": members, + "group_type": group_type, + "hide_members": False, "name": "Living Room", **extra_options, } @@ -78,9 +80,10 @@ async def test_config_flow( config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry.data == {} assert config_entry.options == { - "group_type": group_type, - "name": "Living Room", "entities": members, + "group_type": group_type, + "hide_members": False, + "name": "Living Room", **extra_options, } @@ -91,6 +94,69 @@ async def test_config_flow( assert state.attributes[key] == extra_attrs[key] +@pytest.mark.parametrize( + "hide_members,hidden_by", ((False, None), (True, "integration")) +) +@pytest.mark.parametrize( + "group_type,extra_input", + ( + ("binary_sensor", {"all": False}), + ("cover", {}), + ("fan", {}), + ("light", {}), + ("media_player", {}), + ), +) +async def test_config_flow_hides_members( + hass: HomeAssistant, group_type, extra_input, hide_members, hidden_by +) -> None: + """Test the config flow hides members if requested.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + group_type, "test", "unique", suggested_object_id="one" + ) + assert entry.entity_id == f"{group_type}.one" + assert entry.hidden_by is None + + entry = registry.async_get_or_create( + group_type, "test", "unique3", suggested_object_id="three" + ) + assert entry.entity_id == f"{group_type}.three" + assert entry.hidden_by is None + + members = [f"{group_type}.one", f"{group_type}.two", fake_uuid, entry.id] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"group_type": group_type}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "name": "Living Room", + "entities": members, + "hide_members": hide_members, + **extra_input, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by + assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by + + def get_suggested(schema, key): """Get suggested value for key in voluptuous schema.""" for k in schema.keys(): @@ -124,7 +190,7 @@ async def test_options( for member in members2: hass.states.async_set(member, member_state, {}) - switch_as_x_config_entry = MockConfigEntry( + group_config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={ @@ -135,9 +201,9 @@ async def test_options( }, title="Bed Room", ) - switch_as_x_config_entry.add_to_hass(hass) + group_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + assert await hass.config_entries.async_setup(group_config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get(f"{group_type}.bed_room") @@ -159,15 +225,17 @@ async def test_options( ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - "group_type": group_type, "entities": members2, + "group_type": group_type, + "hide_members": False, "name": "Bed Room", **extra_options, } assert config_entry.data == {} assert config_entry.options == { - "group_type": group_type, "entities": members2, + "group_type": group_type, + "hide_members": False, "name": "Bed Room", **extra_options, } @@ -196,3 +264,83 @@ async def test_options( assert get_suggested(result["data_schema"].schema, "entities") is None assert get_suggested(result["data_schema"].schema, "name") is None + + +@pytest.mark.parametrize( + "hide_members,hidden_by_initial,hidden_by", + ((False, "integration", None), (True, None, "integration")), +) +@pytest.mark.parametrize( + "group_type,extra_input", + ( + ("binary_sensor", {"all": False}), + ("cover", {}), + ("fan", {}), + ("light", {}), + ("media_player", {}), + ), +) +async def test_options_flow_hides_members( + hass: HomeAssistant, + group_type, + extra_input, + hide_members, + hidden_by_initial, + hidden_by, +) -> None: + """Test the options flow hides or unhides members if requested.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + registry = er.async_get(hass) + entry = registry.async_get_or_create( + group_type, + "test", + "unique1", + suggested_object_id="one", + hidden_by=hidden_by_initial, + ) + assert entry.entity_id == f"{group_type}.one" + + entry = registry.async_get_or_create( + group_type, + "test", + "unique3", + suggested_object_id="three", + hidden_by=hidden_by_initial, + ) + assert entry.entity_id == f"{group_type}.three" + + members = [f"{group_type}.one", f"{group_type}.two", fake_uuid, entry.id] + + group_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": members, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_input, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(group_config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members, + "hide_members": hide_members, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + + assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by + assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 19b9245dc7f..ba91b9dbbbc 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1416,3 +1416,97 @@ async def test_setup_and_remove_config_entry( # Check the state and entity registry entry are removed assert hass.states.get(f"{group_type}.bed_room") is None assert registry.async_get(f"{group_type}.bed_room") is None + + +@pytest.mark.parametrize( + "hide_members,hidden_by_initial,hidden_by", + ( + (False, "integration", "integration"), + (False, None, None), + (False, "user", "user"), + (True, "integration", None), + (True, None, None), + (True, "user", "user"), + ), +) +@pytest.mark.parametrize( + "group_type,extra_options", + ( + ("binary_sensor", {"all": False}), + ("cover", {}), + ("fan", {}), + ("light", {}), + ("media_player", {}), + ), +) +async def test_unhide_members_on_remove( + hass: HomeAssistant, + group_type: str, + extra_options: dict[str, Any], + hide_members: bool, + hidden_by_initial: str, + hidden_by: str, +) -> None: + """Test removing a config entry.""" + registry = er.async_get(hass) + + registry = er.async_get(hass) + entry1 = registry.async_get_or_create( + group_type, + "test", + "unique1", + suggested_object_id="one", + hidden_by=hidden_by_initial, + ) + assert entry1.entity_id == f"{group_type}.one" + + entry3 = registry.async_get_or_create( + group_type, + "test", + "unique3", + suggested_object_id="three", + hidden_by=hidden_by_initial, + ) + assert entry3.entity_id == f"{group_type}.three" + + entry4 = registry.async_get_or_create( + group_type, + "test", + "unique4", + suggested_object_id="four", + ) + assert entry4.entity_id == f"{group_type}.four" + + members = [f"{group_type}.one", f"{group_type}.two", entry3.id, entry4.id] + + # Setup the config entry + group_config_entry = MockConfigEntry( + data={}, + domain=group.DOMAIN, + options={ + "entities": members, + "group_type": group_type, + "hide_members": hide_members, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state is present + assert hass.states.get(f"{group_type}.bed_room") + + # Remove one entity registry entry, to make sure this does not trip up config entry + # removal + registry.async_remove(entry4.entity_id) + + # Remove the config entry + assert await hass.config_entries.async_remove(group_config_entry.entry_id) + await hass.async_block_till_done() + + # Check the group members are unhidden + assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by + assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by From d17f8e9ed6cd8b4e3e44e404b639fd58d595a3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 22 Mar 2022 12:21:12 +0100 Subject: [PATCH 0603/1054] Add update platform to the Supervisor integration (#68475) --- homeassistant/components/hassio/__init__.py | 142 ++++++- .../components/hassio/binary_sensor.py | 1 + homeassistant/components/hassio/const.py | 5 + homeassistant/components/hassio/entity.py | 63 ++- homeassistant/components/hassio/handler.py | 24 +- homeassistant/components/hassio/update.py | 276 +++++++++++++ tests/components/hassio/test_binary_sensor.py | 3 + tests/components/hassio/test_init.py | 31 +- tests/components/hassio/test_sensor.py | 3 + tests/components/hassio/test_update.py | 372 ++++++++++++++++++ 10 files changed, 901 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/hassio/update.py create mode 100644 tests/components/hassio/test_update.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index b5549c1b5e4..8a69f553025 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -51,6 +51,7 @@ from .auth import async_setup_auth_view from .const import ( ATTR_ADDON, ATTR_ADDONS, + ATTR_CHANGELOG, ATTR_DISCOVERY, ATTR_FOLDERS, ATTR_HOMEASSISTANT, @@ -63,6 +64,9 @@ from .const import ( ATTR_URL, ATTR_VERSION, DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, DOMAIN, SupervisorEntityModel, ) @@ -77,7 +81,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE] CONF_FRONTEND_REPO = "development_repo" @@ -93,6 +97,7 @@ DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" +DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" DATA_ADDONS_STATS = "hassio_addons_stats" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) @@ -239,14 +244,22 @@ async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: @bind_hass @api_data -async def async_update_addon(hass: HomeAssistant, slug: str) -> dict: +async def async_update_addon( + hass: HomeAssistant, + slug: str, + backup: bool = False, +) -> dict: """Update add-on. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] command = f"/addons/{slug}/update" - return await hassio.send_command(command, timeout=None) + return await hassio.send_command( + command, + payload={"backup": backup}, + timeout=None, + ) @bind_hass @@ -323,6 +336,52 @@ async def async_create_backup( return await hassio.send_command(command, payload=payload, timeout=None) +@bind_hass +@api_data +async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict: + """Update Home Assistant Operating System. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/os/update" + return await hassio.send_command( + command, + payload={"version": version}, + timeout=None, + ) + + +@bind_hass +@api_data +async def async_update_supervisor(hass: HomeAssistant) -> dict: + """Update Home Assistant Supervisor. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/supervisor/update" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_update_core( + hass: HomeAssistant, version: str | None = None, backup: bool = False +) -> dict: + """Update Home Assistant Core. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/core/update" + return await hassio.send_command( + command, + payload={"version": version, "backup": backup}, + timeout=None, + ) + + @callback @bind_hass def get_info(hass): @@ -373,6 +432,16 @@ def get_addons_stats(hass): return hass.data.get(DATA_ADDONS_STATS) +@callback +@bind_hass +def get_addons_changelogs(hass): + """Return Addons changelogs. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_CHANGELOGS) + + @callback @bind_hass def get_os_info(hass): @@ -533,6 +602,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: stats = await hassio.get_addon_stats(slug) return (slug, stats) + async def update_addon_changelog(slug): + """Return the changelog for an add-on.""" + changelog = await hassio.get_addon_changelog(slug) + return (slug, changelog) + async def update_info_data(now): """Update last available supervisor information.""" @@ -562,6 +636,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: *[update_addon_stats(addon[ATTR_SLUG]) for addon in addons] ) hass.data[DATA_ADDONS_STATS] = dict(stats_data) + hass.data[DATA_ADDONS_CHANGELOGS] = dict( + await asyncio.gather( + *[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons] + ) + ) if ADDONS_COORDINATOR in hass.data: await hass.data[ADDONS_COORDINATOR].async_refresh() @@ -699,6 +778,42 @@ def async_register_os_in_dev_reg( dev_reg.async_get_or_create(config_entry_id=entry_id, **params) +@callback +def async_register_core_in_dev_reg( + entry_id: str, + dev_reg: DeviceRegistry, + core_dict: dict[str, Any], +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "core")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.CORE, + sw_version=core_dict[ATTR_VERSION], + name="Home Assistant Core", + entry_type=DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + +@callback +def async_register_supervisor_in_dev_reg( + entry_id: str, + dev_reg: DeviceRegistry, + supervisor_dict: dict[str, Any], +) -> None: + """Register OS in the device registry.""" + params = DeviceInfo( + identifiers={(DOMAIN, "supervisor")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.SUPERVIOSR, + sw_version=supervisor_dict[ATTR_VERSION], + name="Home Assistant Supervisor", + entry_type=DeviceEntryType.SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) + + @callback def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None: """Remove addons from the device registry.""" @@ -720,6 +835,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): name=DOMAIN, update_method=self._async_update_data, ) + self.hassio: HassIO = hass.data[DOMAIN] self.data = {} self.entry_id = config_entry.entry_id self.dev_reg = dev_reg @@ -730,6 +846,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): new_data = {} supervisor_info = get_supervisor_info(self.hass) addons_stats = get_addons_stats(self.hass) + addons_changelogs = get_addons_changelogs(self.hass) store_data = get_store(self.hass) repositories = { @@ -741,6 +858,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): addon[ATTR_SLUG]: { **addon, **((addons_stats or {}).get(addon[ATTR_SLUG], {})), + ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]), ATTR_REPOSITORY: repositories.get( addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") ), @@ -748,16 +866,25 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): for addon in supervisor_info.get("addons", []) } if self.is_hass_os: - new_data["os"] = get_os_info(self.hass) + new_data[DATA_KEY_OS] = get_os_info(self.hass) + + new_data[DATA_KEY_CORE] = get_core_info(self.hass) + new_data[DATA_KEY_SUPERVISOR] = supervisor_info # If this is the initial refresh, register all addons and return the dict if not self.data: async_register_addons_in_dev_reg( self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values() ) + async_register_core_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE] + ) + async_register_supervisor_in_dev_reg( + self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR] + ) if self.is_hass_os: async_register_os_in_dev_reg( - self.entry_id, self.dev_reg, new_data["os"] + self.entry_id, self.dev_reg, new_data[DATA_KEY_OS] ) # Remove add-ons that are no longer installed from device registry @@ -782,3 +909,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): return {} return new_data + + async def force_info_update_supervisor(self) -> None: + """Force update of the supervisor info.""" + self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info() + await self.async_refresh() diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index b5525fe9ce4..c2bcd5eaf68 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -32,6 +32,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): COMMON_ENTITY_DESCRIPTIONS = ( HassioBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 device_class=BinarySensorDeviceClass.UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 7cdc87708ae..7f62748b835 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -43,6 +43,7 @@ ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" ATTR_CPU_PERCENT = "cpu_percent" +ATTR_CHANGELOG = "changelog" ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" ATTR_STATE = "state" @@ -53,6 +54,8 @@ ATTR_REPOSITORY = "repository" DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" +DATA_KEY_SUPERVISOR = "supervisor" +DATA_KEY_CORE = "core" class SupervisorEntityModel(str, Enum): @@ -60,3 +63,5 @@ class SupervisorEntityModel(str, Enum): ADDON = "Home Assistant Add-on" OS = "Home Assistant Operating System" + CORE = "Home Assistant Core" + SUPERVIOSR = "Home Assistant Supervisor" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 9f727269fed..fb9f70f1417 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -8,7 +8,13 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator -from .const import ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ( + ATTR_SLUG, + DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, +) class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): @@ -33,8 +39,9 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Return True if entity is available.""" return ( super().available + and DATA_KEY_ADDONS in self.coordinator.data and self.entity_description.key - in self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] + in self.coordinator.data[DATA_KEY_ADDONS].get(self._addon_slug, {}) ) @@ -58,5 +65,57 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Return True if entity is available.""" return ( super().available + and DATA_KEY_OS in self.coordinator.data and self.entity_description.key in self.coordinator.data[DATA_KEY_OS] ) + + +class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): + """Base Entity for Supervisor.""" + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_name = f"Home Assistant Supervisor: {entity_description.name}" + self._attr_unique_id = f"home_assistant_supervisor_{entity_description.key}" + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "supervisor")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and DATA_KEY_OS in self.coordinator.data + and self.entity_description.key + in self.coordinator.data[DATA_KEY_SUPERVISOR] + ) + + +class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): + """Base Entity for Core.""" + + def __init__( + self, + coordinator: HassioDataUpdateCoordinator, + entity_description: EntityDescription, + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_name = f"Home Assistant Core: {entity_description.name}" + self._attr_unique_id = f"home_assistant_core_{entity_description.key}" + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "core")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and DATA_KEY_CORE in self.coordinator.data + and self.entity_description.key in self.coordinator.data[DATA_KEY_CORE] + ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 4a0312bcecb..66395b49400 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -127,6 +127,15 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/stats", method="get") + def get_addon_changelog(self, addon): + """Return changelog for an Add-on. + + This method returns a coroutine. + """ + return self.send_command( + f"/addons/{addon}/changelog", method="get", return_text=True + ) + @api_data def get_store(self): """Return data from the store. @@ -212,7 +221,14 @@ class HassIO: "/supervisor/options", payload={"diagnostics": diagnostics} ) - async def send_command(self, command, method="post", payload=None, timeout=10): + async def send_command( + self, + command, + method="post", + payload=None, + timeout=10, + return_text=False, + ): """Send API command to Hass.io. This method is a coroutine. @@ -230,8 +246,10 @@ class HassIO: _LOGGER.error("%s return code %d", command, request.status) raise HassioAPIError() - answer = await request.json() - return answer + if return_text: + return await request.text(encoding="utf-8") + + return await request.json() except asyncio.TimeoutError: _LOGGER.error("Timeout on %s request", command) diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py new file mode 100644 index 00000000000..71f97c62b96 --- /dev/null +++ b/homeassistant/components/hassio/update.py @@ -0,0 +1,276 @@ +"""Update platform for Supervisor.""" +from __future__ import annotations + +from typing import Any + +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.components.update import ( + UpdateEntity, + UpdateEntityDescription, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ICON, ATTR_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ( + ADDONS_COORDINATOR, + async_update_addon, + async_update_core, + async_update_os, + async_update_supervisor, +) +from .const import ( + ATTR_CHANGELOG, + ATTR_VERSION, + ATTR_VERSION_LATEST, + DATA_KEY_ADDONS, + DATA_KEY_CORE, + DATA_KEY_OS, + DATA_KEY_SUPERVISOR, +) +from .entity import ( + HassioAddonEntity, + HassioCoreEntity, + HassioOSEntity, + HassioSupervisorEntity, +) +from .handler import HassioAPIError + +ENTITY_DESCRIPTION = UpdateEntityDescription( + name="Update", + key=ATTR_VERSION_LATEST, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Supervisor update based on a config entry.""" + coordinator = hass.data[ADDONS_COORDINATOR] + + entities = [ + SupervisorSupervisorUpdateEntity( + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ), + SupervisorCoreUpdateEntity( + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ), + ] + + for addon in coordinator.data[DATA_KEY_ADDONS].values(): + entities.append( + SupervisorAddonUpdateEntity( + addon=addon, + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ) + ) + + if coordinator.is_hass_os: + entities.append( + SupervisorOSUpdateEntity( + coordinator=coordinator, + entity_description=ENTITY_DESCRIPTION, + ) + ) + + async_add_entities(entities) + + +class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): + """Update entity to handle updates for the Supervisor add-ons.""" + + _attr_supported_features = UpdateEntityFeature.INSTALL | UpdateEntityFeature.BACKUP + + @property + def title(self) -> str | None: + """Return the title of the update.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_NAME] + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + ATTR_VERSION_LATEST + ] + + @property + def current_version(self) -> str | None: + """Version currently in use.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_VERSION] + + @property + def release_summary(self) -> str | None: + """Release summary for the add-on.""" + return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_CHANGELOG] + + @property + def entity_picture(self) -> str | None: + """Return the icon of the add-on if any.""" + if not self.available: + return None + if self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ATTR_ICON]: + return f"/api/hassio/addons/{self._addon_slug}/icon" + return None + + async def async_install( + self, + version: str | None = None, + backup: bool | None = False, + **kwargs: Any, + ) -> None: + """Install an update.""" + try: + await async_update_addon(self.hass, slug=self._addon_slug, backup=backup) + except HassioAPIError as err: + raise HomeAssistantError(f"Error updating {self.title}: {err}") from err + else: + await self.coordinator.force_info_update_supervisor() + + +class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): + """Update entity to handle updates for the Home Assistant Operating System.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION + ) + _attr_title = "Home Assistant Operating System" + + @property + def latest_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST] + + @property + def current_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION] + + @property + def entity_picture(self) -> str | None: + """Return the iconof the entity.""" + return "https://brands.home-assistant.io/homeassistant/icon.png" + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + version = AwesomeVersion(self.latest_version) + if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN: + return "https://github.com/home-assistant/operating-system/commits/dev" + return ( + f"https://github.com/home-assistant/operating-system/releases/tag/{version}" + ) + + async def async_install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update.""" + try: + await async_update_os(self.hass, version) + except HassioAPIError as err: + raise HomeAssistantError( + f"Error updating Home Assistant Operating System: {err}" + ) from err + + +class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): + """Update entity to handle updates for the Home Assistant Supervisor.""" + + _attr_supported_features = UpdateEntityFeature.INSTALL + _attr_title = "Home Assistant Supervisor" + + @property + def latest_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST] + + @property + def current_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION] + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + version = AwesomeVersion(self.latest_version) + if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN: + return "https://github.com/home-assistant/supervisor/commits/main" + return f"https://github.com/home-assistant/supervisor/releases/tag/{version}" + + @property + def entity_picture(self) -> str | None: + """Return the iconof the entity.""" + return "https://brands.home-assistant.io/hassio/icon.png" + + async def async_install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update.""" + try: + await async_update_supervisor(self.hass) + except HassioAPIError as err: + raise HomeAssistantError( + f"Error updating Home Assistant Supervisor: {err}" + ) from err + + +class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity): + """Update entity to handle updates for Home Assistant Core.""" + + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.SPECIFIC_VERSION + | UpdateEntityFeature.BACKUP + ) + _attr_title = "Home Assistant Core" + + @property + def latest_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST] + + @property + def current_version(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION] + + @property + def entity_picture(self) -> str | None: + """Return the iconof the entity.""" + return "https://brands.home-assistant.io/homeassistant/icon.png" + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + version = AwesomeVersion(self.latest_version) + if version.dev: + return "https://github.com/home-assistant/core/commits/dev" + return f"https://{'rc' if version.beta else 'www'}.home-assistant.io/latest-release-notes/" + + async def async_install( + self, + version: str | None = None, + backup: bool | None = None, + **kwargs: Any, + ) -> None: + """Install an update.""" + try: + await async_update_core(self.hass, version=version, backup=backup) + except HassioAPIError as err: + raise HomeAssistantError( + f"Error updating Home Assistant Core {err}" + ) from err diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index e4263eb5529..e48f8a6a481 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -69,6 +69,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "data": { "result": "ok", + "version": "1.0.0", "version_latest": "1.0.0", "addons": [ { @@ -113,6 +114,8 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 689ec138043..30776fd5b17 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -55,7 +55,7 @@ def mock_all(aioclient_mock, request): ) aioclient_mock.get( "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, ) aioclient_mock.get( "http://127.0.0.1/os/info", @@ -65,7 +65,7 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/supervisor/info", json={ "result": "ok", - "data": {"version_latest": "1.0.0"}, + "data": {"version_latest": "1.0.0", "version": "1.0.0"}, "addons": [ { "name": "test", @@ -138,6 +138,8 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) @@ -496,12 +498,15 @@ async def test_device_registry_calls(hass): """Test device registry entries for hassio.""" dev_reg = async_get(hass) supervisor_mock_data = { + "version": "1.0.0", + "version_latest": "1.0.0", "addons": [ { "name": "test", "state": "started", "slug": "test", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", @@ -513,12 +518,13 @@ async def test_device_registry_calls(hass): "state": "started", "slug": "test2", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", "url": "https://github.com", }, - ] + ], } os_mock_data = { "board": "odroid-n2", @@ -539,21 +545,24 @@ async def test_device_registry_calls(hass): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(dev_reg.devices) == 3 + assert len(dev_reg.devices) == 5 supervisor_mock_data = { + "version": "1.0.0", + "version_latest": "1.0.0", "addons": [ { "name": "test2", "state": "started", "slug": "test2", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", "url": "https://github.com", }, - ] + ], } # Test that when addon is removed, next update will remove the add-on and subsequent updates won't @@ -566,19 +575,22 @@ async def test_device_registry_calls(hass): ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=1)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 2 + assert len(dev_reg.devices) == 4 async_fire_time_changed(hass, dt_util.now() + timedelta(hours=2)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 2 + assert len(dev_reg.devices) == 4 supervisor_mock_data = { + "version": "1.0.0", + "version_latest": "1.0.0", "addons": [ { "name": "test2", "slug": "test2", "state": "started", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", @@ -589,12 +601,13 @@ async def test_device_registry_calls(hass): "slug": "test3", "state": "stopped", "installed": True, + "icon": False, "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", "url": "https://github.com", }, - ] + ], } # Test that when addon is added, next update will reload the entry so we register @@ -608,4 +621,4 @@ async def test_device_registry_calls(hass): ): async_fire_time_changed(hass, dt_util.now() + timedelta(hours=3)) await hass.async_block_till_done() - assert len(dev_reg.devices) == 3 + assert len(dev_reg.devices) == 5 diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 481ba1b578f..df309e94360 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -62,6 +62,7 @@ def mock_all(aioclient_mock, request): "result": "ok", "data": { "result": "ok", + "version": "1.0.0", "version_latest": "1.0.0", "addons": [ { @@ -106,6 +107,8 @@ def mock_all(aioclient_mock, request): }, }, ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py new file mode 100644 index 00000000000..3c5f7b52e56 --- /dev/null +++ b/tests/components/hassio/test_update.py @@ -0,0 +1,372 @@ +"""The tests for the hassio update entities.""" + +import os +from unittest.mock import patch + +import pytest + +from homeassistant.components.hassio import DOMAIN +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock, request): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={ + "result": "ok", + "data": {"version_latest": "1.0.0dev222", "version": "1.0.0dev221"}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": { + "version_latest": "1.0.0dev2222", + "version": "1.0.0dev2221", + "update_available": False, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version": "1.0.0", + "version_latest": "1.0.1dev222", + "addons": [ + { + "name": "test", + "state": "started", + "slug": "test", + "installed": True, + "update_available": True, + "icon": False, + "version": "2.0.0", + "version_latest": "2.0.1", + "repository": "core", + "url": "https://github.com/home-assistant/addons/test", + }, + { + "name": "test2", + "state": "stopped", + "slug": "test2", + "installed": True, + "update_available": False, + "icon": True, + "version": "3.1.0", + "version_latest": "3.1.0", + "repository": "core", + "url": "https://github.com", + }, + ], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get("http://127.0.0.1/addons/test/changelog", text="") + aioclient_mock.get("http://127.0.0.1/addons/test2/changelog", text="") + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + + +@pytest.mark.parametrize( + "entity_id,expected", + [ + ("update.home_assistant_operating_system_update", "on"), + ("update.home_assistant_supervisor_update", "on"), + ("update.home_assistant_core_update", "on"), + ("update.test_update", "on"), + ("update.test2_update", "off"), + ], +) +async def test_update_entities(hass, entity_id, expected, aioclient_mock): + """Test update entities.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + # Verify that the entity have the expected state. + state = hass.states.get(entity_id) + assert state.state == expected + + +async def test_update_addon(hass, aioclient_mock): + """Test updating addon update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/addons/test/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.test_update"}, + blocking=True, + ) + + +async def test_update_os(hass, aioclient_mock): + """Test updating OS update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/os/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_operating_system_update"}, + blocking=True, + ) + + +async def test_update_core(hass, aioclient_mock): + """Test updating core update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/core/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_os_update"}, + blocking=True, + ) + + +async def test_update_supervisor(hass, aioclient_mock): + """Test updating supervisor update entity.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/supervisor/update", + json={"result": "ok", "data": {}}, + ) + + assert await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_supervisor_update"}, + blocking=True, + ) + + +async def test_update_addon_with_error(hass, aioclient_mock): + """Test updating addon update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/addons/test/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.test_update"}, + blocking=True, + ) + + +async def test_update_os_with_error(hass, aioclient_mock): + """Test updating OS update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/os/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_operating_system_update"}, + blocking=True, + ) + + +async def test_update_supervisor_with_error(hass, aioclient_mock): + """Test updating supervisor update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/supervisor/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_supervisor_update"}, + blocking=True, + ) + + +async def test_update_core_with_error(hass, aioclient_mock): + """Test updating core update entity with error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + with patch.dict(os.environ, MOCK_ENVIRON): + assert await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + await hass.async_block_till_done() + + aioclient_mock.post( + "http://127.0.0.1/core/update", + exc=HassioAPIError, + ) + + with pytest.raises(HomeAssistantError): + assert not await hass.services.async_call( + "update", + "install", + {"entity_id": "update.home_assistant_core_update"}, + blocking=True, + ) From 5afe8fd2db0a6b4cb07ea717487badca5f01420a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 22 Mar 2022 12:51:24 +0100 Subject: [PATCH 0604/1054] Revert "Add MQTT notify platform (#64728)" (#68505) This reverts commit e574a3ef1d855304b2a78c389861c421b1548d74. --- homeassistant/components/mqtt/__init__.py | 1 - .../components/mqtt/abbreviations.py | 2 - homeassistant/components/mqtt/const.py | 12 +- homeassistant/components/mqtt/discovery.py | 12 +- homeassistant/components/mqtt/mixins.py | 19 +- homeassistant/components/mqtt/notify.py | 406 -------- tests/components/mqtt/test_notify.py | 863 ------------------ 7 files changed, 14 insertions(+), 1301 deletions(-) delete mode 100644 homeassistant/components/mqtt/notify.py delete mode 100644 tests/components/mqtt/test_notify.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c74417ece37..48da8fd7e73 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -160,7 +160,6 @@ PLATFORMS = [ Platform.HUMIDIFIER, Platform.LIGHT, Platform.LOCK, - Platform.NOTIFY, Platform.NUMBER, Platform.SELECT, Platform.SCENE, diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 587f9617124..ddbced5286d 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -185,8 +185,6 @@ ABBREVIATIONS = { "set_fan_spd_t": "set_fan_speed_topic", "set_pos_tpl": "set_position_template", "set_pos_t": "set_position_topic", - "title": "title", - "trgts": "targets", "pos_t": "position_topic", "pos_tpl": "position_template", "spd_cmd_t": "speed_command_topic", diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 63b9d68b863..69865733763 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,6 +1,4 @@ """Constants used by multiple MQTT modules.""" -from typing import Final - from homeassistant.const import CONF_PAYLOAD ATTR_DISCOVERY_HASH = "discovery_hash" @@ -14,11 +12,11 @@ ATTR_TOPIC = "topic" CONF_AVAILABILITY = "availability" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" -CONF_COMMAND_TEMPLATE: Final = "command_template" -CONF_COMMAND_TOPIC: Final = "command_topic" -CONF_ENCODING: Final = "encoding" -CONF_QOS: Final = "qos" -CONF_RETAIN: Final = "retain" +CONF_COMMAND_TEMPLATE = "command_template" +CONF_COMMAND_TOPIC = "command_topic" +CONF_ENCODING = "encoding" +CONF_QOS = ATTR_QOS +CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" CONF_STATE_VALUE_TEMPLATE = "state_value_template" CONF_TOPIC = "topic" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 05e06fec666..11bc0f6839a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -15,7 +15,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -49,7 +48,6 @@ SUPPORTED_COMPONENTS = [ "humidifier", "light", "lock", - "notify", "number", "scene", "siren", @@ -234,15 +232,7 @@ async def async_start( # noqa: C901 from . import device_automation await device_automation.async_setup_entry(hass, config_entry) - elif component in "notify": - # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import notify - - await notify.async_setup_entry( - hass, config_entry, AddEntitiesCallback - ) - elif component in "tag": + elif component == "tag": # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import tag diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index c87e5ccba25..9f3722a8f31 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -5,7 +5,7 @@ from abc import abstractmethod from collections.abc import Callable import json import logging -from typing import Any, Protocol, cast +from typing import Any, Protocol import voluptuous as vol @@ -237,10 +237,10 @@ class SetupEntity(Protocol): async def async_setup_entry_helper(hass, domain, async_setup, schema): - """Set up entity, automation, notify service or tag creation dynamically through MQTT discovery.""" + """Set up entity, automation or tag creation dynamically through MQTT discovery.""" async def async_discover(discovery_payload): - """Discover and add an MQTT entity, automation, notify service or tag.""" + """Discover and add an MQTT entity, automation or tag.""" discovery_data = discovery_payload.discovery_data try: config = schema(discovery_payload) @@ -496,13 +496,11 @@ class MqttAvailability(Entity): return self._available_latest -async def cleanup_device_registry( - hass: HomeAssistant, device_id: str | None, config_entry_id: str | None -) -> None: - """Remove device registry entry if there are no remaining entities, triggers or notify services.""" +async def cleanup_device_registry(hass, device_id, config_entry_id): + """Remove device registry entry if there are no remaining entities or triggers.""" # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import device_trigger, notify, tag + # pylint: disable-next=import-outside-toplevel + from . import device_trigger, tag device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) @@ -513,10 +511,9 @@ async def cleanup_device_registry( ) and not await device_trigger.async_get_triggers(hass, device_id) and not tag.async_has_tags(hass, device_id) - and not notify.device_has_notify_services(hass, device_id) ): device_registry.async_update_device( - device_id, remove_config_entry_id=cast(str, config_entry_id) + device_id, remove_config_entry_id=config_entry_id ) diff --git a/homeassistant/components/mqtt/notify.py b/homeassistant/components/mqtt/notify.py deleted file mode 100644 index 9ba341aab0d..00000000000 --- a/homeassistant/components/mqtt/notify.py +++ /dev/null @@ -1,406 +0,0 @@ -"""Support for MQTT notify.""" -from __future__ import annotations - -import functools -import logging -from typing import Any, Final, TypedDict, cast - -import voluptuous as vol - -from homeassistant.components import notify -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify - -from . import PLATFORMS, MqttCommandTemplate -from .. import mqtt -from .const import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_PAYLOAD, - CONF_COMMAND_TEMPLATE, - CONF_COMMAND_TOPIC, - CONF_ENCODING, - CONF_QOS, - CONF_RETAIN, - DOMAIN, -) -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash -from .mixins import ( - MQTT_ENTITY_DEVICE_INFO_SCHEMA, - async_setup_entry_helper, - cleanup_device_registry, - device_info_from_config, -) - -CONF_TARGETS: Final = "targets" -CONF_TITLE: Final = "title" -CONF_CONFIG_ENTRY: Final = "config_entry" -CONF_DISCOVER_HASH: Final = "discovery_hash" - -MQTT_NOTIFY_SERVICES_SETUP = "mqtt_notify_services_setup" - -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_TARGETS, default=[]): cv.ensure_list, - vol.Optional(CONF_TITLE, default=notify.ATTR_TITLE_DEFAULT): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - } -) - -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, - }, - extra=vol.REMOVE_EXTRA, -) - -_LOGGER = logging.getLogger(__name__) - - -class MqttNotificationConfig(TypedDict, total=False): - """Supply service parameters for MqttNotificationService.""" - - command_topic: str - command_template: Template - encoding: str - name: str | None - qos: int - retain: bool - targets: list - title: str - device: ConfigType - - -async def async_initialize(hass: HomeAssistant) -> None: - """Initialize globals.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - hass.data.setdefault(MQTT_NOTIFY_SERVICES_SETUP, {}) - - -def device_has_notify_services(hass: HomeAssistant, device_id: str) -> bool: - """Check if the device has registered notify services.""" - if MQTT_NOTIFY_SERVICES_SETUP not in hass.data: - return False - for key, service in hass.data[ # pylint: disable=unused-variable - MQTT_NOTIFY_SERVICES_SETUP - ].items(): - if service.device_id == device_id: - return True - return False - - -def _check_notify_service_name( - hass: HomeAssistant, config: MqttNotificationConfig -) -> str | None: - """Check if the service already exists or else return the service name.""" - service_name = slugify(config[CONF_NAME]) - has_services = hass.services.has_service(notify.DOMAIN, service_name) - services = hass.data[MQTT_NOTIFY_SERVICES_SETUP] - if service_name in services.keys() or has_services: - _LOGGER.error( - "Notify service '%s' already exists, cannot register service", - service_name, - ) - return None - return service_name - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up MQTT notify service dynamically through MQTT discovery.""" - await async_initialize(hass) - setup = functools.partial(_async_setup_notify, hass, config_entry=config_entry) - await async_setup_entry_helper(hass, notify.DOMAIN, setup, DISCOVERY_SCHEMA) - - -async def _async_setup_notify( - hass, - legacy_config: ConfigType, - config_entry: ConfigEntry, - discovery_data: dict[str, Any], -): - """Set up the MQTT notify service with auto discovery.""" - config: MqttNotificationConfig = DISCOVERY_SCHEMA( - discovery_data[ATTR_DISCOVERY_PAYLOAD] - ) - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - - if not (service_name := _check_notify_service_name(hass, config)): - async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) - clear_discovery_hash(hass, discovery_hash) - return - - device_id = _update_device(hass, config_entry, config) - - service = MqttNotificationService( - hass, - config, - config_entry, - device_id, - discovery_hash, - ) - hass.data[MQTT_NOTIFY_SERVICES_SETUP][service_name] = service - - await service.async_setup(hass, service_name, service_name) - await service.async_register_services() - - -async def async_get_service( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> MqttNotificationService | None: - """Prepare the MQTT notification service through configuration.yaml.""" - await async_initialize(hass) - notification_config: MqttNotificationConfig = cast(MqttNotificationConfig, config) - - if not (service_name := _check_notify_service_name(hass, notification_config)): - return None - - service = hass.data[MQTT_NOTIFY_SERVICES_SETUP][ - service_name - ] = MqttNotificationService( - hass, - notification_config, - ) - return service - - -class MqttNotificationServiceUpdater: - """Add support for auto discovery updates.""" - - def __init__(self, hass: HomeAssistant, service: MqttNotificationService) -> None: - """Initialize the update service.""" - - async def async_discovery_update( - discovery_payload: DiscoveryInfoType | None, - ) -> None: - """Handle discovery update.""" - if not discovery_payload: - # unregister notify service through auto discovery - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None - ) - await async_tear_down_service() - return - - # update notify service through auto discovery - await service.async_update_service(discovery_payload) - _LOGGER.debug( - "Notify service %s updated has been processed", - service.discovery_hash, - ) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None - ) - - async def async_device_removed(event): - """Handle the removal of a device.""" - device_id = event.data["device_id"] - if ( - event.data["action"] != "remove" - or device_id != service.device_id - or self._device_removed - ): - return - self._device_removed = True - await async_tear_down_service() - - async def async_tear_down_service(): - """Handle the removal of the service.""" - services = hass.data[MQTT_NOTIFY_SERVICES_SETUP] - if self._service.service_name in services.keys(): - del services[self._service.service_name] - if not self._device_removed and service.config_entry: - self._device_removed = True - await cleanup_device_registry( - hass, service.device_id, service.config_entry.entry_id - ) - clear_discovery_hash(hass, service.discovery_hash) - self._remove_discovery() - await service.async_unregister_services() - _LOGGER.info( - "Notify service %s has been removed", - service.discovery_hash, - ) - del self._service - - self._service = service - self._remove_discovery = async_dispatcher_connect( - hass, - MQTT_DISCOVERY_UPDATED.format(service.discovery_hash), - async_discovery_update, - ) - if service.device_id: - self._remove_device_updated = hass.bus.async_listen( - EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed - ) - self._device_removed = False - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(service.discovery_hash), None - ) - _LOGGER.info( - "Notify service %s has been initialized", - service.discovery_hash, - ) - - -class MqttNotificationService(notify.BaseNotificationService): - """Implement the notification service for MQTT.""" - - def __init__( - self, - hass: HomeAssistant, - service_config: MqttNotificationConfig, - config_entry: ConfigEntry | None = None, - device_id: str | None = None, - discovery_hash: tuple | None = None, - ) -> None: - """Initialize the service.""" - self.hass = hass - self._config = service_config - self._commmand_template = MqttCommandTemplate( - service_config.get(CONF_COMMAND_TEMPLATE), hass=hass - ) - self._device_id = device_id - self._discovery_hash = discovery_hash - self._config_entry = config_entry - self._service_name = slugify(service_config[CONF_NAME]) - - self._updater = ( - MqttNotificationServiceUpdater(hass, self) if discovery_hash else None - ) - - @property - def device_id(self) -> str | None: - """Return the device ID.""" - return self._device_id - - @property - def config_entry(self) -> ConfigEntry | None: - """Return the config_entry.""" - return self._config_entry - - @property - def discovery_hash(self) -> tuple | None: - """Return the discovery hash.""" - return self._discovery_hash - - @property - def service_name(self) -> str: - """Return the service ma,e.""" - return self._service_name - - async def async_update_service( - self, - discovery_payload: DiscoveryInfoType, - ) -> None: - """Update the notify service through auto discovery.""" - config: MqttNotificationConfig = DISCOVERY_SCHEMA(discovery_payload) - # Do not rename a service if that service_name is already in use - if ( - new_service_name := slugify(config[CONF_NAME]) - ) != self._service_name and _check_notify_service_name( - self.hass, config - ) is None: - return - # Only refresh services if service name or targets have changes - if ( - new_service_name != self._service_name - or config[CONF_TARGETS] != self._config[CONF_TARGETS] - ): - services = self.hass.data[MQTT_NOTIFY_SERVICES_SETUP] - await self.async_unregister_services() - if self._service_name in services: - del services[self._service_name] - self._config = config - self._service_name = new_service_name - await self.async_register_services() - services[new_service_name] = self - else: - self._config = config - self._commmand_template = MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), hass=self.hass - ) - _update_device(self.hass, self._config_entry, config) - - @property - def targets(self) -> dict[str, str]: - """Return a dictionary of registered targets.""" - return {target: target for target in self._config[CONF_TARGETS]} - - async def async_send_message(self, message: str = "", **kwargs): - """Build and send a MQTT message.""" - target = kwargs.get(notify.ATTR_TARGET) - if ( - target is not None - and self._config[CONF_TARGETS] - and set(target) & set(self._config[CONF_TARGETS]) != set(target) - ): - _LOGGER.error( - "Cannot send %s, target list %s is invalid, valid available targets: %s", - message, - target, - self._config[CONF_TARGETS], - ) - return - variables = { - "message": message, - "name": self._config[CONF_NAME], - "service": self._service_name, - "target": target or self._config[CONF_TARGETS], - "title": kwargs.get(notify.ATTR_TITLE, self._config[CONF_TITLE]), - } - variables.update(kwargs.get(notify.ATTR_DATA) or {}) - payload = self._commmand_template.async_render( - message, - variables=variables, - ) - await mqtt.async_publish( - self.hass, - self._config[CONF_COMMAND_TOPIC], - payload, - self._config[CONF_QOS], - self._config[CONF_RETAIN], - self._config[CONF_ENCODING], - ) - - -def _update_device( - hass: HomeAssistant, - config_entry: ConfigEntry | None, - config: MqttNotificationConfig, -) -> str | None: - """Update device registry.""" - if config_entry is None or CONF_DEVICE not in config: - return None - - device = None - device_registry = dr.async_get(hass) - config_entry_id = config_entry.entry_id - device_info = device_info_from_config(config[CONF_DEVICE]) - - if config_entry_id is not None and device_info is not None: - update_device_info = cast(dict, device_info) - update_device_info["config_entry_id"] = config_entry_id - device = device_registry.async_get_or_create(**update_device_info) - - return device.id if device else None diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py deleted file mode 100644 index 33a32d858af..00000000000 --- a/tests/components/mqtt/test_notify.py +++ /dev/null @@ -1,863 +0,0 @@ -"""The tests for the MQTT button platform.""" -import copy -import json -from unittest.mock import patch - -import pytest -import yaml - -from homeassistant import config as hass_config -from homeassistant.components import notify -from homeassistant.components.mqtt import DOMAIN -from homeassistant.const import CONF_NAME, SERVICE_RELOAD -from homeassistant.exceptions import ServiceNotFound -from homeassistant.setup import async_setup_component -from homeassistant.util import slugify - -from tests.common import async_fire_mqtt_message, mock_device_registry - -DEFAULT_CONFIG = {notify.DOMAIN: {"platform": "mqtt", "command_topic": "test-topic"}} - -COMMAND_TEMPLATE_TEST_PARAMS = ( - "name,service,parameters,expected_result", - [ - ( - "My service", - "my_service", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_DATA: {"par1": "val1"}, - }, - '{"message":"Message",' - '"name":"My service",' - '"service":"my_service",' - '"par1":"val1",' - '"target":[' - "'t1', 't2'" - "]," - '"title":"Title"}', - ), - ( - "My service", - "my_service", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_DATA: {"par1": "val1"}, - notify.ATTR_TARGET: ["t2"], - }, - '{"message":"Message",' - '"name":"My service",' - '"service":"my_service",' - '"par1":"val1",' - '"target":[' - "'t2'" - "]," - '"title":"Title"}', - ), - ( - "My service", - "my_service_t1", - { - notify.ATTR_TITLE: "Title2", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_DATA: {"par1": "val2"}, - }, - '{"message":"Message",' - '"name":"My service",' - '"service":"my_service",' - '"par1":"val2",' - '"target":[' - "'t1'" - "]," - '"title":"Title2"}', - ), - ], -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -async def async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data, service_name -): - """Test setup notify service with a device config.""" - caplog.clear() - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/{service_name}/config", data - ) - await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) - assert device_entry is not None - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - - -@pytest.mark.parametrize(*COMMAND_TEMPLATE_TEST_PARAMS) -async def test_sending_with_command_templates_with_config_setup( - hass, mqtt_mock, caplog, name, service, parameters, expected_result -): - """Test the sending MQTT commands using a template using config setup.""" - config = { - "name": name, - "command_topic": "lcd/set", - "command_template": "{" - '"message":"{{message}}",' - '"name":"{{name}}",' - '"service":"{{service}}",' - '"par1":"{{par1}}",' - '"target":{{target}},' - '"title":"{{title}}"' - "}", - "targets": ["t1", "t2"], - "platform": "mqtt", - "qos": "1", - } - service_base_name = slugify(name) - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: config}, - ) - await hass.async_block_till_done() - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - await hass.services.async_call( - notify.DOMAIN, - service, - parameters, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "lcd/set", expected_result, 1, False - ) - mqtt_mock.async_publish.reset_mock() - - -@pytest.mark.parametrize(*COMMAND_TEMPLATE_TEST_PARAMS) -async def test_sending_with_command_templates_auto_discovery( - hass, mqtt_mock, caplog, name, service, parameters, expected_result -): - """Test the sending MQTT commands using a template and auto discovery.""" - config = { - "name": name, - "command_topic": "lcd/set", - "command_template": "{" - '"message":"{{message}}",' - '"name":"{{name}}",' - '"service":"{{service}}",' - '"par1":"{{par1}}",' - '"target":{{target}},' - '"title":"{{title}}"' - "}", - "targets": ["t1", "t2"], - "qos": "1", - } - if name: - config[CONF_NAME] = name - service_base_name = slugify(name) - else: - service_base_name = DOMAIN - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", json.dumps(config) - ) - await hass.async_block_till_done() - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - await hass.services.async_call( - notify.DOMAIN, - service, - parameters, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "lcd/set", expected_result, 1, False - ) - mqtt_mock.async_publish.reset_mock() - - -async def test_sending_mqtt_commands(hass, mqtt_mock, caplog): - """Test the sending MQTT commands.""" - config1 = { - "command_topic": "command-topic1", - "name": "test1", - "platform": "mqtt", - "qos": "2", - } - config2 = { - "command_topic": "command-topic2", - "name": "test2", - "targets": ["t1", "t2"], - "platform": "mqtt", - "qos": "2", - } - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: [config1, config2]}, - ) - await hass.async_block_till_done() - assert "" in caplog.text - assert "" in caplog.text - assert ( - "" in caplog.text - ) - assert ( - "" in caplog.text - ) - - # test1 simple call without targets - await hass.services.async_call( - notify.DOMAIN, - "test1", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic1", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call without targets - await hass.services.async_call( - notify.DOMAIN, - "test2", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service without target - await hass.services.async_call( - notify.DOMAIN, - "test2", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service with empty target - await hass.services.async_call( - notify.DOMAIN, - "test2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: [], - }, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service with single target - await hass.services.async_call( - notify.DOMAIN, - "test2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["t1"], - }, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic2", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - # test2 simple call main service with invalid target - await hass.services.async_call( - notify.DOMAIN, - "test2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["invalid"], - }, - blocking=True, - ) - - assert ( - "Cannot send Message, target list ['invalid'] is invalid, valid available targets: ['t1', 't2']" - in caplog.text - ) - mqtt_mock.async_publish.call_count == 0 - mqtt_mock.async_publish.reset_mock() - - -async def test_with_same_name(hass, mqtt_mock, caplog): - """Test the multiple setups with the same name.""" - config1 = { - "command_topic": "command-topic1", - "name": "test_same_name", - "platform": "mqtt", - "qos": "2", - } - config2 = { - "command_topic": "command-topic2", - "name": "test_same_name", - "targets": ["t1", "t2"], - "platform": "mqtt", - "qos": "2", - } - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: [config1, config2]}, - ) - await hass.async_block_till_done() - assert ( - "" - in caplog.text - ) - assert ( - "Notify service 'test_same_name' already exists, cannot register service" - in caplog.text - ) - - # test call main service on service with multiple targets with the same name - # the first configured service should publish - await hass.services.async_call( - notify.DOMAIN, - "test_same_name", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - }, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "command-topic1", "Message", 2, False - ) - mqtt_mock.async_publish.reset_mock() - - with pytest.raises(ServiceNotFound): - await hass.services.async_call( - notify.DOMAIN, - "test_same_name_t2", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["t2"], - }, - blocking=True, - ) - - -async def test_discovery_without_device(hass, mqtt_mock, caplog): - """Test discovery, update and removal of notify service without device.""" - data = '{ "name": "Old name", "command_topic": "test_topic" }' - data_update = '{ "command_topic": "test_topic_update", "name": "New name" }' - data_update_with_targets1 = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target2"] }' - data_update_with_targets2 = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target3"] }' - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - - assert ( - "" in caplog.text - ) - - await hass.services.async_call( - notify.DOMAIN, - "old_name", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with("test_topic", "Message", 0, False) - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update - ) - await hass.async_block_till_done() - - assert "" in caplog.text - assert ( - "" in caplog.text - ) - assert "Notify service ('notify', 'bla') updated has been processed" in caplog.text - - await hass.services.async_call( - notify.DOMAIN, - "new_name", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - - mqtt_mock.async_publish.assert_called_once_with( - "test_topic_update", "Message", 0, False - ) - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") - await hass.async_block_till_done() - - assert "" in caplog.text - - # rediscover with targets - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update_with_targets1 - ) - await hass.async_block_till_done() - - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - caplog.clear() - - # update available targets - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/bla/config", data_update_with_targets2 - ) - await hass.async_block_till_done() - - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - caplog.clear() - - # test if a new service with same name fails to setup - config1 = { - "command_topic": "command-topic-config.yaml", - "name": "test-setup1", - "platform": "mqtt", - "qos": "2", - } - assert await async_setup_component( - hass, - notify.DOMAIN, - {notify.DOMAIN: [config1]}, - ) - await hass.async_block_till_done() - data = '{ "name": "test-setup1", "command_topic": "test_topic" }' - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/test-setup1/config", data - ) - await hass.async_block_till_done() - assert ( - "Notify service 'test_setup1' already exists, cannot register service" - in caplog.text - ) - await hass.services.async_call( - notify.DOMAIN, - "test_setup1", - { - notify.ATTR_TITLE: "Title", - notify.ATTR_MESSAGE: "Message", - notify.ATTR_TARGET: ["t2"], - }, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "command-topic-config.yaml", "Message", 2, False - ) - - # Test with same discovery on new name - data = '{ "name": "testa", "command_topic": "test_topic_a" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testa/config", data) - await hass.async_block_till_done() - assert "" in caplog.text - - data = '{ "name": "testb", "command_topic": "test_topic_b" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testb/config", data) - await hass.async_block_till_done() - assert "" in caplog.text - - # Try to update from new discovery of existing service test - data = '{ "name": "testa", "command_topic": "test_topic_c" }' - caplog.clear() - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testc/config", data) - await hass.async_block_till_done() - assert ( - "Notify service 'testa' already exists, cannot register service" in caplog.text - ) - - # Try to update the same discovery to existing service test - data = '{ "name": "testa", "command_topic": "test_topic_c" }' - caplog.clear() - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/testb/config", data) - await hass.async_block_till_done() - assert ( - "Notify service 'testa' already exists, cannot register service" in caplog.text - ) - - -async def test_discovery_with_device_update(hass, mqtt_mock, caplog, device_reg): - """Test discovery, update and removal of notify service with a device config.""" - - # Initial setup - data = '{ "command_topic": "test_topic", "name": "My notify service", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' - service_name = "my_notify_service" - await async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data, service_name - ) - assert "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - - -async def test_discovery_with_device_removal(hass, mqtt_mock, caplog, device_reg): - """Test discovery, update and removal of notify service with a device config.""" - - # Initial setup - data1 = '{ "command_topic": "test_topic", "name": "My notify service1", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' - data2 = '{ "command_topic": "test_topic", "name": "My notify service2", "targets": ["target1", "target2"], "device":{"identifiers":["LCD_61236812_ADBA"], "name": "Test123" } }' - service_name1 = "my_notify_service1" - service_name2 = "my_notify_service2" - await async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data1, service_name1 - ) - assert "" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - not in caplog.text - ) - caplog.clear() - - # The device should still be there - device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) - assert device_entry is not None - device_id = device_entry.id - assert device_id == device_entry.id - assert device_entry.name == "Test123" - - # Test removal device from device registry after removing second service - async_fire_mqtt_message( - hass, f"homeassistant/{notify.DOMAIN}/{service_name2}/config", "{}" - ) - await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "LCD_61236812_ADBA")}) - assert device_entry is None - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - caplog.clear() - - # Recreate the service and device - await async_setup_notifify_service_with_auto_discovery( - hass, mqtt_mock, caplog, device_reg, data1, service_name1 - ) - assert "" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - assert ( - f"" - in caplog.text - ) - - -async def test_publishing_with_custom_encoding(hass, mqtt_mock, caplog): - """Test publishing MQTT payload with different encoding via discovery and configuration.""" - # test with default encoding using configuration setup - assert await async_setup_component( - hass, - notify.DOMAIN, - { - notify.DOMAIN: { - "command_topic": "command-topic", - "name": "test", - "platform": "mqtt", - "qos": "2", - } - }, - ) - await hass.async_block_till_done() - - # test with raw encoding and discovery - data = '{"name": "test2", "command_topic": "test_topic2", "command_template": "{{ pack(int(message), \'b\') }}" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been initialized" in caplog.text - assert "" in caplog.text - - await hass.services.async_call( - notify.DOMAIN, - "test2", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "4"}, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with("test_topic2", b"\x04", 0, False) - mqtt_mock.async_publish.reset_mock() - - # test with utf-16 and update discovery - data = '{"encoding":"utf-16", "name": "test3", "command_topic": "test_topic3", "command_template": "{{ message }}" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - assert ( - "Component has already been discovered: notify bla, sending update" - in caplog.text - ) - - await hass.services.async_call( - notify.DOMAIN, - "test3", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "test_topic3", "Message".encode("utf-16"), 0, False - ) - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been removed" in caplog.text - - -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): - """Test reloading the MQTT platform.""" - domain = notify.DOMAIN - config = DEFAULT_CONFIG[domain] - - # Create and test an old config of 2 entities based on the config supplied - old_config_1 = copy.deepcopy(config) - old_config_1["name"] = "Test old 1" - old_config_2 = copy.deepcopy(config) - old_config_2["name"] = "Test old 2" - - assert await async_setup_component( - hass, domain, {domain: [old_config_1, old_config_2]} - ) - await hass.async_block_till_done() - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - caplog.clear() - - # Add an auto discovered notify target - data = '{"name": "Test old 3", "command_topic": "test_topic_discovery" }' - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", data) - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been initialized" in caplog.text - assert ( - "" - in caplog.text - ) - - # Create temporary fixture for configuration.yaml based on the supplied config and test a reload with this new config - new_config_1 = copy.deepcopy(config) - new_config_1["name"] = "Test new 1" - new_config_2 = copy.deepcopy(config) - new_config_2["name"] = "test new 2" - new_config_3 = copy.deepcopy(config) - new_config_3["name"] = "test new 3" - new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({domain: [new_config_1, new_config_2, new_config_3]}) - new_yaml_config_file.write_text(new_yaml_config) - assert new_yaml_config_file.read_text() == new_yaml_config - - with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert ( - "" in caplog.text - ) - assert ( - "" in caplog.text - ) - - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert ( - "" - in caplog.text - ) - assert "" in caplog.text - caplog.clear() - - # test if the auto discovered item survived the platform reload - await hass.services.async_call( - notify.DOMAIN, - "test_old_3", - {notify.ATTR_TITLE: "Title", notify.ATTR_MESSAGE: "Message"}, - blocking=True, - ) - mqtt_mock.async_publish.assert_called_once_with( - "test_topic_discovery", "Message", 0, False - ) - - mqtt_mock.async_publish.reset_mock() - - async_fire_mqtt_message(hass, f"homeassistant/{notify.DOMAIN}/bla/config", "") - await hass.async_block_till_done() - - assert "Notify service ('notify', 'bla') has been removed" in caplog.text From 5ed5bccfe8c02555ff1ed1af0050d1683551709a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 22 Mar 2022 13:04:42 +0100 Subject: [PATCH 0605/1054] Clarify what the Tailscale integration does not (#68499) --- homeassistant/components/tailscale/config_flow.py | 3 +++ homeassistant/components/tailscale/strings.json | 2 +- homeassistant/components/tailscale/translations/en.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py index cda4020a290..f1180db5254 100644 --- a/homeassistant/components/tailscale/config_flow.py +++ b/homeassistant/components/tailscale/config_flow.py @@ -65,6 +65,9 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", + description_placeholders={ + "authkeys_url": "https://login.tailscale.com/admin/settings/authkeys" + }, data_schema=vol.Schema( { vol.Required( diff --git a/homeassistant/components/tailscale/strings.json b/homeassistant/components/tailscale/strings.json index 247d6032c03..d8437bf8169 100644 --- a/homeassistant/components/tailscale/strings.json +++ b/homeassistant/components/tailscale/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "To authenticate with Tailscale you'll need to create an API key at https://login.tailscale.com/admin/settings/authkeys.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo).", + "description": "This integration monitors your Tailscale network, it **DOES NOT** make your Home Assistant accessible via Tailscale VPN. \n\nTo authenticate with Tailscale you'll need to create an API key at {authkeys_url}.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo).", "data": { "tailnet": "Tailnet", "api_key": "[%key:common::config_flow::data::api_key%]" diff --git a/homeassistant/components/tailscale/translations/en.json b/homeassistant/components/tailscale/translations/en.json index f1e79785cbf..dd607f6360d 100644 --- a/homeassistant/components/tailscale/translations/en.json +++ b/homeassistant/components/tailscale/translations/en.json @@ -19,7 +19,7 @@ "api_key": "API Key", "tailnet": "Tailnet" }, - "description": "To authenticate with Tailscale you'll need to create an API key at https://login.tailscale.com/admin/settings/authkeys.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo)." + "description": "This integration monitors your Tailscale network, it **DOES NOT** make your Home Assistant accessible via Tailscale VPN. \n\nTo authenticate with Tailscale you'll need to create an API key at {authkeys_url}.\n\nA Tailnet is the name of your Tailscale network. You can find it in the top left corner in the Tailscale Admin Panel (beside the Tailscale logo)." } } } From dfc689f49a1d1eb9b0def870663251b89fd51e59 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 22 Mar 2022 08:27:54 -0400 Subject: [PATCH 0606/1054] Deprecate Dune HD YAML configuration (#68381) Co-authored-by: Franck Nijhof --- homeassistant/components/dunehd/config_flow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 6c6f12280f5..434bbc7bd84 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -76,6 +76,12 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle configuration by yaml file.""" + _LOGGER.warning( + "Configuration of the Dune HD integration in YAML is deprecated and will be " + "removed in Home Assistant 2022.6; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) assert user_input is not None host: str = user_input[CONF_HOST] From 174f0a5695b1155bc9e184578e1397ac9ff98ae7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 22 Mar 2022 08:37:26 -0400 Subject: [PATCH 0607/1054] Remove deprecated config option for eight_sleep (#68495) --- .../components/eight_sleep/__init__.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 6d99ba59735..3495d1da28b 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -29,8 +29,6 @@ from homeassistant.helpers.update_coordinator import ( _LOGGER = logging.getLogger(__name__) -CONF_PARTNER = "partner" - DATA_EIGHT = "eight_sleep" DATA_HEAT = "heat" DATA_USER = "user" @@ -44,9 +42,6 @@ USER_ENTITY = "user" HEAT_SCAN_INTERVAL = timedelta(seconds=60) USER_SCAN_INTERVAL = timedelta(seconds=300) -SIGNAL_UPDATE_HEAT = "eight_heat_update" -SIGNAL_UPDATE_USER = "eight_user_update" - NAME_MAP = { "left_current_sleep": "Left Sleep Session", "left_current_sleep_fitness": "Left Sleep Fitness", @@ -83,16 +78,12 @@ SERVICE_EIGHT_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.All( - cv.deprecated(CONF_PARTNER), - vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PARTNER): cv.boolean, - } - ), - ) + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ), }, extra=vol.ALLOW_EXTRA, ) From a8df10bb2c5f5cb2ca45b272ae4803b764bc5866 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Mar 2022 14:47:09 +0100 Subject: [PATCH 0608/1054] Bump actions/cache from 2.1.7 to 3 (#68496) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Franck Nijhof --- .github/workflows/ci.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b099f5fb9dd..899f3001125 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: >- @@ -189,7 +189,7 @@ jobs: # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PIP_CACHE }} key: >- @@ -212,7 +212,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -241,7 +241,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -253,7 +253,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -291,7 +291,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -303,7 +303,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -342,7 +342,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -354,7 +354,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -384,7 +384,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -396,7 +396,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -489,7 +489,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -518,7 +518,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -560,7 +560,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: >- @@ -577,7 +577,7 @@ jobs: # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: ${{ env.PIP_CACHE }} key: >- @@ -616,7 +616,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -658,7 +658,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -702,7 +702,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -745,7 +745,7 @@ jobs: uses: actions/checkout@v3.0.0 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v2.1.7 + uses: actions/cache@v3.0.0 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From 1c2b8ee606ec5a645d676c83ca896fd6291006d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 22 Mar 2022 14:48:36 +0100 Subject: [PATCH 0609/1054] Add typing to Alert integration (#68365) --- .strict-typing | 1 + homeassistant/components/alert/__init__.py | 102 ++++++++++++--------- mypy.ini | 11 +++ 3 files changed, 72 insertions(+), 42 deletions(-) diff --git a/.strict-typing b/.strict-typing index cf77894f77f..a7a157c54bf 100644 --- a/.strict-typing +++ b/.strict-typing @@ -28,6 +28,7 @@ homeassistant.util.unit_system # --- Add components below this line --- homeassistant.components +homeassistant.components.alert.* homeassistant.components.abode.* homeassistant.components.acer_projector.* homeassistant.components.accuweather.* diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 252bba54b1c..20f3eaf30ca 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,6 +1,10 @@ """Support for repeating alerts when conditions are met.""" +from __future__ import annotations + +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any, final import voluptuous as vol @@ -23,10 +27,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import event, service +from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.event import ( + async_track_point_in_time, + async_track_state_change_event, +) +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType from homeassistant.util.dt import now @@ -62,7 +71,7 @@ ALERT_SCHEMA = vol.Schema( vol.Optional(CONF_DONE_MESSAGE): cv.template, vol.Optional(CONF_TITLE): cv.template, vol.Optional(CONF_DATA): dict, - vol.Required(CONF_NOTIFIERS): cv.ensure_list, + vol.Required(CONF_NOTIFIERS): vol.All(cv.ensure_list, [cv.string]), } ) @@ -73,14 +82,14 @@ CONFIG_SCHEMA = vol.Schema( ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the alert is firing and not acknowledged.""" return hass.states.is_state(entity_id, STATE_ON) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Alert component.""" - entities = [] + entities: list[Alert] = [] for object_id, cfg in config[DOMAIN].items(): if not cfg: @@ -144,10 +153,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=ALERT_SERVICE_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + DOMAIN, + SERVICE_TURN_ON, + async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA, ) hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA + DOMAIN, + SERVICE_TOGGLE, + async_handle_alert_service, + schema=ALERT_SERVICE_SCHEMA, ) for alert in entities: @@ -163,20 +178,20 @@ class Alert(ToggleEntity): def __init__( self, - hass, - entity_id, - name, - watched_entity_id, - state, - repeat, - skip_first, - message_template, - done_message_template, - notifiers, - can_ack, - title_template, - data, - ): + hass: HomeAssistant, + entity_id: str, + name: str, + watched_entity_id: str, + state: str, + repeat: list[float], + skip_first: bool, + message_template: Template | None, + done_message_template: Template | None, + notifiers: list[str], + can_ack: bool, + title_template: Template | None, + data: dict[Any, Any], + ) -> None: """Initialize the alert.""" self.hass = hass self._attr_name = name @@ -204,16 +219,18 @@ class Alert(ToggleEntity): self._firing = False self._ack = False - self._cancel = None + self._cancel: Callable[[], None] | None = None self._send_done_message = False self.entity_id = f"{DOMAIN}.{entity_id}" - event.async_track_state_change_event( + async_track_state_change_event( hass, [watched_entity_id], self.watched_entity_change ) + @final # type: ignore[misc] @property - def state(self): # pylint: disable=overridden-final-method + # pylint: disable=overridden-final-method + def state(self) -> str: # type: ignore[override] """Return the alert status.""" if self._firing: if self._ack: @@ -221,17 +238,17 @@ class Alert(ToggleEntity): return STATE_ON return STATE_IDLE - async def watched_entity_change(self, ev): + async def watched_entity_change(self, event: Event) -> None: """Determine if the alert should start or stop.""" - if (to_state := ev.data.get("new_state")) is None: + if (to_state := event.data.get("new_state")) is None: return - _LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id")) + _LOGGER.debug("Watched entity (%s) has changed", event.data.get("entity_id")) if to_state.state == self._alert_state and not self._firing: await self.begin_alerting() if to_state.state != self._alert_state and self._firing: await self.end_alerting() - async def begin_alerting(self): + async def begin_alerting(self) -> None: """Begin the alert procedures.""" _LOGGER.debug("Beginning Alert: %s", self._attr_name) self._ack = False @@ -245,26 +262,27 @@ class Alert(ToggleEntity): self.async_write_ha_state() - async def end_alerting(self): + async def end_alerting(self) -> None: """End the alert procedures.""" _LOGGER.debug("Ending Alert: %s", self._attr_name) - self._cancel() + if self._cancel is not None: + self._cancel() + self._cancel = None + self._ack = False self._firing = False if self._send_done_message: await self._notify_done_message() self.async_write_ha_state() - async def _schedule_notify(self): + async def _schedule_notify(self) -> None: """Schedule a notification.""" delay = self._delay[self._next_delay] next_msg = now() + delay - self._cancel = event.async_track_point_in_time( - self.hass, self._notify, next_msg - ) + self._cancel = async_track_point_in_time(self.hass, self._notify, next_msg) self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) - async def _notify(self, *args): + async def _notify(self, *args: Any) -> None: """Send the alert notification.""" if not self._firing: return @@ -281,7 +299,7 @@ class Alert(ToggleEntity): await self._send_notification_message(message) await self._schedule_notify() - async def _notify_done_message(self, *args): + async def _notify_done_message(self) -> None: """Send notification of complete alert.""" _LOGGER.info("Alerting: %s", self._done_message_template) self._send_done_message = False @@ -293,15 +311,15 @@ class Alert(ToggleEntity): await self._send_notification_message(message) - async def _send_notification_message(self, message): + async def _send_notification_message(self, message: Any) -> None: msg_payload = {ATTR_MESSAGE: message} if self._title_template is not None: title = self._title_template.async_render(parse_result=False) - msg_payload.update({ATTR_TITLE: title}) + msg_payload[ATTR_TITLE] = title if self._data: - msg_payload.update({ATTR_DATA: self._data}) + msg_payload[ATTR_DATA] = self._data _LOGGER.debug(msg_payload) @@ -310,19 +328,19 @@ class Alert(ToggleEntity): DOMAIN_NOTIFY, target, msg_payload, context=self._context ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._attr_name) self._ack = False self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._attr_name) self._ack = True self.async_write_ha_state() - async def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs: Any) -> None: """Async toggle alert.""" if self._ack: return await self.async_turn_on() diff --git a/mypy.ini b/mypy.ini index 2f1792438a5..293b8a62c37 100644 --- a/mypy.ini +++ b/mypy.ini @@ -110,6 +110,17 @@ warn_return_any = true warn_unreachable = true no_implicit_reexport = true +[mypy-homeassistant.components.alert.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.abode.*] check_untyped_defs = true disallow_incomplete_defs = true From 0802b64d951852c5c5217094739419f81e0c435e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 22 Mar 2022 14:49:43 +0100 Subject: [PATCH 0610/1054] Add boolean for certificate usage to analytics (#68254) * Add boolean for certificate usage to analytics * Mock hass.http --- .../components/analytics/analytics.py | 2 ++ homeassistant/components/analytics/const.py | 1 + tests/components/analytics/test_analytics.py | 23 ++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index d1b8879bf7c..39c813b2696 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -31,6 +31,7 @@ from .const import ( ATTR_AUTOMATION_COUNT, ATTR_BASE, ATTR_BOARD, + ATTR_CERTIFICATE, ATTR_CONFIGURED, ATTR_CUSTOM_INTEGRATIONS, ATTR_DIAGNOSTICS, @@ -228,6 +229,7 @@ class Analytics: ) if self.preferences.get(ATTR_USAGE, False): + payload[ATTR_CERTIFICATE] = self.hass.http.ssl_certificate is not None payload[ATTR_INTEGRATIONS] = integrations payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations if supervisor_info is not None: diff --git a/homeassistant/components/analytics/const.py b/homeassistant/components/analytics/const.py index 8576e22073f..63fdf820923 100644 --- a/homeassistant/components/analytics/const.py +++ b/homeassistant/components/analytics/const.py @@ -21,6 +21,7 @@ ATTR_AUTO_UPDATE = "auto_update" ATTR_AUTOMATION_COUNT = "automation_count" ATTR_BASE = "base" ATTR_BOARD = "board" +ATTR_CERTIFICATE = "certificate" ATTR_CONFIGURED = "configured" ATTR_CUSTOM_INTEGRATIONS = "custom_integrations" ATTR_DIAGNOSTICS = "diagnostics" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 2534c4678e8..f36fbdd9d79 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -173,6 +173,7 @@ async def test_send_usage(hass, caplog, aioclient_mock): """Test send usage preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] @@ -184,13 +185,14 @@ async def test_send_usage(hass, caplog, aioclient_mock): assert "'integrations': ['default_config']" in caplog.text assert "'integration_count':" not in caplog.text + assert "'certificate': False" in caplog.text async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): """Test send usage with supervisor preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) - analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_USAGE] @@ -365,6 +367,7 @@ async def test_custom_integrations(hass, aioclient_mock, enable_custom_integrati """Test sending custom integrations.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) assert await async_setup_component(hass, "test_package", {"test_package": {}}) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) @@ -430,6 +433,7 @@ async def test_send_with_no_energy(hass, aioclient_mock): """Test send base preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) + hass.http = Mock(ssl_certificate=None) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) @@ -493,3 +497,20 @@ async def test_send_with_energy_config(hass, aioclient_mock): postdata = aioclient_mock.mock_calls[-1][2] assert postdata["energy"]["configured"] + + +async def test_send_usage_with_certificate(hass, caplog, aioclient_mock): + """Test send usage preferences with certificate.""" + aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) + analytics = Analytics(hass) + hass.http = Mock(ssl_certificate="/some/path/to/cert.pem") + await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) + + assert analytics.preferences[ATTR_BASE] + assert analytics.preferences[ATTR_USAGE] + hass.config.components = ["default_config"] + + with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): + await analytics.send_analytics() + + assert "'certificate': True" in caplog.text From 94cd656670ecff6b0dde4ee42782a6df55bb80d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 03:58:16 -1000 Subject: [PATCH 0611/1054] Use new internal_state property in tplink diagnostics (#68497) --- homeassistant/components/tplink/diagnostics.py | 11 +++-------- tests/components/tplink/test_diagnostics.py | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py index 5771bee5bd3..5121def2e47 100644 --- a/homeassistant/components/tplink/diagnostics.py +++ b/homeassistant/components/tplink/diagnostics.py @@ -36,11 +36,6 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - device = coordinator.device - - data = {} - data[ - "device_last_response" - ] = device._last_update # pylint: disable=protected-access - - return async_redact_data(data, TO_REDACT) + return async_redact_data( + {"device_last_response": coordinator.device.internal_state}, TO_REDACT + ) diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py index d09ea72b70a..55dd1cee9c7 100644 --- a/tests/components/tplink/test_diagnostics.py +++ b/tests/components/tplink/test_diagnostics.py @@ -38,7 +38,7 @@ async def test_diagnostics( """Test diagnostics for config entry.""" diagnostics_data = json.loads(load_fixture(fixture_file, "tplink")) - mocked_dev._last_update = diagnostics_data["device_last_response"] + mocked_dev.internal_state = diagnostics_data["device_last_response"] config_entry = await initialize_config_entry_for_device(hass, mocked_dev) result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) From bdc92271f233b1a69cfa469f36046360294e86fd Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 22 Mar 2022 15:13:09 +0100 Subject: [PATCH 0612/1054] Add zha typing [core.gateway] (1) (#68397) * Add zha typing [core.gateway] (1) * Add temporary type ignores * Fix pylint --- homeassistant/components/zha/__init__.py | 4 +- homeassistant/components/zha/core/gateway.py | 124 +++++++++++-------- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 1d5656a1b8d..4f1e80e0a7b 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -109,8 +109,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_registry = await hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee))}, - identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, + connections={(CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee))}, # type: ignore[attr-defined] + identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, # type: ignore[attr-defined] name="Zigbee Coordinator", manufacturer="ZHA", model=zha_gateway.radio_description, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 252893683ef..636e161d45c 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import collections +from collections.abc import Callable from datetime import timedelta from enum import Enum import itertools @@ -10,13 +11,18 @@ import logging import os import time import traceback +from typing import TYPE_CHECKING, Any, Union from serial import SerialException from zigpy.config import CONF_DEVICE -import zigpy.device as zigpy_dev +import zigpy.device +import zigpy.endpoint +import zigpy.group +from zigpy.types.named import EUI64 from homeassistant.components.system_log import LogEntry, _figure_out_source -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import ( CONNECTION_ZIGBEE, @@ -28,8 +34,9 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as get_ent_reg, ) from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType -from . import discovery, typing as zha_typing +from . import discovery from .const import ( ATTR_IEEE, ATTR_MANUFACTURER, @@ -81,7 +88,13 @@ from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS from .store import async_get_registry -from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType + +if TYPE_CHECKING: + from logging import Filter, LogRecord + + from ..entity import ZhaEntity + + _LogFilterType = Union[Filter, Callable[[LogRecord], int]] _LOGGER = logging.getLogger(__name__) @@ -103,29 +116,33 @@ class DevicePairingStatus(Enum): class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" - def __init__(self, hass, config, config_entry): + def __init__( + self, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry + ) -> None: """Initialize the gateway.""" self._hass = hass self._config = config - self._devices = {} - self._groups = {} - self.coordinator_zha_device = None - self._device_registry = collections.defaultdict(list) + self._devices: dict[EUI64, ZHADevice] = {} + self._groups: dict[int, ZHAGroup] = {} + self.coordinator_zha_device: ZHADevice | None = None + self._device_registry: collections.defaultdict[ + EUI64, list[EntityReference] + ] = collections.defaultdict(list) self.zha_storage = None self.ha_device_registry = None self.ha_entity_registry = None self.application_controller = None self.radio_description = None - self._log_levels = { + self._log_levels: dict[str, dict[str, int]] = { DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), DEBUG_LEVEL_CURRENT: async_capture_log_levels(), } self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry - self._unsubs = [] + self._unsubs: list[Callable[[], None]] = [] - async def async_initialize(self): + async def async_initialize(self) -> None: """Initialize controller and connect radio.""" discovery.PROBE.initialize(self._hass) discovery.GROUP_PROBE.initialize(self._hass) @@ -211,7 +228,7 @@ class ZHAGateway: """Initialize devices and load entities.""" semaphore = asyncio.Semaphore(2) - async def _throttle(zha_device: zha_typing.ZhaDeviceType, cached: bool): + async def _throttle(zha_device: ZHADevice, cached: bool) -> None: async with semaphore: await zha_device.async_initialize(from_cache=cached) @@ -233,7 +250,7 @@ class ZHAGateway: ) ) - def device_joined(self, device): + def device_joined(self, device: zigpy.device.Device) -> None: """Handle device joined. At this point, no information about the device is known other than its @@ -252,7 +269,7 @@ class ZHAGateway: }, ) - def raw_device_initialized(self, device): + def raw_device_initialized(self, device: zigpy.device.Device) -> None: """Handle a device initialization without quirks loaded.""" manuf = device.manufacturer async_dispatcher_send( @@ -271,16 +288,16 @@ class ZHAGateway: }, ) - def device_initialized(self, device): + def device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered.""" self._hass.async_create_task(self.async_device_initialized(device)) - def device_left(self, device: zigpy_dev.Device): + def device_left(self, device: zigpy.device.Device) -> None: """Handle device leaving the network.""" self.async_update_device(device, False) def group_member_removed( - self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType + self, zigpy_group: zigpy.group.Group, endpoint: zigpy.endpoint.Endpoint ) -> None: """Handle zigpy group member removed event.""" # need to handle endpoint correctly on groups @@ -292,7 +309,7 @@ class ZHAGateway: ) def group_member_added( - self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType + self, zigpy_group: zigpy.group.Group, endpoint: zigpy.endpoint.Endpoint ) -> None: """Handle zigpy group member added event.""" # need to handle endpoint correctly on groups @@ -306,14 +323,14 @@ class ZHAGateway: # we need to do this because there wasn't already a group entity to remove and re-add discovery.GROUP_PROBE.discover_group_entities(zha_group) - def group_added(self, zigpy_group: ZigpyGroupType) -> None: + def group_added(self, zigpy_group: zigpy.group.Group) -> None: """Handle zigpy group added event.""" zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_added") # need to dispatch for entity creation here self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_ADDED) - def group_removed(self, zigpy_group: ZigpyGroupType) -> None: + def group_removed(self, zigpy_group: zigpy.group.Group) -> None: """Handle zigpy group removed event.""" self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_REMOVED) zha_group = self._groups.pop(zigpy_group.group_id, None) @@ -321,7 +338,7 @@ class ZHAGateway: self._cleanup_group_entity_registry_entries(zigpy_group) def _send_group_gateway_message( - self, zigpy_group: ZigpyGroupType, gateway_message_type: str + self, zigpy_group: zigpy.group.Group, gateway_message_type: str ) -> None: """Send the gateway event for a zigpy group event.""" zha_group = self._groups.get(zigpy_group.group_id) @@ -335,7 +352,9 @@ class ZHAGateway: }, ) - async def _async_remove_device(self, device, entity_refs): + async def _async_remove_device( + self, device: ZHADevice, entity_refs: list[EntityReference] | None + ) -> None: if entity_refs is not None: remove_tasks = [] for entity_ref in entity_refs: @@ -346,7 +365,7 @@ class ZHAGateway: if reg_device is not None: self.ha_device_registry.async_remove_device(reg_device.id) - def device_removed(self, device): + def device_removed(self, device: zigpy.device.Device) -> None: """Handle device being removed from the network.""" zha_device = self._devices.pop(device.ieee, None) entity_refs = self._device_registry.pop(device.ieee, None) @@ -365,23 +384,23 @@ class ZHAGateway: }, ) - def get_device(self, ieee): + def get_device(self, ieee: EUI64) -> ZHADevice | None: """Return ZHADevice for given ieee.""" return self._devices.get(ieee) - def get_group(self, group_id: str) -> ZhaGroupType | None: + def get_group(self, group_id: int) -> ZHAGroup | None: """Return Group for given group id.""" return self.groups.get(group_id) @callback - def async_get_group_by_name(self, group_name: str) -> ZhaGroupType | None: + def async_get_group_by_name(self, group_name: str) -> ZHAGroup | None: """Get ZHA group by name.""" for group in self.groups.values(): if group.name == group_name: return group return None - def get_entity_reference(self, entity_id): + def get_entity_reference(self, entity_id: str) -> EntityReference | None: """Return entity reference for given entity_id if found.""" for entity_reference in itertools.chain.from_iterable( self.device_registry.values() @@ -389,7 +408,7 @@ class ZHAGateway: if entity_id == entity_reference.reference_id: return entity_reference - def remove_entity_reference(self, entity): + def remove_entity_reference(self, entity: ZhaEntity) -> None: """Remove entity reference for given entity_id if found.""" if entity.zha_device.ieee in self.device_registry: entity_refs = self.device_registry.get(entity.zha_device.ieee) @@ -398,7 +417,7 @@ class ZHAGateway: ] def _cleanup_group_entity_registry_entries( - self, zigpy_group: ZigpyGroupType + self, zigpy_group: zigpy.group.Group ) -> None: """Remove entity registry entries for group entities when the groups are removed from HA.""" # first we collect the potential unique ids for entities that could be created from this group @@ -429,17 +448,17 @@ class ZHAGateway: self.ha_entity_registry.async_remove(entry.entity_id) @property - def devices(self): + def devices(self) -> dict[EUI64, ZHADevice]: """Return devices.""" return self._devices @property - def groups(self): + def groups(self) -> dict[int, ZHAGroup]: """Return groups.""" return self._groups @property - def device_registry(self): + def device_registry(self) -> collections.defaultdict[EUI64, list[EntityReference]]: """Return entities by ieee.""" return self._device_registry @@ -464,7 +483,7 @@ class ZHAGateway: ) @callback - def async_enable_debug_mode(self, filterer=None): + def async_enable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: """Enable debug mode for ZHA.""" self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() async_set_logger_levels(DEBUG_LEVELS) @@ -479,7 +498,7 @@ class ZHAGateway: self.debug_enabled = True @callback - def async_disable_debug_mode(self, filterer=None): + def async_disable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: """Disable debug mode for ZHA.""" async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() @@ -491,8 +510,8 @@ class ZHAGateway: @callback def _async_get_or_create_device( - self, zigpy_device: zha_typing.ZigpyDeviceType, restored: bool = False - ): + self, zigpy_device: zigpy.device.Device, restored: bool = False + ) -> ZHADevice: """Get or create a ZHA device.""" if (zha_device := self._devices.get(zigpy_device.ieee)) is None: zha_device = ZHADevice.new(self._hass, zigpy_device, self, restored) @@ -511,7 +530,7 @@ class ZHAGateway: return zha_device @callback - def _async_get_or_create_group(self, zigpy_group: ZigpyGroupType) -> ZhaGroupType: + def _async_get_or_create_group(self, zigpy_group: zigpy.group.Group) -> ZHAGroup: """Get or create a ZHA group.""" zha_group = self._groups.get(zigpy_group.group_id) if zha_group is None: @@ -521,7 +540,7 @@ class ZHAGateway: @callback def async_update_device( - self, sender: zigpy_dev.Device, available: bool = True + self, sender: zigpy.device.Device, available: bool = True ) -> None: """Update device that has just become available.""" if sender.ieee in self.devices: @@ -530,12 +549,12 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self, *_): + async def async_update_device_storage(self, *_: Any) -> None: """Update the devices in the store.""" for device in self.devices.values(): self.zha_storage.async_update_device(device) - async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): + async def async_device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) # This is an active device so set a last seen if it is none @@ -576,7 +595,7 @@ class ZHAGateway: }, ) - async def _async_device_joined(self, zha_device: zha_typing.ZhaDeviceType) -> None: + async def _async_device_joined(self, zha_device: ZHADevice) -> None: zha_device.available = True device_info = zha_device.device_info await zha_device.async_configure() @@ -592,7 +611,7 @@ class ZHAGateway: await zha_device.async_initialize(from_cache=False) async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) - async def _async_device_rejoined(self, zha_device): + async def _async_device_rejoined(self, zha_device: ZHADevice) -> None: _LOGGER.debug( "skipping discovery for previously discovered device - %s:%s", zha_device.nwk, @@ -615,8 +634,11 @@ class ZHAGateway: zha_device.update_available(True) async def async_create_zigpy_group( - self, name: str, members: list[GroupMember], group_id: int = None - ) -> ZhaGroupType: + self, + name: str, + members: list[GroupMember] | None, + group_id: int | None = None, + ) -> ZHAGroup | None: """Create a new Zigpy Zigbee group.""" # we start with two to fill any gaps from a user removing existing groups @@ -659,7 +681,7 @@ class ZHAGateway: await asyncio.gather(*tasks) self.application_controller.groups.pop(group_id) - async def shutdown(self): + async def shutdown(self) -> None: """Stop ZHA Controller Application.""" _LOGGER.debug("Shutting down ZHA ControllerApplication") for unsubscribe in self._unsubs: @@ -668,7 +690,7 @@ class ZHAGateway: def handle_message( self, - sender: zigpy_dev.Device, + sender: zigpy.device.Device, profile: int, cluster: int, src_ep: int, @@ -681,7 +703,7 @@ class ZHAGateway: @callback -def async_capture_log_levels(): +def async_capture_log_levels() -> dict[str, int]: """Capture current logger levels for ZHA.""" return { DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), @@ -703,7 +725,7 @@ def async_capture_log_levels(): @callback -def async_set_logger_levels(levels): +def async_set_logger_levels(levels: dict[str, int]) -> None: """Set logger levels for ZHA.""" logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) @@ -717,13 +739,13 @@ def async_set_logger_levels(levels): class LogRelayHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, gateway): + def __init__(self, hass: HomeAssistant, gateway: ZHAGateway) -> None: """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.gateway = gateway - def emit(self, record): + def emit(self, record: LogRecord) -> None: """Relay log message via dispatcher.""" stack = [] if record.levelno >= logging.WARN and not record.exc_info: From df05e8b950cf5389fb206778ede01f885c4b7077 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 22 Mar 2022 15:14:35 +0100 Subject: [PATCH 0613/1054] Add zha typing [core.channels] (#68377) --- .../components/zha/core/channels/__init__.py | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index d60c38c69a6..b63c20e14eb 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,12 +2,14 @@ from __future__ import annotations import asyncio -from typing import Any +from collections.abc import Coroutine +from typing import TYPE_CHECKING, Any, TypeVar +import zigpy.endpoint import zigpy.zcl.clusters.closures from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from . import ( # noqa: F401 @@ -29,20 +31,25 @@ from .. import ( device as zha_core_device, discovery as zha_disc, registries as zha_regs, - typing as zha_typing, ) -ChannelsDict = dict[str, zha_typing.ChannelType] +if TYPE_CHECKING: + from ...entity import ZhaEntity + from ..device import ZHADevice + +_ChannelsT = TypeVar("_ChannelsT", bound="Channels") +_ChannelPoolT = TypeVar("_ChannelPoolT", bound="ChannelPool") +_ChannelsDictType = dict[str, base.ZigbeeChannel] class Channels: """All discovered channels of a device.""" - def __init__(self, zha_device: zha_typing.ZhaDeviceType) -> None: + def __init__(self, zha_device: ZHADevice) -> None: """Initialize instance.""" - self._pools: list[zha_typing.ChannelPoolType] = [] - self._power_config = None - self._identify = None + self._pools: list[ChannelPool] = [] + self._power_config: base.ZigbeeChannel | None = None + self._identify: base.ZigbeeChannel | None = None self._semaphore = asyncio.Semaphore(3) self._unique_id = str(zha_device.ieee) self._zdo_channel = base.ZDOChannel(zha_device.device.endpoints[0], zha_device) @@ -54,23 +61,23 @@ class Channels: return self._pools @property - def power_configuration_ch(self) -> zha_typing.ChannelType: + def power_configuration_ch(self) -> base.ZigbeeChannel | None: """Return power configuration channel.""" return self._power_config @power_configuration_ch.setter - def power_configuration_ch(self, channel: zha_typing.ChannelType) -> None: + def power_configuration_ch(self, channel: base.ZigbeeChannel) -> None: """Power configuration channel setter.""" if self._power_config is None: self._power_config = channel @property - def identify_ch(self) -> zha_typing.ChannelType: + def identify_ch(self) -> base.ZigbeeChannel | None: """Return power configuration channel.""" return self._identify @identify_ch.setter - def identify_ch(self, channel: zha_typing.ChannelType) -> None: + def identify_ch(self, channel: base.ZigbeeChannel) -> None: """Power configuration channel setter.""" if self._identify is None: self._identify = channel @@ -81,17 +88,17 @@ class Channels: return self._semaphore @property - def zdo_channel(self) -> zha_typing.ZDOChannelType: + def zdo_channel(self) -> base.ZDOChannel: """Return ZDO channel.""" return self._zdo_channel @property - def zha_device(self) -> zha_typing.ZhaDeviceType: + def zha_device(self) -> ZHADevice: """Return parent zha device.""" return self._zha_device @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id for this channel.""" return self._unique_id @@ -104,7 +111,7 @@ class Channels: } @classmethod - def new(cls, zha_device: zha_typing.ZhaDeviceType) -> Channels: + def new(cls: type[_ChannelsT], zha_device: ZHADevice) -> _ChannelsT: """Create new instance.""" channels = cls(zha_device) for ep_id in sorted(zha_device.device.endpoints): @@ -142,9 +149,9 @@ class Channels: def async_new_entity( self, component: str, - entity_class: zha_typing.CALLABLE_T, + entity_class: type[ZhaEntity], unique_id: str, - channels: list[zha_typing.ChannelType], + channels: list[base.ZigbeeChannel], ): """Signal new entity addition.""" if self.zha_device.status == zha_core_device.DeviceStatus.INITIALIZED: @@ -178,30 +185,30 @@ class ChannelPool: def __init__(self, channels: Channels, ep_id: int) -> None: """Initialize instance.""" - self._all_channels: ChannelsDict = {} - self._channels: Channels = channels - self._claimed_channels: ChannelsDict = {} - self._id: int = ep_id - self._client_channels: dict[str, zha_typing.ClientChannelType] = {} - self._unique_id: str = f"{channels.unique_id}-{ep_id}" + self._all_channels: _ChannelsDictType = {} + self._channels = channels + self._claimed_channels: _ChannelsDictType = {} + self._id = ep_id + self._client_channels: dict[str, base.ClientChannel] = {} + self._unique_id = f"{channels.unique_id}-{ep_id}" @property - def all_channels(self) -> ChannelsDict: + def all_channels(self) -> _ChannelsDictType: """All server channels of an endpoint.""" return self._all_channels @property - def claimed_channels(self) -> ChannelsDict: + def claimed_channels(self) -> _ChannelsDictType: """Channels in use.""" return self._claimed_channels @property - def client_channels(self) -> dict[str, zha_typing.ClientChannelType]: + def client_channels(self) -> dict[str, base.ClientChannel]: """Return a dict of client channels.""" return self._client_channels @property - def endpoint(self) -> zha_typing.ZigpyEndpointType: + def endpoint(self) -> zigpy.endpoint.Endpoint: """Return endpoint of zigpy device.""" return self._channels.zha_device.device.endpoints[self.id] @@ -216,7 +223,7 @@ class ChannelPool: return self._channels.zha_device.nwk @property - def is_mains_powered(self) -> bool: + def is_mains_powered(self) -> bool | None: """Device is_mains_powered.""" return self._channels.zha_device.is_mains_powered @@ -231,7 +238,7 @@ class ChannelPool: return self._channels.zha_device.manufacturer_code @property - def hass(self): + def hass(self) -> HomeAssistant: """Return hass.""" return self._channels.zha_device.hass @@ -246,7 +253,7 @@ class ChannelPool: return self._channels.zha_device.skip_configuration @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id for this channel.""" return self._unique_id @@ -272,7 +279,7 @@ class ChannelPool: ) @classmethod - def new(cls, channels: Channels, ep_id: int) -> ChannelPool: + def new(cls: type[_ChannelPoolT], channels: Channels, ep_id: int) -> _ChannelPoolT: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() @@ -330,7 +337,7 @@ class ChannelPool: async def _execute_channel_tasks(self, func_name: str, *args: Any) -> None: """Add a throttled channel task and swallow exceptions.""" - async def _throttle(coro): + async def _throttle(coro: Coroutine[Any, Any, None]) -> None: async with self._channels.semaphore: return await coro @@ -347,9 +354,9 @@ class ChannelPool: def async_new_entity( self, component: str, - entity_class: zha_typing.CALLABLE_T, + entity_class: type[ZhaEntity], unique_id: str, - channels: list[zha_typing.ChannelType], + channels: list[base.ZigbeeChannel], ): """Signal new entity addition.""" self._channels.async_new_entity(component, entity_class, unique_id, channels) @@ -360,12 +367,12 @@ class ChannelPool: self._channels.async_send_signal(signal, *args) @callback - def claim_channels(self, channels: list[zha_typing.ChannelType]) -> None: + def claim_channels(self, channels: list[base.ZigbeeChannel]) -> None: """Claim a channel.""" self.claimed_channels.update({ch.id: ch for ch in channels}) @callback - def unclaimed_channels(self) -> list[zha_typing.ChannelType]: + def unclaimed_channels(self) -> list[base.ZigbeeChannel]: """Return a list of available (unclaimed) channels.""" claimed = set(self.claimed_channels) available = set(self.all_channels) From 6a66b4dbffea0c14ad760b224ad8fb0d55f6b8db Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 22 Mar 2022 15:15:39 +0100 Subject: [PATCH 0614/1054] Add zha typing [api] (3) (#68353) --- homeassistant/components/zha/api.py | 60 +++++++++++++++-------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ac028597ea8..060e8a5d5eb 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -105,13 +105,15 @@ SERVICE_ZIGBEE_BIND = "service_zigbee_bind" IEEE_SERVICE = "ieee_based_service" SERVICE_PERMIT_PARAMS = { - vol.Optional(ATTR_IEEE, default=None): EUI64.convert, + vol.Optional(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), - vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): EUI64.convert, - vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): convert_install_code, - vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(str, qr_to_install_code), + vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): vol.All(cv.string, EUI64.convert), + vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): vol.All( + cv.string, convert_install_code + ), + vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(cv.string, qr_to_install_code), } SERVICE_SCHEMAS = { @@ -124,12 +126,12 @@ SERVICE_SCHEMAS = { IEEE_SERVICE: vol.Schema( vol.All( cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE), - {vol.Required(ATTR_IEEE): EUI64.convert}, + {vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert)}, ) ), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -140,7 +142,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED ): cv.positive_int, @@ -154,7 +156,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_WARN: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY ): cv.positive_int, @@ -175,7 +177,7 @@ SERVICE_SCHEMAS = { ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -320,7 +322,7 @@ async def websocket_get_groups( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/device", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), } ) @websocket_api.async_response @@ -501,7 +503,7 @@ async def websocket_remove_group_members( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), } ) @websocket_api.async_response @@ -551,7 +553,7 @@ async def websocket_update_topology( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), } ) @websocket_api.async_response @@ -592,7 +594,7 @@ async def websocket_device_clusters( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -639,7 +641,7 @@ async def websocket_device_cluster_attributes( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -699,7 +701,7 @@ async def websocket_device_cluster_commands( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -754,7 +756,7 @@ async def websocket_read_zigbee_cluster_attributes( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bindable", - vol.Required(ATTR_IEEE): EUI64.convert, + vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), } ) @websocket_api.async_response @@ -787,8 +789,8 @@ async def websocket_get_bindable_devices( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, - vol.Required(ATTR_TARGET_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_TARGET_IEEE): vol.All(cv.string, EUI64.convert), } ) @websocket_api.async_response @@ -815,8 +817,8 @@ async def websocket_bind_devices( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, - vol.Required(ATTR_TARGET_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_TARGET_IEEE): vol.All(cv.string, EUI64.convert), } ) @websocket_api.async_response @@ -860,7 +862,7 @@ def is_cluster_binding(value: Any) -> ClusterBinding: @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/bind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(GROUP_ID): cv.positive_int, vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), } @@ -882,7 +884,7 @@ async def websocket_bind_group( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/unbind", - vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), vol.Required(GROUP_ID): cv.positive_int, vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), } @@ -1257,13 +1259,13 @@ def async_load_api(hass: HomeAssistant) -> None: async def warning_device_warn(service: ServiceCall) -> None: """Issue the warning command for an IAS warning device.""" - ieee = service.data[ATTR_IEEE] - mode = service.data.get(ATTR_WARNING_DEVICE_MODE) - strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) - level = service.data.get(ATTR_LEVEL) - duration = service.data.get(ATTR_WARNING_DEVICE_DURATION) - duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE) - intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY) + ieee: EUI64 = service.data[ATTR_IEEE] + mode: int = service.data[ATTR_WARNING_DEVICE_MODE] + strobe: int = service.data[ATTR_WARNING_DEVICE_STROBE] + level: int = service.data[ATTR_LEVEL] + duration: int = service.data[ATTR_WARNING_DEVICE_DURATION] + duty_mode: int = service.data[ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE] + intensity: int = service.data[ATTR_WARNING_DEVICE_STROBE_INTENSITY] if (zha_device := zha_gateway.get_device(ieee)) is not None: if channel := _get_ias_wd_channel(zha_device): From 0720b0f8911671cb0111debb95f8e912c002fc0a Mon Sep 17 00:00:00 2001 From: Jeff Rescignano Date: Tue, 22 Mar 2022 11:09:18 -0400 Subject: [PATCH 0615/1054] Add all option to light group (#68447) Co-authored-by: Franck Nijhof Co-authored-by: Erik Montnemery --- homeassistant/components/group/config_flow.py | 14 +++- homeassistant/components/group/light.py | 36 ++++++++-- homeassistant/components/group/strings.json | 3 + .../components/group/translations/en.json | 43 ++---------- tests/components/group/test_config_flow.py | 5 +- tests/components/group/test_init.py | 4 +- tests/components/group/test_light.py | 67 +++++++++++++++++++ 7 files changed, 124 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 38c08692fe4..dafb43924a7 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -46,10 +46,20 @@ BINARY_SENSOR_OPTIONS_SCHEMA = basic_group_options_schema("binary_sensor").exten } ) +LIGHT_OPTIONS_SCHEMA = basic_group_options_schema("light").extend( + { + vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}), + } +) + BINARY_SENSOR_CONFIG_SCHEMA = vol.Schema( {vol.Required("name"): selector.selector({"text": {}})} ).extend(BINARY_SENSOR_OPTIONS_SCHEMA.schema) +LIGHT_CONFIG_SCHEMA = vol.Schema( + {vol.Required("name"): selector.selector({"text": {}})} +).extend(LIGHT_OPTIONS_SCHEMA.schema) + INITIAL_STEP_SCHEMA = vol.Schema( { @@ -81,7 +91,7 @@ CONFIG_FLOW = { "binary_sensor": HelperFlowStep(BINARY_SENSOR_CONFIG_SCHEMA), "cover": HelperFlowStep(basic_group_config_schema("cover")), "fan": HelperFlowStep(basic_group_config_schema("fan")), - "light": HelperFlowStep(basic_group_config_schema("light")), + "light": HelperFlowStep(LIGHT_CONFIG_SCHEMA), "media_player": HelperFlowStep(basic_group_config_schema("media_player")), } @@ -91,7 +101,7 @@ OPTIONS_FLOW = { "binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA), "cover": HelperFlowStep(basic_group_options_schema("cover")), "fan": HelperFlowStep(basic_group_options_schema("fan")), - "light": HelperFlowStep(basic_group_options_schema("light")), + "light": HelperFlowStep(LIGHT_OPTIONS_SCHEMA), "media_player": HelperFlowStep(basic_group_options_schema("media_player")), } diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 12f316497e6..e5c87e91889 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -47,6 +47,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er @@ -58,6 +59,7 @@ from . import GroupEntity from .util import find_state_attributes, mean_tuple, reduce_attribute DEFAULT_NAME = "Light Group" +CONF_ALL = "all" # No limit on parallel updates to enable a group calling another group PARALLEL_UPDATES = 0 @@ -67,6 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN), + vol.Optional(CONF_ALL): cv.boolean, } ) @@ -87,7 +90,10 @@ async def async_setup_platform( async_add_entities( [ LightGroup( - config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES] + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + config.get(CONF_ALL), ) ] ) @@ -103,9 +109,10 @@ async def async_setup_entry( entities = er.async_validate_entity_ids( registry, config_entry.options[CONF_ENTITIES] ) + mode = config_entry.options[CONF_ALL] async_add_entities( - [LightGroup(config_entry.entry_id, config_entry.title, entities)] + [LightGroup(config_entry.entry_id, config_entry.title, entities, mode)] ) @@ -132,12 +139,13 @@ class LightGroup(GroupEntity, LightEntity): _attr_available = False _attr_icon = "mdi:lightbulb-group" - _attr_is_on = False _attr_max_mireds = 500 _attr_min_mireds = 154 _attr_should_poll = False - def __init__(self, unique_id: str | None, name: str, entity_ids: list[str]) -> None: + def __init__( + self, unique_id: str | None, name: str, entity_ids: list[str], mode: str | None + ) -> None: """Initialize a light group.""" self._entity_ids = entity_ids self._white_value: int | None = None @@ -145,6 +153,9 @@ class LightGroup(GroupEntity, LightEntity): self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} self._attr_unique_id = unique_id + self.mode = any + if mode: + self.mode = all async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -207,7 +218,22 @@ class LightGroup(GroupEntity, LightEntity): states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._attr_is_on = len(on_states) > 0 + # filtered_states are members currently in the state machine + filtered_states: list[str] = [x.state for x in all_states if x is not None] + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + ) + + if not valid_state: + # Set as unknown if any / all member is unknown or unavailable + self._attr_is_on = None + else: + # Set as ON if any / all member is ON + self._attr_is_on = self.mode( + list(map(lambda x: x == STATE_ON, filtered_states)) + ) + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) self._attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index e0b1eb1ab23..fb343f9006a 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -32,7 +32,9 @@ } }, "light": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "title": "[%key:component::group::config::step::user::title%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" @@ -67,6 +69,7 @@ }, "light_options": { "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" } }, diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index b2b5750795e..a8c9e09dd0b 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -15,62 +15,30 @@ "entities": "Members", "name": "Name", "title": "New Group" - }, - "description": "Select group options" - }, - "cover_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" + } }, "fan": { "data": { "entities": "Members", "name": "Name", "title": "New Group" - }, - "description": "Select group options" - }, - "fan_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" - }, - "init": { - "data": { - "group_type": "Group type" - }, - "description": "Select group type" + } }, "light": { "data": { + "all": "All entities", "entities": "Members", "name": "Name", "title": "New Group" }, - "description": "Select group options" - }, - "light_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, "media_player": { "data": { "entities": "Members", "name": "Name", "title": "New Group" - }, - "description": "Select group options" - }, - "media_player_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" + } }, "user": { "data": { @@ -100,6 +68,7 @@ }, "light_options": { "data": { + "all": "All entities", "entities": "Members" } }, diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 16fa2ad6933..f5a9fb22222 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -19,7 +19,8 @@ from tests.common import MockConfigEntry ("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}), ("cover", "open", "open", {}, {}, {}, {}), ("fan", "on", "on", {}, {}, {}, {}), - ("light", "on", "on", {}, {}, {}, {}), + ("light", "on", "on", {}, {}, {"all": False}, {}), + ("light", "on", "on", {}, {"all": True}, {"all": True}, {}), ("media_player", "on", "on", {}, {}, {}, {}), ), ) @@ -174,7 +175,7 @@ def get_suggested(schema, key): ("binary_sensor", "on", {"all": False}), ("cover", "open", {}), ("fan", "on", {}), - ("light", "on", {}), + ("light", "on", {"all": False}), ("media_player", "on", {}), ), ) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index ba91b9dbbbc..56553ff263c 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1370,7 +1370,7 @@ async def test_plant_group(hass): ("binary_sensor", "on", {"all": False}), ("cover", "open", {}), ("fan", "on", {}), - ("light", "on", {}), + ("light", "on", {"all": False}), ("media_player", "on", {}), ), ) @@ -1435,7 +1435,7 @@ async def test_setup_and_remove_config_entry( ("binary_sensor", {"all": False}), ("cover", {}), ("fan", {}), - ("light", {}), + ("light", {"all": False}), ("media_player", {}), ), ) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index d356b20b40f..0f125dd3e88 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -49,6 +49,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -68,6 +69,7 @@ async def test_default_state(hass): "entities": ["light.kitchen", "light.bedroom"], "name": "Bedroom Group", "unique_id": "unique_identifier", + "all": "false", } }, ) @@ -102,6 +104,7 @@ async def test_state_reporting(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -130,6 +133,49 @@ async def test_state_reporting(hass): assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE +async def test_state_reporting_all(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + "all": "true", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_UNAVAILABLE) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + + async def test_brightness(hass, enable_custom_integrations): """Test brightness reporting.""" platform = getattr(hass.components, "test.light") @@ -155,6 +201,7 @@ async def test_brightness(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -225,6 +272,7 @@ async def test_color_hs(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -296,6 +344,7 @@ async def test_color_rgb(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -367,6 +416,7 @@ async def test_color_rgbw(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -438,6 +488,7 @@ async def test_color_rgbww(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -489,6 +540,7 @@ async def test_white_value(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -548,6 +600,7 @@ async def test_white(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -603,6 +656,7 @@ async def test_color_temp(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -674,6 +728,7 @@ async def test_emulated_color_temp_group(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", }, ] }, @@ -739,6 +794,7 @@ async def test_min_max_mireds(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", }, ] }, @@ -784,6 +840,7 @@ async def test_effect_list(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -843,6 +900,7 @@ async def test_effect(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", } }, ) @@ -909,6 +967,7 @@ async def test_supported_color_modes(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", }, ] }, @@ -957,6 +1016,7 @@ async def test_color_mode(hass, enable_custom_integrations): { "platform": DOMAIN, "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", }, ] }, @@ -1051,6 +1111,7 @@ async def test_color_mode2(hass, enable_custom_integrations): "light.test5", "light.test6", ], + "all": "false", }, ] }, @@ -1082,6 +1143,7 @@ async def test_supported_features(hass): LIGHT_DOMAIN: { "platform": DOMAIN, "entities": ["light.test1", "light.test2"], + "all": "false", } }, ) @@ -1157,6 +1219,7 @@ async def test_service_calls(hass, enable_custom_integrations, supported_color_m "light.ceiling_lights", "light.kitchen_lights", ], + "all": "false", }, ] }, @@ -1269,6 +1332,7 @@ async def test_service_call_effect(hass): "light.ceiling_lights", "light.kitchen_lights", ], + "all": "false", }, ] }, @@ -1369,6 +1433,7 @@ async def test_reload(hass): "light.ceiling_lights", "light.kitchen_lights", ], + "all": "false", }, ] }, @@ -1481,11 +1546,13 @@ async def test_nested_group(hass): "platform": DOMAIN, "entities": ["light.bedroom_group"], "name": "Nested Group", + "all": "false", }, { "platform": DOMAIN, "entities": ["light.bed_light", "light.kitchen_lights"], "name": "Bedroom Group", + "all": "false", }, ] }, From 0c6a6c360b03c37a0d6aee0190aa9cc66a321af3 Mon Sep 17 00:00:00 2001 From: Warwick Davison <34164294+Waz-Cpt@users.noreply.github.com> Date: Tue, 22 Mar 2022 19:24:57 +0200 Subject: [PATCH 0616/1054] Fix tuya light 2 channel dimmer module (#68109) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 4c35c850c33..c4f874d0687 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -111,7 +111,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { # Not documented # Based on multiple reports: manufacturer customized Dimmer 2 switches TuyaLightEntityDescription( - key=DPCode.SWITCH_LED_1, + key=DPCode.SWITCH_1, name="Light", brightness=DPCode.BRIGHT_VALUE_1, ), From 43772b3fa9db00d146292854ee3b52392a29dd37 Mon Sep 17 00:00:00 2001 From: Ben Felton <44007102+itjedi42@users.noreply.github.com> Date: Tue, 22 Mar 2022 12:26:23 -0500 Subject: [PATCH 0617/1054] Add World Message/MOTD support for MinecraftServer Integration (#66297) --- .../components/minecraft_server/__init__.py | 3 +++ .../components/minecraft_server/const.py | 3 +++ .../components/minecraft_server/sensor.py | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index d8f42454498..b45cc0fb2e3 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -95,6 +95,7 @@ class MinecraftServer: self.players_online = None self.players_max = None self.players_list = None + self.motd = None # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" @@ -179,6 +180,7 @@ class MinecraftServer: self.players_online = status_response.players.online self.players_max = status_response.players.max self.latency_time = status_response.latency + self.motd = (status_response.description).get("text") self.players_list = [] if status_response.players.sample is not None: for player in status_response.players.sample: @@ -201,6 +203,7 @@ class MinecraftServer: self.players_max = None self.latency_time = None self.players_list = None + self.motd = None # Inform user once about failed update if necessary. if not self._last_status_request_failed: diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py index 52e6ae8fd5e..ab5d67dc426 100644 --- a/homeassistant/components/minecraft_server/const.py +++ b/homeassistant/components/minecraft_server/const.py @@ -14,6 +14,7 @@ ICON_PLAYERS_ONLINE = "mdi:account-multiple" ICON_PROTOCOL_VERSION = "mdi:numeric" ICON_STATUS = "mdi:lan" ICON_VERSION = "mdi:numeric" +ICON_MOTD = "mdi:minecraft" KEY_SERVERS = "servers" @@ -25,6 +26,7 @@ NAME_PLAYERS_ONLINE = "Players Online" NAME_PROTOCOL_VERSION = "Protocol Version" NAME_STATUS = "Status" NAME_VERSION = "Version" +NAME_MOTD = "World Message" SCAN_INTERVAL = 60 @@ -36,3 +38,4 @@ UNIT_PLAYERS_MAX = "players" UNIT_PLAYERS_ONLINE = "players" UNIT_PROTOCOL_VERSION = None UNIT_VERSION = None +UNIT_MOTD = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index a59ef3db363..d7ca73d1411 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -14,15 +14,18 @@ from .const import ( ATTR_PLAYERS_LIST, DOMAIN, ICON_LATENCY_TIME, + ICON_MOTD, ICON_PLAYERS_MAX, ICON_PLAYERS_ONLINE, ICON_PROTOCOL_VERSION, ICON_VERSION, NAME_LATENCY_TIME, + NAME_MOTD, NAME_PLAYERS_MAX, NAME_PLAYERS_ONLINE, NAME_PROTOCOL_VERSION, NAME_VERSION, + UNIT_MOTD, UNIT_PLAYERS_MAX, UNIT_PLAYERS_ONLINE, UNIT_PROTOCOL_VERSION, @@ -45,6 +48,7 @@ async def async_setup_entry( MinecraftServerLatencyTimeSensor(server), MinecraftServerPlayersOnlineSensor(server), MinecraftServerPlayersMaxSensor(server), + MinecraftServerMOTDSensor(server), ] # Add sensor entities. @@ -176,3 +180,20 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" self._state = self._server.players_max + + +class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server MOTD sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize MOTD sensor.""" + super().__init__( + server=server, + type_name=NAME_MOTD, + icon=ICON_MOTD, + unit=UNIT_MOTD, + ) + + async def async_update(self) -> None: + """Update MOTD.""" + self._state = self._server.motd From eb068bc8508d88a63c8022924e27136810df08cd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Mar 2022 18:45:27 +0100 Subject: [PATCH 0618/1054] Fix targeting all or none entities in service calls (#68513) * Fix targeting all or none entities in service calls * Add test --- homeassistant/helpers/service.py | 9 ++++++--- tests/helpers/test_service.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 966666b27fc..9d446f10913 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -218,9 +218,12 @@ def async_prepare_call_from_config( if CONF_ENTITY_ID in target: registry = entity_registry.async_get(hass) - target[CONF_ENTITY_ID] = entity_registry.async_validate_entity_ids( - registry, cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) - ) + entity_ids = cv.comp_entity_ids_or_uuids(target[CONF_ENTITY_ID]) + if entity_ids not in (ENTITY_MATCH_ALL, ENTITY_MATCH_NONE): + entity_ids = entity_registry.async_validate_entity_ids( + registry, entity_ids + ) + target[CONF_ENTITY_ID] = entity_ids except TemplateError as ex: raise HomeAssistantError( f"Error rendering service target template: {ex}" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index ce4a32bb2a4..7bde035d1f8 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -421,6 +421,22 @@ async def test_service_call_entry_id(hass): assert dict(calls[0].data) == {"entity_id": ["hello.world"]} +@pytest.mark.parametrize("target", ("all", "none")) +async def test_service_call_all_none(hass, target): + """Test service call targeting all.""" + calls = async_mock_service(hass, "test_domain", "test_service") + + config = { + "service": "test_domain.test_service", + "target": {"entity_id": target}, + } + + await service.async_call_from_config(hass, config) + await hass.async_block_till_done() + + assert dict(calls[0].data) == {"entity_id": target} + + async def test_extract_entity_ids(hass): """Test extract_entity_ids method.""" hass.states.async_set("light.Bowl", STATE_ON) From c5f3f9e924bbe2c312f14e25f380c2c8946233c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 08:34:46 -1000 Subject: [PATCH 0619/1054] Convert plant to use history api for database access (#68410) --- homeassistant/components/plant/__init__.py | 47 ++++++++-------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index d9d207e215b..75d412f7f5f 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -6,9 +6,7 @@ import logging import voluptuous as vol -from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import StateAttributes, States -from homeassistant.components.recorder.util import execute, session_scope +from homeassistant.components.recorder import get_instance, history from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, @@ -29,6 +27,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -298,7 +297,7 @@ class Plant(Entity): This only needs to be done once during startup. """ - start_date = datetime.now() - timedelta(days=self._conf_check_days) + start_date = dt_util.utcnow() - timedelta(days=self._conf_check_days) entity_id = self._readingmap.get(READING_BRIGHTNESS) if entity_id is None: _LOGGER.debug( @@ -306,36 +305,22 @@ class Plant(Entity): "there is no brightness sensor configured" ) return - _LOGGER.debug("Initializing values for %s from the database", self._name) - with session_scope(hass=self.hass) as session: - query = ( - session.query(States, StateAttributes) - .filter( - (States.entity_id == entity_id.lower()) - and (States.last_updated > start_date) + lower_entity_id = entity_id.lower() + history_list = history.state_changes_during_period( + self.hass, + start_date, + entity_id=lower_entity_id, + no_attributes=True, + ) + for state in history_list.get(lower_entity_id, []): + # filter out all None, NaN and "unknown" states + # only keep real values + with suppress(ValueError): + self._brightness_history.add_measurement( + int(state.state), state.last_updated ) - .outerjoin( - StateAttributes, - States.attributes_id == StateAttributes.attributes_id, - ) - .order_by(States.last_updated.asc()) - ) - states = [] - if results := execute(query, to_native=False, validate_entity_ids=False): - for state, attributes in results: - native = state.to_native() - if not native.attributes: - native.attributes = attributes.to_native() - states.append(native) - for state in states: - # filter out all None, NaN and "unknown" states - # only keep real values - with suppress(ValueError): - self._brightness_history.add_measurement( - int(state.state), state.last_updated - ) _LOGGER.debug("Initializing from database completed") @property From c2233970987eec63e3e7616c016c110578bcc7d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 08:49:07 -1000 Subject: [PATCH 0620/1054] Remove unneeded attributes selection from history_states api calls (#68409) --- homeassistant/components/history_stats/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b33b7ca4db9..5177d5f5239 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -232,14 +232,16 @@ class HistoryStatsSensor(SensorEntity): def _update(self, start, end, now_timestamp, start_timestamp, end_timestamp): # Get history between start and end history_list = history.state_changes_during_period( - self.hass, start, end, str(self._entity_id) + self.hass, start, end, str(self._entity_id), no_attributes=True ) if self._entity_id not in history_list: return # Get the first state - last_state = history.get_state(self.hass, start, self._entity_id) + last_state = history.get_state( + self.hass, start, self._entity_id, no_attributes=True + ) last_state = last_state is not None and last_state in self._entity_states last_time = start_timestamp elapsed = 0 From 34063836ba569055213d446b6409e232062a9244 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Mar 2022 19:51:24 +0100 Subject: [PATCH 0621/1054] Fix scaffold script (#68516) --- script/scaffold/__main__.py | 2 +- script/scaffold/model.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 05ef300d33e..2d4454c254b 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -61,7 +61,7 @@ def main(): if not args.template.startswith("config_flow"): if info.helper: template = "config_flow_helper" - if info.oauth2: + elif info.oauth2: template = "config_flow_oauth2" elif info.authentication or not info.discoverable: template = "config_flow" diff --git a/script/scaffold/model.py b/script/scaffold/model.py index a73165de770..2b1ee71fc63 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -22,6 +22,7 @@ class Info: authentication: str = attr.ib(default=None) discoverable: str = attr.ib(default=None) oauth2: str = attr.ib(default=None) + helper: str = attr.ib(default=None) files_added: set[Path] = attr.ib(factory=set) tests_added: set[Path] = attr.ib(factory=set) From 78a41022ade7f070db6ed5c7678a0ba1171b72e6 Mon Sep 17 00:00:00 2001 From: Inovelli <37669481+InovelliUSA@users.noreply.github.com> Date: Tue, 22 Mar 2022 14:59:57 -0400 Subject: [PATCH 0622/1054] Updating to allow for Button Press event logs for Inovelli devices (#68277) Co-authored-by: codyhackw <49957005+codyhackw@users.noreply.github.com> --- .../components/zha/core/channels/manufacturerspecific.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 8b840e76317..a69442ef38c 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -84,3 +84,11 @@ class SmartThingsAcceleration(ZigbeeChannel): ATTR_VALUE: value, }, ) + + +@registries.CHANNEL_ONLY_CLUSTERS.register(0xFC31) +@registries.CLIENT_CHANNELS_REGISTRY.register(0xFC31) +class InovelliCluster(ZigbeeChannel): + """Inovelli Button Press Event channel.""" + + REPORT_CONFIG = [] From b9526b05eead82c74c8ae1d29f7216cb1367630c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 11:05:30 -1000 Subject: [PATCH 0623/1054] Disable extra emonitor sensors by default (#68519) --- homeassistant/components/emonitor/sensor.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 39a8c90f283..27117d88fe4 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -26,8 +26,12 @@ from .const import DOMAIN SENSORS = ( SensorEntityDescription(key="inst_power"), - SensorEntityDescription(key="avg_power", name="Average"), - SensorEntityDescription(key="max_power", name="Max"), + SensorEntityDescription( + key="avg_power", name="Average", entity_registry_enabled_default=False + ), + SensorEntityDescription( + key="max_power", name="Max", entity_registry_enabled_default=False + ), ) @@ -74,16 +78,18 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): self.channel_number = channel_number super().__init__(coordinator) mac_address = self.emonitor_status.network.mac_address + device_name = name_short_mac(mac_address[-6:]) + label = self.channel_data.label or f"{device_name} {channel_number}" if description.name: - self._attr_name = f"{self.channel_data.label} {description.name}" + self._attr_name = f"{label} {description.name}" self._attr_unique_id = f"{mac_address}_{channel_number}_{description.key}" else: - self._attr_name = self.channel_data.label + self._attr_name = label self._attr_unique_id = f"{mac_address}_{channel_number}" self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_NETWORK_MAC, mac_address)}, manufacturer="Powerhouse Dynamics, Inc.", - name=name_short_mac(mac_address[-6:]), + name=device_name, sw_version=self.emonitor_status.hardware.firmware_version, ) From c5a3ba40657c6fa1ef2751dc777af26898c9803f Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Tue, 22 Mar 2022 18:14:01 -0400 Subject: [PATCH 0624/1054] Add support for general API exception in Sense integration (#68517) --- .../components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/config_flow.py | 6 ++--- homeassistant/components/sense/const.py | 12 ++++++++-- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sense/test_config_flow.py | 22 ++++++++++++++++++- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index c11fecb3ff1..c7f9175bc5e 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.10.2"], + "requirements": ["sense_energy==0.10.3"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index eea36424662..8769d4cb83f 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_TIMEOUT_EXCEPTIONS +from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS _LOGGER = logging.getLogger(__name__) @@ -76,7 +76,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.validate_input(user_input) except SenseMFARequiredException: return await self.async_step_validation() - except SENSE_TIMEOUT_EXCEPTIONS: + except SENSE_CONNECT_EXCEPTIONS: errors["base"] = "cannot_connect" except SenseAuthenticationException: errors["base"] = "invalid_auth" @@ -93,7 +93,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input: try: await self._gateway.validate_mfa(user_input[CONF_CODE]) - except SENSE_TIMEOUT_EXCEPTIONS: + except SENSE_CONNECT_EXCEPTIONS: errors["base"] = "cannot_connect" except SenseAuthenticationException: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index bb323151950..622e9897a66 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -3,8 +3,11 @@ import asyncio import socket -from sense_energy import SenseAPITimeoutException -from sense_energy.sense_exceptions import SenseWebsocketException +from sense_energy import ( + SenseAPIException, + SenseAPITimeoutException, + SenseWebsocketException, +) DOMAIN = "sense" DEFAULT_TIMEOUT = 10 @@ -40,6 +43,11 @@ ICON = "mdi:flash" SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) SENSE_EXCEPTIONS = (socket.gaierror, SenseWebsocketException) +SENSE_CONNECT_EXCEPTIONS = ( + asyncio.TimeoutError, + SenseAPITimeoutException, + SenseAPIException, +) MDI_ICONS = { "ac": "air-conditioner", diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 30de722a7bc..04b0b451db4 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.10.2"], + "requirements": ["sense_energy==0.10.3"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0cb4a718af6..33f282fba67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,7 +2118,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.2 +sense_energy==0.10.3 # homeassistant.components.sentry sentry-sdk==1.5.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e905d2a194..4dd9485f930 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ securetar==2022.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.2 +sense_energy==0.10.3 # homeassistant.components.sentry sentry-sdk==1.5.8 diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index f939142aee4..690e5d2e530 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, patch import pytest from sense_energy import ( + SenseAPIException, SenseAPITimeoutException, SenseAuthenticationException, SenseMFARequiredException, @@ -189,7 +190,7 @@ async def test_form_mfa_required_exception(hass, mock_sense): assert result3["errors"] == {"base": "unknown"} -async def test_form_cannot_connect(hass): +async def test_form_timeout(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -208,6 +209,25 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "sense_energy.ASyncSenseable.authenticate", + side_effect=SenseAPIException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"timeout": "6", "email": "test-email", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + async def test_form_unknown_exception(hass): """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( From b5c5da96acbb8d16f9a0974cf45c92e5da516a81 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Mar 2022 23:18:30 +0100 Subject: [PATCH 0625/1054] Add WS API to adjust incorrect energy statistics (#65147) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/__init__.py | 30 ++++ .../components/recorder/statistics.py | 69 +++++++++- .../components/recorder/websocket_api.py | 30 ++++ tests/components/recorder/test_statistics.py | 62 ++++++++- tests/components/sensor/test_recorder.py | 130 +++++++++++++----- 5 files changed, 280 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index c188dd6c4b6..aae6576c225 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -462,6 +462,31 @@ class ExternalStatisticsTask(RecorderTask): instance.queue.put(ExternalStatisticsTask(self.metadata, self.statistics)) +@dataclass +class AdjustStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an adjust statistics task.""" + + statistic_id: str + start_time: datetime + sum_adjustment: float + + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.adjust_statistics( + instance, + self.statistic_id, + self.start_time, + self.sum_adjustment, + ): + return + # Schedule a new adjust statistics task if this one didn't finish + instance.queue.put( + AdjustStatisticsTask( + self.statistic_id, self.start_time, self.sum_adjustment + ) + ) + + @dataclass class WaitTask(RecorderTask): """An object to insert into the recorder queue to tell it set the _queue_watch event.""" @@ -761,6 +786,11 @@ class Recorder(threading.Thread): start = statistics.get_start_time() self.queue.put(StatisticsTask(start)) + @callback + def async_adjust_statistics(self, statistic_id, start_time, sum_adjustment): + """Adjust statistics.""" + self.queue.put(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) + @callback def async_clear_statistics(self, statistic_ids): """Clear statistics for a list of statistic_ids.""" diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index df53bd55307..b27a08f489c 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -19,6 +19,7 @@ from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.sql.expression import literal_column, true +import voluptuous as vol from homeassistant.const import ( PRESSURE_PA, @@ -163,6 +164,14 @@ def valid_statistic_id(statistic_id: str) -> bool: return VALID_STATISTIC_ID.match(statistic_id) is not None +def validate_statistic_id(value: str) -> str: + """Validate statistic ID.""" + if valid_statistic_id(value): + return value + + raise vol.Invalid(f"Statistics ID {value} is an invalid statistic ID") + + @dataclasses.dataclass class ValidationIssue: """Error or warning message.""" @@ -567,6 +576,30 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: return True +def _adjust_sum_statistics( + session: scoped_session, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + start_time: datetime, + adj: float, +) -> None: + """Adjust statistics in the database.""" + try: + session.query(table).filter_by(metadata_id=metadata_id).filter( + table.start >= start_time + ).update( + { + table.sum: table.sum + adj, + }, + synchronize_session=False, + ) + except SQLAlchemyError: + _LOGGER.exception( + "Unexpected exception when updating statistics %s", + id, + ) + + def _insert_statistics( session: scoped_session, table: type[Statistics | StatisticsShortTerm], @@ -606,7 +639,7 @@ def _update_statistics( except SQLAlchemyError: _LOGGER.exception( "Unexpected exception when updating statistics %s:%s ", - id, + stat_id, statistic, ) @@ -1249,7 +1282,7 @@ def add_external_statistics( metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> bool: - """Process an add_statistics job.""" + """Process an add_external_statistics job.""" with session_scope( session=instance.get_session(), # type: ignore[misc] @@ -1265,3 +1298,35 @@ def add_external_statistics( _insert_statistics(session, Statistics, metadata_id, stat) return True + + +@retryable_database_job("adjust_statistics") +def adjust_statistics( + instance: Recorder, + statistic_id: str, + start_time: datetime, + sum_adjustment: float, +) -> bool: + """Process an add_statistics job.""" + + with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + metadata = get_metadata_with_session( + instance.hass, session, statistic_ids=(statistic_id,) + ) + if statistic_id not in metadata: + return True + + tables: tuple[type[Statistics | StatisticsShortTerm], ...] = ( + Statistics, + StatisticsShortTerm, + ) + for table in tables: + _adjust_sum_statistics( + session, + table, + metadata[statistic_id][0], + start_time, + sum_adjustment, + ) + + return True diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index a480439eaac..241dca9026c 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.util import dt as dt_util from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG from .statistics import list_statistic_ids, validate_statistics @@ -29,6 +30,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_info) websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_backup_end) + websocket_api.async_register_command(hass, ws_adjust_sum_statistics) @websocket_api.websocket_command( @@ -105,6 +107,34 @@ def ws_update_statistics_metadata( connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/adjust_sum_statistics", + vol.Required("statistic_id"): str, + vol.Required("start_time"): str, + vol.Required("adjustment"): vol.Any(float, int), + } +) +@callback +def ws_adjust_sum_statistics( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Adjust sum statistics.""" + start_time_str = msg["start_time"] + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") + return + + hass.data[DATA_INSTANCE].async_adjust_statistics( + msg["statistic_id"], start_time, msg["adjustment"] + ) + connection.send_result(msg["id"]) + + @websocket_api.websocket_command( { vol.Required("type"): "recorder/info", diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index fe05dbc25ab..29853c2cc0e 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -34,7 +34,13 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, mock_registry +from .common import async_wait_recording_done_without_instance + +from tests.common import ( + async_init_recorder_component, + get_test_home_assistant, + mock_registry, +) from tests.components.recorder.common import wait_recording_done ORIG_TZ = dt_util.DEFAULT_TIME_ZONE @@ -327,10 +333,11 @@ def test_statistics_duplicated(hass_recorder, caplog): caplog.clear() -def test_external_statistics(hass_recorder, caplog): +async def test_external_statistics(hass, hass_ws_client, caplog): """Test inserting external statistics.""" - hass = hass_recorder() - wait_recording_done(hass) + client = await hass_ws_client() + await async_init_recorder_component(hass) + assert "Compiling statistics for" not in caplog.text assert "Statistics already compiled" not in caplog.text @@ -363,7 +370,7 @@ def test_external_statistics(hass_recorder, caplog): async_add_external_statistics( hass, external_metadata, (external_statistics1, external_statistics2) ) - wait_recording_done(hass) + await async_wait_recording_done_without_instance(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { "test:total_energy_import": [ @@ -439,7 +446,7 @@ def test_external_statistics(hass_recorder, caplog): "sum": 6, } async_add_external_statistics(hass, external_metadata, (external_statistics,)) - wait_recording_done(hass) + await async_wait_recording_done_without_instance(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { "test:total_energy_import": [ @@ -479,7 +486,7 @@ def test_external_statistics(hass_recorder, caplog): "sum": 5, } async_add_external_statistics(hass, external_metadata, (external_statistics,)) - wait_recording_done(hass) + await async_wait_recording_done_without_instance(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { "test:total_energy_import": [ @@ -508,6 +515,47 @@ def test_external_statistics(hass_recorder, caplog): ] } + await client.send_json( + { + "id": 1, + "type": "recorder/adjust_sum_statistics", + "statistic_id": "test:total_energy_import", + "start_time": period2.isoformat(), + "adjustment": 1000.0, + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done_without_instance(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), + "last_reset": None, + "state": approx(4.0), + "sum": approx(5.0), + }, + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(1003.0), + }, + ] + } + def test_external_statistics_errors(hass_recorder, caplog): """Test validation of external statistics.""" diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index af1494b381e..f26b5e7ead3 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,6 +1,7 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta +from functools import partial import math from statistics import mean from unittest.mock import patch @@ -26,8 +27,15 @@ from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import async_setup_component, init_recorder_component -from tests.components.recorder.common import wait_recording_done +from tests.common import ( + async_init_recorder_component, + async_setup_component, + init_recorder_component, +) +from tests.components.recorder.common import ( + async_wait_recording_done_without_instance, + wait_recording_done, +) BATTERY_SENSOR_ATTRIBUTES = { "device_class": "battery", @@ -307,34 +315,44 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( - "units,device_class,unit,display_unit,factor", + "units,device_class,unit,display_unit,factor,factor2", [ - (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1), - (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), - (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1), - (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1), - (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711), - (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1), - (METRIC_SYSTEM, "energy", "kWh", "kWh", 1), - (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000), - (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1), - (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1), - (METRIC_SYSTEM, "gas", "m³", "m³", 1), - (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466), + (IMPERIAL_SYSTEM, "energy", "kWh", "kWh", 1, 1), + (IMPERIAL_SYSTEM, "energy", "Wh", "kWh", 1 / 1000, 1), + (IMPERIAL_SYSTEM, "monetary", "EUR", "EUR", 1, 1), + (IMPERIAL_SYSTEM, "monetary", "SEK", "SEK", 1, 1), + (IMPERIAL_SYSTEM, "gas", "m³", "ft³", 35.314666711, 35.314666711), + (IMPERIAL_SYSTEM, "gas", "ft³", "ft³", 1, 35.314666711), + (METRIC_SYSTEM, "energy", "kWh", "kWh", 1, 1), + (METRIC_SYSTEM, "energy", "Wh", "kWh", 1 / 1000, 1), + (METRIC_SYSTEM, "monetary", "EUR", "EUR", 1, 1), + (METRIC_SYSTEM, "monetary", "SEK", "SEK", 1, 1), + (METRIC_SYSTEM, "gas", "m³", "m³", 1, 1), + (METRIC_SYSTEM, "gas", "ft³", "m³", 0.0283168466, 1), ], ) -def test_compile_hourly_sum_statistics_amount( - hass_recorder, caplog, units, state_class, device_class, unit, display_unit, factor +async def test_compile_hourly_sum_statistics_amount( + hass, + hass_ws_client, + caplog, + units, + state_class, + device_class, + unit, + display_unit, + factor, + factor2, ): """Test compiling hourly statistics.""" period0 = dt_util.utcnow() period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) - hass = hass_recorder() + client = await hass_ws_client() + await async_init_recorder_component(hass) hass.config.units = units recorder = hass.data[DATA_INSTANCE] - setup_component(hass, "sensor", {}) + await async_setup_component(hass, "sensor", {}) attributes = { "device_class": device_class, "state_class": state_class, @@ -343,21 +361,28 @@ def test_compile_hourly_sum_statistics_amount( } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] - four, eight, states = record_meter_states( - hass, period0, "sensor.test1", attributes, seq + four, eight, states = await hass.async_add_executor_job( + record_meter_states, hass, period0, "sensor.test1", attributes, seq ) + await async_wait_recording_done_without_instance(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, eight + timedelta.resolution ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=period0) - wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period1) - wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period2) - wait_recording_done(hass) - statistic_ids = list_statistic_ids(hass) + await hass.async_add_executor_job( + partial(recorder.do_adhoc_statistics, start=period0) + ) + await async_wait_recording_done_without_instance(hass) + await hass.async_add_executor_job( + partial(recorder.do_adhoc_statistics, start=period1) + ) + await async_wait_recording_done_without_instance(hass) + await hass.async_add_executor_job( + partial(recorder.do_adhoc_statistics, start=period2) + ) + await async_wait_recording_done_without_instance(hass) + statistic_ids = await hass.async_add_executor_job(list_statistic_ids, hass) assert statistic_ids == [ { "statistic_id": "sensor.test1", @@ -416,20 +441,57 @@ def test_compile_hourly_sum_statistics_amount( stats = statistics_during_period( hass, period0 + timedelta(minutes=5), period="5minute" ) - expected_stats["sensor.test1"] = expected_stats["sensor.test1"][1:3] - assert stats == expected_stats + assert stats == {"sensor.test1": expected_stats["sensor.test1"][1:3]} # With an offset of 6 minutes, we expect to get the 2nd and 3rd periods stats = statistics_during_period( hass, period0 + timedelta(minutes=6), period="5minute" ) - assert stats == expected_stats + assert stats == {"sensor.test1": expected_stats["sensor.test1"][1:3]} assert "Error while processing event StatisticsTask" not in caplog.text assert "Detected new cycle for sensor.test1, last_reset set to" in caplog.text assert "Compiling initial sum statistics for sensor.test1" in caplog.text assert "Detected new cycle for sensor.test1, value dropped" not in caplog.text + # Adjust the inserted statistics + await client.send_json( + { + "id": 1, + "type": "recorder/adjust_sum_statistics", + "statistic_id": "sensor.test1", + "start_time": period1.isoformat(), + "adjustment": 100.0, + } + ) + response = await client.receive_json() + assert response["success"] + await async_wait_recording_done_without_instance(hass) + + expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + factor2 * 100) + expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 + factor2 * 100) + stats = statistics_during_period(hass, period0, period="5minute") + assert stats == expected_stats + + # Adjust the inserted statistics + await client.send_json( + { + "id": 2, + "type": "recorder/adjust_sum_statistics", + "statistic_id": "sensor.test1", + "start_time": period2.isoformat(), + "adjustment": -400.0, + } + ) + response = await client.receive_json() + assert response["success"] + await async_wait_recording_done_without_instance(hass) + + expected_stats["sensor.test1"][1]["sum"] = approx(factor * 40.0 + factor2 * 100) + expected_stats["sensor.test1"][2]["sum"] = approx(factor * 70.0 - factor2 * 300) + stats = statistics_during_period(hass, period0, period="5minute") + assert stats == expected_stats + @pytest.mark.parametrize("state_class", ["total"]) @pytest.mark.parametrize( @@ -836,6 +898,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( four, eight, states = record_meter_states( hass, period0, "sensor.test1", attributes, seq ) + wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, eight + timedelta.resolution ) @@ -927,6 +990,7 @@ def test_compile_hourly_sum_statistics_total_increasing( four, eight, states = record_meter_states( hass, period0, "sensor.test1", attributes, seq ) + wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, eight + timedelta.resolution ) @@ -1016,6 +1080,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( four, eight, states = record_meter_states( hass, period0, "sensor.test1", attributes, seq ) + wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, eight + timedelta.resolution ) @@ -1118,6 +1183,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): states = {**states, **_states} _, _, _states = record_meter_states(hass, period0, "sensor.test3", sns3_attr, seq3) states = {**states, **_states} + wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, eight + timedelta.resolution @@ -1207,6 +1273,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): states = {**states, **_states} _, _, _states = record_meter_states(hass, period0, "sensor.test3", sns3_attr, seq3) states = {**states, **_states} + wait_recording_done(hass) hist = history.get_significant_states( hass, period0 - timedelta.resolution, eight + timedelta.resolution ) @@ -3164,7 +3231,6 @@ def record_meter_states(hass, zero, entity_id, _attributes, seq): def set_state(entity_id, state, **kwargs): """Set the state.""" hass.states.set(entity_id, state, **kwargs) - wait_recording_done(hass) return hass.states.get(entity_id) one = zero + timedelta(seconds=15 * 5) # 00:01:15 From b2d7fe15bb3fb91e81ff40074539b581ff50fb18 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 22 Mar 2022 19:48:21 -0500 Subject: [PATCH 0626/1054] Bump Frontend to 20220322.0 (#68535) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c6b1eebaae0..152f141da87 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20220317.0" + "home-assistant-frontend==20220322.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d8d88133149..c1acde9fcbd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==35.0.0 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220317.0 +home-assistant-frontend==20220322.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 33f282fba67..18d4c0d9979 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -809,7 +809,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220317.0 +home-assistant-frontend==20220322.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4dd9485f930..3bfd6769e08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220317.0 +home-assistant-frontend==20220322.0 # homeassistant.components.home_connect homeconnect==0.7.0 From a11a5366be1f7d3f903a4fe04c2fcda164343e34 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 22 Mar 2022 23:45:35 -0400 Subject: [PATCH 0627/1054] Remove deprecated yaml config from androidtv (#68339) --- .../components/androidtv/__init__.py | 36 +------ .../components/androidtv/config_flow.py | 18 ---- homeassistant/components/androidtv/const.py | 2 - .../components/androidtv/media_player.py | 98 +------------------ .../components/androidtv/test_config_flow.py | 64 +----------- 5 files changed, 6 insertions(+), 212 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 157b618a264..0c76b4454f1 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -1,13 +1,11 @@ """Support for functionality to interact with Android TV/Fire TV devices.""" -import logging import os from adb_shell.auth.keygen import keygen from androidtv.adb_manager.adb_manager_sync import ADBPythonSync from androidtv.setup_async import setup -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, @@ -17,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR @@ -35,7 +32,6 @@ from .const import ( DEVICE_FIRETV, DOMAIN, PROP_ETHMAC, - PROP_SERIALNO, PROP_WIFIMAC, SIGNAL_CONFIG_ENTITY, ) @@ -45,8 +41,6 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] _INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} -_LOGGER = logging.getLogger(__name__) - def get_androidtv_mac(dev_props): """Return formatted mac from device properties.""" @@ -116,30 +110,6 @@ async def async_connect_androidtv( return aftv, None -def _migrate_aftv_entity(hass, aftv, entry_unique_id): - """Migrate a entity to new unique id.""" - entity_reg = er.async_get(hass) - - entity_unique_id = entry_unique_id - if entity_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, entity_unique_id): - # entity already exist, nothing to do - return - - if not (old_unique_id := aftv.device_properties.get(PROP_SERIALNO)): - # serial no not found, exit - return - - migr_entity = entity_reg.async_get_entity_id(MP_DOMAIN, DOMAIN, old_unique_id) - if not migr_entity: - # old entity not found, exit - return - - try: - entity_reg.async_update_entity(migr_entity, new_unique_id=entity_unique_id) - except ValueError as exp: - _LOGGER.warning("Migration of old entity failed: %s", exp) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Android TV integration.""" return True @@ -155,10 +125,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not aftv: raise ConfigEntryNotReady(error_message) - # migrate existing entity to new unique ID - if entry.source == SOURCE_IMPORT: - _migrate_aftv_entity(hass, aftv, entry.unique_id) - async def async_close_connection(event): """Close Android TV connection on HA Stop.""" await aftv.adb_close() diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 8f0efc34799..3dbae1cf393 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -20,7 +20,6 @@ from .const import ( CONF_APPS, CONF_EXCLUDE_UNNAMED_APPS, CONF_GET_SOURCES, - CONF_MIGRATION_OPTIONS, CONF_SCREENCAP, CONF_STATE_DETECTION_RULES, CONF_TURN_OFF_COMMAND, @@ -72,10 +71,6 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): - """Initialize AndroidTV config flow.""" - self._import_options = None - @callback def _show_setup_form(self, user_input=None, error=None): """Show the setup form to the user.""" @@ -171,24 +166,11 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=user_input.get(CONF_NAME) or host, data=user_input, - options=self._import_options, ) user_input = user_input or {} return self._show_setup_form(user_input, error) - async def async_step_import(self, import_config=None): - """Import a config entry.""" - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == import_config[CONF_HOST]: - _LOGGER.warning( - "Host [%s] already configured. This yaml configuration has already been imported. Please remove it", - import_config[CONF_HOST], - ) - return self.async_abort(reason="already_configured") - self._import_options = import_config.pop(CONF_MIGRATION_OPTIONS, None) - return await self.async_step_user(import_config) - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/androidtv/const.py b/homeassistant/components/androidtv/const.py index f6f0c07286f..9c7fd1d7de2 100644 --- a/homeassistant/components/androidtv/const.py +++ b/homeassistant/components/androidtv/const.py @@ -10,7 +10,6 @@ CONF_ADBKEY = "adbkey" CONF_APPS = "apps" CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" CONF_GET_SOURCES = "get_sources" -CONF_MIGRATION_OPTIONS = "migration_options" CONF_SCREENCAP = "screencap" CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_TURN_OFF_COMMAND = "turn_off_command" @@ -28,7 +27,6 @@ DEVICE_FIRETV = "firetv" DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] PROP_ETHMAC = "ethmac" -PROP_SERIALNO = "serialno" PROP_WIFIMAC = "wifimac" SIGNAL_CONFIG_ENTITY = "androidtv_config" diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 1ab592143c6..19cae59a1b4 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -12,13 +12,12 @@ from adb_shell.exceptions import ( InvalidResponseError, TcpTimeoutException, ) -from androidtv import ha_state_detection_rules_validator from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -32,17 +31,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, ATTR_CONNECTIONS, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SW_VERSION, - CONF_DEVICE_CLASS, CONF_HOST, - CONF_NAME, - CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, @@ -55,31 +51,21 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, - CONF_ADB_SERVER_IP, - CONF_ADB_SERVER_PORT, - CONF_ADBKEY, CONF_APPS, CONF_EXCLUDE_UNNAMED_APPS, CONF_GET_SOURCES, - CONF_MIGRATION_OPTIONS, CONF_SCREENCAP, - CONF_STATE_DETECTION_RULES, CONF_TURN_OFF_COMMAND, CONF_TURN_ON_COMMAND, - DEFAULT_ADB_SERVER_PORT, - DEFAULT_DEVICE_CLASS, DEFAULT_EXCLUDE_UNNAMED_APPS, DEFAULT_GET_SOURCES, - DEFAULT_PORT, DEFAULT_SCREENCAP, DEVICE_ANDROIDTV, - DEVICE_CLASSES, DOMAIN, SIGNAL_CONFIG_ENTITY, ) @@ -123,34 +109,6 @@ SERVICE_UPLOAD = "upload" DEFAULT_NAME = "Android TV" -# Deprecated in Home Assistant 2022.2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( - DEVICE_CLASSES - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_ADBKEY): cv.isfile, - vol.Optional(CONF_ADB_SERVER_IP): cv.string, - vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, - vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional(CONF_APPS, default={}): vol.Schema( - {cv.string: vol.Any(cv.string, None)} - ), - vol.Optional(CONF_TURN_ON_COMMAND): cv.string, - vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, - vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( - {cv.string: ha_state_detection_rules_validator(vol.Invalid)} - ), - vol.Optional( - CONF_EXCLUDE_UNNAMED_APPS, default=DEFAULT_EXCLUDE_UNNAMED_APPS - ): cv.boolean, - vol.Optional(CONF_SCREENCAP, default=DEFAULT_SCREENCAP): cv.boolean, - } -) - # Translate from `AndroidTV` / `FireTV` reported state to HA state. ANDROIDTV_STATES = { "off": STATE_OFF, @@ -161,53 +119,6 @@ ANDROIDTV_STATES = { } -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Android TV / Fire TV platform.""" - - host = config[CONF_HOST] - - # get main data - config_data = { - CONF_HOST: host, - CONF_DEVICE_CLASS: config.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS), - CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), - } - for key in (CONF_ADBKEY, CONF_ADB_SERVER_IP, CONF_ADB_SERVER_PORT, CONF_NAME): - if key in config: - config_data[key] = config[key] - - # get options - config_options = { - key: config[key] - for key in ( - CONF_APPS, - CONF_EXCLUDE_UNNAMED_APPS, - CONF_GET_SOURCES, - CONF_SCREENCAP, - CONF_STATE_DETECTION_RULES, - CONF_TURN_OFF_COMMAND, - CONF_TURN_ON_COMMAND, - ) - if key in config - } - - # save option to use with entry - if config_options: - config_data[CONF_MIGRATION_OPTIONS] = config_options - - # Launch config entries setup - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config_data - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -217,10 +128,7 @@ async def async_setup_entry( aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] device_class = aftv.DEVICE_CLASS device_type = "Android TV" if device_class == DEVICE_ANDROIDTV else "Fire TV" - if CONF_NAME in entry.data: - device_name = entry.data[CONF_NAME] - else: - device_name = f"{device_type} {entry.data[CONF_HOST]}" + device_name = f"{device_type} {entry.data[CONF_HOST]}" device_args = [ aftv, diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index 991d3757749..3bef7afb1f6 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -33,10 +33,8 @@ from homeassistant.components.androidtv.const import ( PROP_ETHMAC, PROP_WIFIMAC, ) -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PLATFORM, CONF_PORT -from homeassistant.setup import async_setup_component +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from tests.common import MockConfigEntry from tests.components.androidtv.patchers import isfile @@ -132,28 +130,6 @@ async def test_user(hass, config, eth_mac, wifi_mac): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test import config.""" - - # test with all provided - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONFIG_PYTHON_ADB, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"] == CONFIG_PYTHON_ADB - - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_user_adbkey(hass): """Test user step with adbkey file.""" config_data = CONFIG_PYTHON_ADB.copy() @@ -178,25 +154,6 @@ async def test_user_adbkey(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_data(hass): - """Test import from configuration file.""" - config_data = CONFIG_PYTHON_ADB.copy() - config_data[CONF_PLATFORM] = DOMAIN - config_data[CONF_ADBKEY] = ADBKEY - config_data[CONF_TURN_OFF_COMMAND] = "off" - platform_data = {MP_DOMAIN: config_data} - - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP, PATCH_ISFILE, PATCH_ACCESS: - - assert await async_setup_component(hass, MP_DOMAIN, platform_data) - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_error_both_key_server(hass): """Test we abort if both adb key and server are provided.""" config_data = CONFIG_ADB_SERVER.copy() @@ -317,23 +274,6 @@ async def test_abort_if_host_exist(hass): assert result["reason"] == "already_configured" -async def test_abort_import_if_host_exist(hass): - """Test we abort if component is already setup.""" - MockConfigEntry( - domain=DOMAIN, data=CONFIG_ADB_SERVER, unique_id=ETH_MAC - ).add_to_hass(hass) - - # Should fail, same Host in entry - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONFIG_ADB_SERVER, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_abort_if_unique_exist(hass): """Test we abort if component is already setup.""" config_data = CONFIG_ADB_SERVER.copy() From 45a80f182df3d67acc8b95b26985b38322511f65 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 22 Mar 2022 23:50:02 -0400 Subject: [PATCH 0628/1054] Dump entities in zwave_js device diagnostics (#68536) --- .../components/zwave_js/diagnostics.py | 61 ++++++++++++++++++- tests/components/zwave_js/test_diagnostics.py | 8 ++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index c9e45f09685..dd88f2b6d07 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -1,9 +1,11 @@ """Provides diagnostics for Z-Wave JS.""" from __future__ import annotations +from typing import Any + from zwave_js_server.client import Client from zwave_js_server.dump import dump_msgs -from zwave_js_server.model.node import NodeDataType +from zwave_js_server.model.node import Node, NodeDataType from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -11,6 +13,8 @@ from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import async_entries_for_device, async_get from .const import DATA_CLIENT, DOMAIN from .helpers import get_home_and_node_id_from_device_entry @@ -18,6 +22,59 @@ from .helpers import get_home_and_node_id_from_device_entry TO_REDACT = {"homeId", "location"} +def get_device_entities( + hass: HomeAssistant, node: Node, device: DeviceEntry +) -> list[dict[str, Any]]: + """Get entities for a device.""" + entity_entries = async_entries_for_device( + async_get(hass), device.id, include_disabled_entities=True + ) + entities = [] + for entry in entity_entries: + state_key = None + split_unique_id = entry.unique_id.split(".") + # If the unique ID has three parts, it's either one of the generic per node + # entities (node status sensor, ping button) or a binary sensor for a particular + # state. If we can get the state key, we will add it to the dictionary. + if len(split_unique_id) == 3: + try: + state_key = int(split_unique_id[-1]) + # If the third part of the unique ID isn't a state key, the entity must be a + # generic entity. We won't add those since they won't help with + # troubleshooting. + except ValueError: + continue + value_id = split_unique_id[1] + zwave_value = node.values[value_id] + primary_value_data = { + "command_class": zwave_value.command_class, + "command_class_name": zwave_value.command_class_name, + "endpoint": zwave_value.endpoint, + "property": zwave_value.property_, + "property_name": zwave_value.property_name, + "property_key": zwave_value.property_key, + "property_key_name": zwave_value.property_key_name, + } + if state_key is not None: + primary_value_data["state_key"] = state_key + entity = { + "domain": entry.domain, + "entity_id": entry.entity_id, + "original_name": entry.original_name, + "original_device_class": entry.original_device_class, + "disabled": entry.disabled, + "disabled_by": entry.disabled_by, + "hidden_by": entry.hidden_by, + "original_icon": entry.original_icon, + "entity_category": entry.entity_category, + "supported_features": entry.supported_features, + "unit_of_measurement": entry.unit_of_measurement, + "primary_value": primary_value_data, + } + entities.append(entity) + return entities + + async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> list[dict]: @@ -38,6 +95,7 @@ async def async_get_device_diagnostics( if node_id is None or node_id not in client.driver.controller.nodes: raise ValueError(f"Node for device {device.id} can't be found") node = client.driver.controller.nodes[node_id] + entities = get_device_entities(hass, node, device) return { "versionInfo": { "driverVersion": client.version.driver_version, @@ -45,5 +103,6 @@ async def async_get_device_diagnostics( "minSchemaVersion": client.version.min_schema_version, "maxSchemaVersion": client.version.max_schema_version, }, + "entities": entities, "state": async_redact_data(node.data, TO_REDACT), } diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 332c8c84635..84fb401e31b 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -5,6 +5,7 @@ import pytest from zwave_js_server.event import Event from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics +from homeassistant.components.zwave_js.discovery import async_discover_node_values from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.helpers.device_registry import async_get @@ -69,7 +70,12 @@ async def test_device_diagnostics( "minSchemaVersion": 0, "maxSchemaVersion": 0, } - + # Assert that we only have the entities that were discovered for this device + # Entities that are created outside of discovery (e.g. node status sensor and + # ping button) should not be in dump. + assert len(diagnostics_data["entities"]) == len( + list(async_discover_node_values(multisensor_6, device, {device.id: set()})) + ) assert diagnostics_data["state"] == multisensor_6.data From e1ae940a34912836966b8a2f4361577c9640b709 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 23 Mar 2022 00:01:24 -0400 Subject: [PATCH 0629/1054] Add config flow to deluge (#58789) --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/components/deluge/__init__.py | 88 +++++++++- .../components/deluge/config_flow.py | 125 ++++++++++++++ homeassistant/components/deluge/const.py | 12 ++ .../components/deluge/coordinator.py | 68 ++++++++ homeassistant/components/deluge/manifest.json | 3 +- homeassistant/components/deluge/sensor.py | 149 +++++++--------- homeassistant/components/deluge/strings.json | 23 +++ homeassistant/components/deluge/switch.py | 122 +++++-------- .../components/deluge/translations/en.json | 23 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/deluge/__init__.py | 32 ++++ tests/components/deluge/test_config_flow.py | 160 ++++++++++++++++++ 15 files changed, 650 insertions(+), 163 deletions(-) create mode 100644 homeassistant/components/deluge/config_flow.py create mode 100644 homeassistant/components/deluge/const.py create mode 100644 homeassistant/components/deluge/coordinator.py create mode 100644 homeassistant/components/deluge/strings.json create mode 100644 homeassistant/components/deluge/translations/en.json create mode 100644 tests/components/deluge/__init__.py create mode 100644 tests/components/deluge/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 1216a78370a..06d5265c9f2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -197,6 +197,8 @@ omit = homeassistant/components/decora/light.py homeassistant/components/decora_wifi/light.py homeassistant/components/delijn/* + homeassistant/components/deluge/__init__.py + homeassistant/components/deluge/coordinator.py homeassistant/components/deluge/sensor.py homeassistant/components/deluge/switch.py homeassistant/components/denon/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 6ce57254f5a..bc801a5f8f0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -210,6 +210,8 @@ tests/components/deconz/* @Kane610 homeassistant/components/default_config/* @home-assistant/core tests/components/default_config/* @home-assistant/core homeassistant/components/delijn/* @bollewolle @Emilv2 +homeassistant/components/deluge/* @tkdrob +tests/components/deluge/* @tkdrob homeassistant/components/demo/* @home-assistant/core tests/components/demo/* @home-assistant/core homeassistant/components/denonavr/* @ol-iver @starkillerOG diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index ad40b688fcf..d8d6945be2a 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -1 +1,87 @@ -"""The deluge component.""" +"""The Deluge integration.""" +from __future__ import annotations + +import logging +import socket +from ssl import SSLError + +from deluge_client.client import DelugeRPCClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import CONF_WEB_PORT, DEFAULT_NAME, DOMAIN +from .coordinator import DelugeDataUpdateCoordinator + +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Deluge from a config entry.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + api = await hass.async_add_executor_job( + DelugeRPCClient, host, port, username, password + ) + api.web_port = entry.data[CONF_WEB_PORT] + try: + await hass.async_add_executor_job(api.connect) + except ( + ConnectionRefusedError, + socket.timeout, # pylint:disable=no-member + SSLError, + ) as ex: + raise ConfigEntryNotReady("Connection to Deluge Daemon failed") from ex + except Exception as ex: # pylint:disable=broad-except + if type(ex).__name__ == "BadLoginError": + raise ConfigEntryAuthFailed( + "Credentials for Deluge client are not valid" + ) from ex + _LOGGER.error("Unknown error connecting to Deluge: %s", ex) + + coordinator = DelugeDataUpdateCoordinator(hass, api, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]): + """Representation of a Deluge entity.""" + + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: + """Initialize a Deluge entity.""" + super().__init__(coordinator) + self._server_unique_id = coordinator.config_entry.entry_id + self._attr_device_info = DeviceInfo( + configuration_url=f"http://{coordinator.api.host}:{coordinator.api.web_port}", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, + sw_version=coordinator.api.deluge_version, + ) diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py new file mode 100644 index 00000000000..a15c0608029 --- /dev/null +++ b/homeassistant/components/deluge/config_flow.py @@ -0,0 +1,125 @@ +"""Config flow for the Deluge integration.""" +from __future__ import annotations + +import logging +import socket +from ssl import SSLError +from typing import Any + +from deluge_client.client import DelugeRPCClient +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SOURCE, + CONF_USERNAME, +) +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_WEB_PORT, + DEFAULT_NAME, + DEFAULT_RPC_PORT, + DEFAULT_WEB_PORT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Deluge.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + title = None + + if user_input is not None: + if CONF_NAME in user_input: + title = user_input.pop(CONF_NAME) + if (error := await self.validate_input(user_input)) is None: + for entry in self._async_current_entries(): + if ( + user_input[CONF_HOST] == entry.data[CONF_HOST] + and user_input[CONF_PORT] == entry.data[CONF_PORT] + ): + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH: + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="already_configured") + return self.async_create_entry( + title=title or DEFAULT_NAME, + data=user_input, + ) + errors["base"] = error + user_input = user_input or {} + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): cv.string, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME) + ): cv.string, + vol.Required(CONF_PASSWORD, default=""): cv.string, + vol.Optional( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_RPC_PORT) + ): int, + vol.Optional( + CONF_WEB_PORT, + default=user_input.get(CONF_WEB_PORT, DEFAULT_WEB_PORT), + ): int, + } + ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + return await self.async_step_user() + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + if CONF_MONITORED_VARIABLES in config: + config.pop(CONF_MONITORED_VARIABLES) + config[CONF_WEB_PORT] = DEFAULT_WEB_PORT + + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == config[CONF_HOST]: + _LOGGER.warning( + "Deluge yaml config has been imported. Please remove it" + ) + return self.async_abort(reason="already_configured") + return await self.async_step_user(config) + + async def validate_input(self, user_input: dict[str, Any]) -> str | None: + """Handle common flow input validation.""" + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + api = DelugeRPCClient( + host=host, port=port, username=username, password=password + ) + try: + await self.hass.async_add_executor_job(api.connect) + except ( + ConnectionRefusedError, + socket.timeout, # pylint:disable=no-member + SSLError, + ): + return "cannot_connect" + except Exception as ex: # pylint:disable=broad-except + if type(ex).__name__ == "BadLoginError": + return "invalid_auth" # pragma: no cover + return "unknown" + return None diff --git a/homeassistant/components/deluge/const.py b/homeassistant/components/deluge/const.py new file mode 100644 index 00000000000..505c20e860f --- /dev/null +++ b/homeassistant/components/deluge/const.py @@ -0,0 +1,12 @@ +"""Constants for the Deluge integration.""" +import logging + +CONF_WEB_PORT = "web_port" +DEFAULT_NAME = "Deluge" +DEFAULT_RPC_PORT = 58846 +DEFAULT_WEB_PORT = 8112 +DHT_UPLOAD = 1000 +DHT_DOWNLOAD = 1000 +DOMAIN = "deluge" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py new file mode 100644 index 00000000000..36c2d5d2240 --- /dev/null +++ b/homeassistant/components/deluge/coordinator.py @@ -0,0 +1,68 @@ +"""Data update coordinator for the Deluge integration.""" +from __future__ import annotations + +from datetime import timedelta +import socket +from ssl import SSLError + +from deluge_client.client import DelugeRPCClient, FailedToReconnectException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + + +class DelugeDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for the Deluge integration.""" + + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, api: DelugeRPCClient, entry: ConfigEntry + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=entry.title, + update_interval=timedelta(seconds=30), + ) + self.api = api + self.config_entry = entry + + async def _async_update_data(self) -> dict[Platform, dict[str, int | str]]: + """Get the latest data from Deluge and updates the state.""" + data = {} + try: + data[Platform.SENSOR] = await self.hass.async_add_executor_job( + self.api.call, + "core.get_session_status", + [ + "upload_rate", + "download_rate", + "dht_upload_rate", + "dht_download_rate", + ], + ) + data[Platform.SWITCH] = await self.hass.async_add_executor_job( + self.api.call, "core.get_torrents_status", {}, ["paused"] + ) + except ( + ConnectionRefusedError, + socket.timeout, # pylint:disable=no-member + SSLError, + FailedToReconnectException, + ) as ex: + raise UpdateFailed(f"Connection to Deluge Daemon Lost: {ex}") from ex + except Exception as ex: # pylint:disable=broad-except + if type(ex).__name__ == "BadLoginError": + raise ConfigEntryAuthFailed( + "Credentials for Deluge client are not valid" + ) from ex + LOGGER.error("Unknown error connecting to Deluge: %s", ex) + raise ex + return data diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 5bf4651096c..920e560b70f 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -3,7 +3,8 @@ "name": "Deluge", "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": ["deluge-client==1.7.1"], - "codeowners": [], + "codeowners": ["@tkdrob"], + "config_flow": true, "iot_class": "local_polling", "loggers": ["deluge_client"] } diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 01241b0d120..99e63e6ef17 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,16 +1,15 @@ """Support for monitoring the Deluge BitTorrent client API.""" from __future__ import annotations -import logging - -from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, + SensorStateClass, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MONITORED_VARIABLES, @@ -20,20 +19,17 @@ from homeassistant.const import ( CONF_USERNAME, DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, + Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -_LOGGER = logging.getLogger(__name__) -_THROTTLED_REFRESH = None +from . import DelugeEntity +from .const import DEFAULT_NAME, DEFAULT_RPC_PORT, DOMAIN +from .coordinator import DelugeDataUpdateCoordinator -DEFAULT_NAME = "Deluge" -DEFAULT_PORT = 58846 -DHT_UPLOAD = 1000 -DHT_DOWNLOAD = 1000 SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="current_status", @@ -43,21 +39,24 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="download_speed", name="Down Speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="upload_speed", name="Up Speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, ), ) SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +# Deprecated in Home Assistant 2022.3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port, vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( @@ -67,92 +66,70 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: entity_platform.AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Deluge sensors.""" + """Set up the Deluge sensor component.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) - name = config[CONF_NAME] - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - port = config[CONF_PORT] - deluge_api = DelugeRPCClient(host, port, username, password) - try: - deluge_api.connect() - except ConnectionRefusedError as err: - _LOGGER.error("Connection to Deluge Daemon failed") - raise PlatformNotReady from err - monitored_variables = config[CONF_MONITORED_VARIABLES] - entities = [ - DelugeSensor(deluge_api, name, description) +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up the Deluge sensor.""" + async_add_entities( + DelugeSensor(hass.data[DOMAIN][entry.entry_id], description) for description in SENSOR_TYPES - if description.key in monitored_variables - ] - - add_entities(entities) + ) -class DelugeSensor(SensorEntity): +class DelugeSensor(DelugeEntity, SensorEntity): """Representation of a Deluge sensor.""" def __init__( - self, deluge_client, client_name, description: SensorEntityDescription - ): + self, + coordinator: DelugeDataUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.entity_description = description - self.client = deluge_client - self.data = None + self._attr_name = f"{coordinator.config_entry.title} {description.name}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" - self._attr_available = False - self._attr_name = f"{client_name} {description.name}" + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + if self.coordinator.data: + data = self.coordinator.data[Platform.SENSOR] + upload = data[b"upload_rate"] - data[b"dht_upload_rate"] + download = data[b"download_rate"] - data[b"dht_download_rate"] + if self.entity_description.key == "current_status": + if data: + if upload > 0 and download > 0: + return "Up/Down" + if upload > 0 and download == 0: + return "Seeding" + if upload == 0 and download > 0: + return "Downloading" + return STATE_IDLE - def update(self): - """Get the latest data from Deluge and updates the state.""" - - try: - self.data = self.client.call( - "core.get_session_status", - [ - "upload_rate", - "download_rate", - "dht_upload_rate", - "dht_download_rate", - ], - ) - self._attr_available = True - except FailedToReconnectException: - _LOGGER.error("Connection to Deluge Daemon Lost") - self._attr_available = False - return - - upload = self.data[b"upload_rate"] - self.data[b"dht_upload_rate"] - download = self.data[b"download_rate"] - self.data[b"dht_download_rate"] - - sensor_type = self.entity_description.key - if sensor_type == "current_status": - if self.data: - if upload > 0 and download > 0: - self._attr_native_value = "Up/Down" - elif upload > 0 and download == 0: - self._attr_native_value = "Seeding" - elif upload == 0 and download > 0: - self._attr_native_value = "Downloading" - else: - self._attr_native_value = STATE_IDLE - else: - self._attr_native_value = None - - if self.data: - if sensor_type == "download_speed": - kb_spd = float(download) - kb_spd = kb_spd / 1024 - self._attr_native_value = round(kb_spd, 2 if kb_spd < 0.1 else 1) - elif sensor_type == "upload_speed": - kb_spd = float(upload) - kb_spd = kb_spd / 1024 - self._attr_native_value = round(kb_spd, 2 if kb_spd < 0.1 else 1) + if data: + if self.entity_description.key == "download_speed": + kb_spd = float(download) + kb_spd = kb_spd / 1024 + return round(kb_spd, 2 if kb_spd < 0.1 else 1) + if self.entity_description.key == "upload_speed": + kb_spd = float(upload) + kb_spd = kb_spd / 1024 + return round(kb_spd, 2 if kb_spd < 0.1 else 1) + return None diff --git a/homeassistant/components/deluge/strings.json b/homeassistant/components/deluge/strings.json new file mode 100644 index 00000000000..9474e25a534 --- /dev/null +++ b/homeassistant/components/deluge/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "web_port": "Web port (for visiting service)" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 16a5f023052..d438d236e5c 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -1,118 +1,90 @@ """Support for setting the Deluge BitTorrent client in Pause.""" from __future__ import annotations -import logging +from typing import Any -from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - STATE_OFF, - STATE_ON, + Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Deluge Switch" -DEFAULT_PORT = 58846 +from . import DelugeEntity +from .const import DEFAULT_RPC_PORT, DOMAIN +from .coordinator import DelugeDataUpdateCoordinator +# Deprecated in Home Assistant 2022.3 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME, default="Deluge Switch"): cv.string, } ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: entity_platform.AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Deluge sensor component.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up the Deluge switch.""" - - name = config[CONF_NAME] - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - port = config[CONF_PORT] - - deluge_api = DelugeRPCClient(host, port, username, password) - try: - deluge_api.connect() - except ConnectionRefusedError as err: - _LOGGER.error("Connection to Deluge Daemon failed") - raise PlatformNotReady from err - - add_entities([DelugeSwitch(deluge_api, name)]) + async_add_entities([DelugeSwitch(hass.data[DOMAIN][entry.entry_id])]) -class DelugeSwitch(SwitchEntity): +class DelugeSwitch(DelugeEntity, SwitchEntity): """Representation of a Deluge switch.""" - def __init__(self, deluge_client, name): + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize the Deluge switch.""" - self._name = name - self.deluge_client = deluge_client - self._state = STATE_OFF - self._available = False + super().__init__(coordinator) + self._attr_name = coordinator.config_entry.title + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_enabled" - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state == STATE_ON - - @property - def available(self): - """Return true if device is available.""" - return self._available - - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - torrent_ids = self.deluge_client.call("core.get_session_state") - self.deluge_client.call("core.resume_torrent", torrent_ids) + torrent_ids = self.coordinator.api.call("core.get_session_state") + self.coordinator.api.call("core.resume_torrent", torrent_ids) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - torrent_ids = self.deluge_client.call("core.get_session_state") - self.deluge_client.call("core.pause_torrent", torrent_ids) + torrent_ids = self.coordinator.api.call("core.get_session_state") + self.coordinator.api.call("core.pause_torrent", torrent_ids) - def update(self): - """Get the latest data from deluge and updates the state.""" - - try: - torrent_list = self.deluge_client.call( - "core.get_torrents_status", {}, ["paused"] - ) - self._available = True - except FailedToReconnectException: - _LOGGER.error("Connection to Deluge Daemon Lost") - self._available = False - return - for torrent in torrent_list.values(): - item = torrent.popitem() - if not item[1]: - self._state = STATE_ON - return - - self._state = STATE_OFF + @property + def is_on(self) -> bool: + """Return state of the switch.""" + if self.coordinator.data: + data: dict = self.coordinator.data[Platform.SWITCH] + for torrent in data.values(): + item = torrent.popitem() + if not item[1]: + return True + return False diff --git a/homeassistant/components/deluge/translations/en.json b/homeassistant/components/deluge/translations/en.json new file mode 100644 index 00000000000..a3a2f539126 --- /dev/null +++ b/homeassistant/components/deluge/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls", + "data": { + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "web_port": "Web port (for visiting service)" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "abort": { + "already_configured": "Service is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 25d6ec2c807..e1b6e95800d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -66,6 +66,7 @@ FLOWS = { "crownstone", "daikin", "deconz", + "deluge", "denonavr", "devolo_home_control", "devolo_home_network", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bfd6769e08..357e9a10a9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -373,6 +373,9 @@ debugpy==1.5.1 # homeassistant.components.ohmconnect defusedxml==0.7.1 +# homeassistant.components.deluge +deluge-client==1.7.1 + # homeassistant.components.denonavr denonavr==0.10.10 diff --git a/tests/components/deluge/__init__.py b/tests/components/deluge/__init__.py new file mode 100644 index 00000000000..47339f8dfd5 --- /dev/null +++ b/tests/components/deluge/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the Deluge integration.""" + +from homeassistant.components.deluge.const import ( + CONF_WEB_PORT, + DEFAULT_RPC_PORT, + DEFAULT_WEB_PORT, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +CONF_DATA = { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_PORT: DEFAULT_RPC_PORT, + CONF_WEB_PORT: DEFAULT_WEB_PORT, +} + +IMPORT_DATA = { + CONF_HOST: "1.2.3.4", + CONF_NAME: "Deluge Torrent", + CONF_MONITORED_VARIABLES: ["current_status", "download_speed", "upload_speed"], + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_PORT: DEFAULT_RPC_PORT, +} diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py new file mode 100644 index 00000000000..56dbac55674 --- /dev/null +++ b/tests/components/deluge/test_config_flow.py @@ -0,0 +1,160 @@ +"""Test Deluge config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import CONF_DATA, IMPORT_DATA + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="api") +def mock_deluge_api(): + """Mock an api.""" + with patch("deluge_client.client.DelugeRPCClient.connect"), patch( + "deluge_client.client.DelugeRPCClient._create_socket" + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "deluge_client.client.DelugeRPCClient.connect", + side_effect=ConnectionRefusedError("111: Connection refused"), + ), patch("deluge_client.client.DelugeRPCClient._create_socket"): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch( + "deluge_client.client.DelugeRPCClient.connect", side_effect=Exception + ), patch("deluge_client.client.DelugeRPCClient._create_socket"): + yield + + +@pytest.fixture(name="deluge_setup", autouse=True) +def deluge_setup_fixture(): + """Mock deluge entry setup.""" + with patch("homeassistant.components.deluge.async_setup_entry", return_value=True): + yield + + +async def test_flow_user(hass: HomeAssistant, api): + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured(hass: HomeAssistant, api): + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error): + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error): + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_import(hass: HomeAssistant, api): + """Test import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Deluge Torrent" + assert result["data"] == CONF_DATA + + +async def test_flow_import_already_configured(hass: HomeAssistant, api): + """Test import step already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_reauth(hass: HomeAssistant, api): + """Test reauth step.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=CONF_DATA, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == CONF_DATA From 7381c2114fd1a4b3aed1250345d8db400f2c5c6a Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 05:57:04 +0100 Subject: [PATCH 0630/1054] Remove color temperature workaround in Hue integration (#68531) --- homeassistant/components/hue/v2/light.py | 62 ++++++------------------ 1 file changed, 14 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 5b4574c717c..ff20e7f5881 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -95,9 +95,6 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= SUPPORT_TRANSITION - self._last_xy: tuple[float, float] | None = self.xy_color - self._last_color_temp: int = self.color_temp - self._set_color_mode() @property def brightness(self) -> int | None: @@ -112,6 +109,20 @@ class HueLight(HueBaseEntity, LightEntity): """Return true if device is on (brightness above 0).""" return self.resource.on.on + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if color_temp := self.resource.color_temperature: + # Hue lights return `mired_valid` to indicate CT is active + if color_temp.mirek_valid and color_temp.mirek is not None: + return COLOR_MODE_COLOR_TEMP + if self.resource.supports_color: + return COLOR_MODE_XY + if self.resource.supports_dimming: + return COLOR_MODE_BRIGHTNESS + # fallback to on_off + return COLOR_MODE_ONOFF + @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color.""" @@ -153,11 +164,6 @@ class HueLight(HueBaseEntity, LightEntity): "dynamics": self.resource.dynamics.status.value, } - @callback - def on_update(self) -> None: - """Call on update event.""" - self._set_color_mode() - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) @@ -212,43 +218,3 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, short=flash == FLASH_SHORT, ) - - @callback - def _set_color_mode(self) -> None: - """Set current colormode of light.""" - last_xy = self._last_xy - last_color_temp = self._last_color_temp - self._last_xy = self.xy_color - self._last_color_temp = self.color_temp - - # Certified Hue lights return `mired_valid` to indicate CT is active - if color_temp := self.resource.color_temperature: - if color_temp.mirek_valid and color_temp.mirek is not None: - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - return - - # Non-certified lights do not report their current color mode correctly - # so we keep track of the color values to determine which is active - if last_color_temp != self.color_temp: - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - return - if last_xy != self.xy_color: - self._attr_color_mode = COLOR_MODE_XY - return - - # if we didn't detect any changes, abort and use previous values - if self._attr_color_mode is not None: - return - - # color mode not yet determined, work it out here - # Note that for lights that do not correctly report `mirek_valid` - # we might have an invalid startup state which will be auto corrected - if self.resource.supports_color: - self._attr_color_mode = COLOR_MODE_XY - elif self.resource.supports_color_temperature: - self._attr_color_mode = COLOR_MODE_COLOR_TEMP - elif self.resource.supports_dimming: - self._attr_color_mode = COLOR_MODE_BRIGHTNESS - else: - # fallback to on_off - self._attr_color_mode = COLOR_MODE_ONOFF From dd1d7fdbabbc7c96e7fd5c9f39dc340a760ad701 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 05:59:06 +0100 Subject: [PATCH 0631/1054] Simplify Hue error handling a bit (#68529) --- homeassistant/components/hue/bridge.py | 19 +++++++---------- homeassistant/components/hue/v2/group.py | 27 ++---------------------- homeassistant/components/hue/v2/light.py | 11 ---------- tests/components/hue/conftest.py | 2 +- 4 files changed, 11 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index dd6182b244a..e15da5c8489 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -117,22 +117,19 @@ class HueBridge: self.authorized = True return True - async def async_request_call( - self, task: Callable, *args, allowed_errors: list[str] | None = None, **kwargs - ) -> Any: - """Send request to the Hue bridge, optionally omitting error(s).""" + async def async_request_call(self, task: Callable, *args, **kwargs) -> Any: + """Send request to the Hue bridge.""" try: return await task(*args, **kwargs) except AiohueException as err: - # The (new) Hue api can be a bit fanatic with throwing errors - # some of which we accept in certain conditions - # handle that here. Note that these errors are strings and do not have - # an identifier or something. - if allowed_errors is not None and str(err) in allowed_errors: + # The (new) Hue api can be a bit fanatic with throwing errors so + # we have some logic to treat some responses as warning only. + msg = f"Request failed: {err}" + if "may not have effect" in str(err): # log only - self.logger.debug("Ignored error/warning from Hue API: %s", str(err)) + self.logger.debug(msg) return None - raise HomeAssistantError(f"Request failed: {err}") from err + raise HomeAssistantError(msg) from err except aiohttp.ClientError as err: raise HomeAssistantError( f"Request failed due connection error: {err}" diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 948609f4c13..162ef58d320 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -37,21 +37,6 @@ from .helpers import ( normalize_hue_transition, ) -ALLOWED_ERRORS = [ - "device (groupedLight) has communication issues, command (on) may not have effect", - 'device (groupedLight) is "soft off", command (on) may not have effect', - "device (light) has communication issues, command (on) may not have effect", - 'device (light) is "soft off", command (on) may not have effect', - "device (grouped_light) has communication issues, command (.on) may not have effect", - 'device (grouped_light) is "soft off", command (.on) may not have effect' - "device (grouped_light) has communication issues, command (.on.on) may not have effect", - 'device (grouped_light) is "soft off", command (.on.on) may not have effect' - "device (light) has communication issues, command (.on) may not have effect", - 'device (light) is "soft off", command (.on) may not have effect', - "device (light) has communication issues, command (.on.on) may not have effect", - 'device (light) is "soft off", command (.on.on) may not have effect', -] - async def async_setup_entry( hass: HomeAssistant, @@ -183,10 +168,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): and flash is None ): await self.bridge.async_request_call( - self.controller.set_state, - id=self.resource.id, - on=True, - allowed_errors=ALLOWED_ERRORS, + self.controller.set_state, id=self.resource.id, on=True ) return @@ -202,7 +184,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity): color_xy=xy_color if light.supports_color else None, color_temp=color_temp if light.supports_color_temperature else None, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) for light in self.controller.get_lights(self.resource.id) ] @@ -222,10 +203,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # To set other features, you'll have to control the attached lights if transition is None: await self.bridge.async_request_call( - self.controller.set_state, - id=self.resource.id, - on=False, - allowed_errors=ALLOWED_ERRORS, + self.controller.set_state, id=self.resource.id, on=False ) return @@ -237,7 +215,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity): light.id, on=False, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) for light in self.controller.get_lights(self.resource.id) ] diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index ff20e7f5881..28d972b54ec 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -36,15 +36,6 @@ from .helpers import ( normalize_hue_transition, ) -ALLOWED_ERRORS = [ - "device (light) has communication issues, command (on) may not have effect", - 'device (light) is "soft off", command (on) may not have effect', - "device (light) has communication issues, command (.on) may not have effect", - 'device (light) is "soft off", command (.on) may not have effect', - "device (light) has communication issues, command (.on.on) may not have effect", - 'device (light) is "soft off", command (.on.on) may not have effect', -] - async def async_setup_entry( hass: HomeAssistant, @@ -188,7 +179,6 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) async def async_turn_off(self, **kwargs: Any) -> None: @@ -208,7 +198,6 @@ class HueLight(HueBaseEntity, LightEntity): id=self.resource.id, on=False, transition_time=transition, - allowed_errors=ALLOWED_ERRORS, ) async def async_set_flash(self, flash: str) -> None: diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index d0d15d320e0..d730d3f18f5 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -60,7 +60,7 @@ def create_mock_bridge(hass, api_version=1): bridge.async_initialize_bridge = async_initialize_bridge - async def async_request_call(task, *args, allowed_errors=None, **kwargs): + async def async_request_call(task, *args, **kwargs): await task(*args, **kwargs) bridge.async_request_call = async_request_call From 7deeb92485bfff8a104fa04b2fa4226e5575424e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 19:24:21 -1000 Subject: [PATCH 0632/1054] Switch sqlalchemy execute to use .all() instead of list() on the iterator (#68540) --- homeassistant/components/recorder/util.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9d5ee6f29d3..d1d16be4ae2 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -17,6 +17,7 @@ from awesomeversion import ( ) from sqlalchemy import text from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from homeassistant.core import HomeAssistant @@ -111,12 +112,13 @@ def commit(session, work): return False -def execute(qry, to_native=False, validate_entity_ids=True) -> list | None: +def execute( + qry: Query, to_native: bool = False, validate_entity_ids: bool = True +) -> list | None: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. """ - for tryno in range(0, RETRIES): try: timer_start = time.perf_counter() @@ -130,7 +132,7 @@ def execute(qry, to_native=False, validate_entity_ids=True) -> list | None: if row is not None ] else: - result = list(qry) + result = qry.all() if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start @@ -427,6 +429,7 @@ def retryable_database_job(description: str) -> Callable: try: return job(instance, *args, **kwargs) except OperationalError as err: + assert instance.engine is not None if ( instance.engine.dialect.name == "mysql" and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS @@ -453,7 +456,7 @@ def perodic_db_cleanups(instance: Recorder): These cleanups will happen nightly or after any purge. """ - + assert instance.engine is not None if instance.engine.dialect.name == "sqlite": # Execute sqlite to create a wal checkpoint and free up disk space _LOGGER.debug("WAL checkpoint") @@ -464,7 +467,7 @@ def perodic_db_cleanups(instance: Recorder): @contextmanager def write_lock_db_sqlite(instance: Recorder): """Lock database for writes.""" - + assert instance.engine is not None with instance.engine.connect() as connection: # Execute sqlite to create a wal checkpoint # This is optional but makes sure the backup is going to be minimal From df3a163a6664a123e193b2937d2797e96b481a40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Mar 2022 08:10:21 +0100 Subject: [PATCH 0633/1054] Update freezegun to 1.2.1 (#68512) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index a319d6c3005..8b56e50b9e2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ -r requirements_test_pre_commit.txt codecov==2.1.12 coverage==6.3.2 -freezegun==1.2.0 +freezegun==1.2.1 mock-open==1.4.0 mypy==0.941 pre-commit==2.17.0 From 08d6a3d9d47422a06e2528fb164e1ca3119fce9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 21:21:56 -1000 Subject: [PATCH 0634/1054] Cache newly written state attribute ids (#68355) --- homeassistant/components/recorder/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index aae6576c225..d046fe30d5c 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1116,9 +1116,18 @@ class Recorder(threading.Thread): if dbstate in self.event_session: self.event_session.expunge(dbstate) self._pending_expunge = [] - self._pending_state_attributes = {} self.event_session.commit() + # We just committed the state attributes to the database + # and we now know the attributes_ids. We can save + # a many selects for matching attributes by loading them + # into the LRU cache now. + for state_attr in self._pending_state_attributes.values(): + self._state_attributes_ids[ + state_attr.shared_attrs + ] = state_attr.attributes_id + self._pending_state_attributes = {} + # Expire is an expensive operation (frequently more expensive # than the flush and commit itself) so we only # do it after EXPIRE_AFTER_COMMITS commits From 11cdc3706c1b852700b856a113fa86ceef7b07d5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 23 Mar 2022 08:29:00 +0100 Subject: [PATCH 0635/1054] Bump samsungtvws to 2.5.0 (#68542) Co-authored-by: epenet --- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 3ee8fc44c94..2a5b2f76da3 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", - "samsungtvws[async,encrypted]==2.4.0", + "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1" ], "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 18d4c0d9979..518345320fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async,encrypted]==2.4.0 +samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.satel_integra satel_integra==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 357e9a10a9e..678a071db62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1342,7 +1342,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async,encrypted]==2.4.0 +samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.dhcp scapy==2.4.5 From 0c45241d43596eb76680cf4367b8f6018638857e Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 23 Mar 2022 08:50:30 +0100 Subject: [PATCH 0636/1054] Add inverter size to Forecast.Solar (#68263) --- homeassistant/components/forecast_solar/__init__.py | 7 +++++++ homeassistant/components/forecast_solar/config_flow.py | 9 +++++++++ homeassistant/components/forecast_solar/const.py | 1 + homeassistant/components/forecast_solar/manifest.json | 2 +- homeassistant/components/forecast_solar/strings.json | 1 + .../components/forecast_solar/translations/en.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/forecast_solar/conftest.py | 2 ++ tests/components/forecast_solar/test_config_flow.py | 3 +++ 10 files changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 760ad04af98..d4e8e3af969 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -16,6 +16,7 @@ from .const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -29,6 +30,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # this if statement is here to catch that. api_key = entry.options.get(CONF_API_KEY) or None + if ( + inverter_size := entry.options.get(CONF_INVERTER_SIZE) + ) is not None and inverter_size > 0: + inverter_size = inverter_size / 1000 + session = async_get_clientsession(hass) forecast = ForecastSolar( api_key=api_key, @@ -39,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: azimuth=(entry.options[CONF_AZIMUTH] - 180), kwp=(entry.options[CONF_MODULES_POWER] / 1000), damping=entry.options.get(CONF_DAMPING, 0), + inverter=inverter_size, ) # Free account have a resolution of 1 hour, using that as the default diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index e7f41777062..86de17ef285 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -15,6 +15,7 @@ from .const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -118,6 +119,14 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): CONF_DAMPING, default=self.config_entry.options.get(CONF_DAMPING, 0.0), ): vol.Coerce(float), + vol.Optional( + CONF_INVERTER_SIZE, + description={ + "suggested_value": self.config_entry.options.get( + CONF_INVERTER_SIZE + ) + }, + ): vol.Coerce(int), } ), ) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index 63d5bd10084..d9742cf5dfc 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -14,6 +14,7 @@ CONF_DECLINATION = "declination" CONF_AZIMUTH = "azimuth" CONF_MODULES_POWER = "modules power" CONF_DAMPING = "damping" +CONF_INVERTER_SIZE = "inverter_size" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json index dc4b88d160c..472f5cac213 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -3,7 +3,7 @@ "name": "Forecast.Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/forecast_solar", - "requirements": ["forecast_solar==2.1.0"], + "requirements": ["forecast_solar==2.2.0"], "codeowners": ["@klaasnicolaas", "@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index e1ae451a04f..b10e927eb8b 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -22,6 +22,7 @@ "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", "damping": "Damping factor: adjusts the results in the morning and evening", + "inverter_size": "Inverter size (Watt)", "declination": "Declination (0 = Horizontal, 90 = Vertical)", "modules power": "Total Watt peak power of your solar modules" } diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index f9eef2b5c0a..db9bead2e8c 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -21,6 +21,7 @@ "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", "damping": "Damping factor: adjusts the results in the morning and evening", + "inverter_size": "Inverter size (Watt)", "declination": "Declination (0 = Horizontal, 90 = Vertical)", "modules power": "Total Watt peak power of your solar modules" }, diff --git a/requirements_all.txt b/requirements_all.txt index 518345320fd..4047f66c811 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ fnvhash==0.1.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast_solar==2.1.0 +forecast_solar==2.2.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 678a071db62..f1c79379eb8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -453,7 +453,7 @@ fnvhash==0.1.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast_solar==2.1.0 +forecast_solar==2.2.0 # homeassistant.components.freebox freebox-api==0.0.10 diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 408c5423861..0d9f76b367e 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -11,6 +11,7 @@ from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -38,6 +39,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_AZIMUTH: 190, CONF_MODULES_POWER: 5100, CONF_DAMPING: 0.5, + CONF_INVERTER_SIZE: 2000, }, ) diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index d175be986ca..f0611d1678d 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -5,6 +5,7 @@ from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, CONF_DAMPING, CONF_DECLINATION, + CONF_INVERTER_SIZE, CONF_MODULES_POWER, DOMAIN, ) @@ -81,6 +82,7 @@ async def test_options_flow( CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, }, ) @@ -91,4 +93,5 @@ async def test_options_flow( CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122, CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, } From c9cc2eb7c8357ab556f41a4386b0ddcf86e1e454 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:30:01 +0100 Subject: [PATCH 0637/1054] Fix flaky datetime test (#68524) --- tests/conftest.py | 5 +++++ tests/util/test_init.py | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9f19415cafa..92f19f087a6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -797,6 +797,8 @@ def enable_nightly_purge(): @pytest.fixture def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage): """Home Assistant fixture with in-memory recorder.""" + original_tz = dt_util.DEFAULT_TIME_ZONE + hass = get_test_home_assistant() stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None @@ -821,6 +823,9 @@ def hass_recorder(enable_nightly_purge, enable_statistics, hass_storage): yield setup_recorder hass.stop() + # Restore timezone, it is set when creating the hass object + dt_util.DEFAULT_TIME_ZONE = original_tz + @pytest.fixture async def recorder_mock(enable_nightly_purge, enable_statistics, hass): diff --git a/tests/util/test_init.py b/tests/util/test_init.py index d6508b40c37..c1eb878ab95 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -67,9 +67,13 @@ def test_repr_helper(): assert util.repr_helper(5) == "5" assert util.repr_helper(True) == "True" assert util.repr_helper({"test": 1}) == "test=1" - assert ( - util.repr_helper(datetime(1986, 7, 9, 12, 0, 0)) == "1986-07-09T12:00:00+00:00" - ) + + tz = dt_util.get_time_zone("Europe/Copenhagen") + with patch("homeassistant.util.dt.DEFAULT_TIME_ZONE", tz): + assert ( + util.repr_helper(datetime(1986, 7, 9, 12, 0, 0)) + == "1986-07-09T12:00:00+02:00" + ) def test_convert(): From 49bc572d6d411770e0fb6cc17b798fd978406766 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 22 Mar 2022 23:01:20 -1000 Subject: [PATCH 0638/1054] Fix tplink effect not being restored when turning back on (#68533) Co-authored-by: Martin Hjelmare --- homeassistant/components/tplink/light.py | 118 +++++++++++++++++------ tests/components/tplink/test_light.py | 79 +++++++++++++-- 2 files changed, 156 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 3f8179f42f2..b2c6f207f3e 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -43,11 +43,18 @@ async def async_setup_entry( ) -> None: """Set up switches.""" coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - device = cast(SmartBulb, coordinator.device) - if device.is_light_strip: - async_add_entities([TPLinkSmartLightStrip(device, coordinator)]) - elif device.is_bulb or device.is_dimmer: - async_add_entities([TPLinkSmartBulb(device, coordinator)]) + if coordinator.device.is_light_strip: + async_add_entities( + [ + TPLinkSmartLightStrip( + cast(SmartLightStrip, coordinator.device), coordinator + ) + ] + ) + elif coordinator.device.is_bulb or coordinator.device.is_dimmer: + async_add_entities( + [TPLinkSmartBulb(cast(SmartBulb, coordinator.device), coordinator)] + ) class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): @@ -72,9 +79,10 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): else: self._attr_unique_id = device.mac.replace(":", "").upper() - @async_refresh_after - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the light on.""" + @callback + def _async_extract_brightness_transition( + self, **kwargs: Any + ) -> tuple[int | None, int | None]: if (transition := kwargs.get(ATTR_TRANSITION)) is not None: transition = int(transition * 1_000) @@ -89,35 +97,48 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): # except when transition is defined, so we leverage that here for now. transition = 1 - # Handle turning to temp mode - if ATTR_COLOR_TEMP in kwargs: - # Handle temp conversion mireds -> kelvin being slightly outside of valid range - kelvin = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) - kelvin_range = self.device.valid_temperature_range - color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin)) - _LOGGER.debug("Changing color temp to %s", color_tmp) - await self.device.set_color_temp( - color_tmp, brightness=brightness, transition=transition - ) - return + return brightness, transition - # Handling turning to hs color mode - if ATTR_HS_COLOR in kwargs: - # TP-Link requires integers. - hue, sat = tuple(int(val) for val in kwargs[ATTR_HS_COLOR]) - await self.device.set_hsv(hue, sat, brightness, transition=transition) - return + async def _async_set_color_temp( + self, color_temp_mireds: int, brightness: int | None, transition: int | None + ) -> None: + # Handle temp conversion mireds -> kelvin being slightly outside of valid range + kelvin = mired_to_kelvin(color_temp_mireds) + kelvin_range = self.device.valid_temperature_range + color_tmp = max(kelvin_range.min, min(kelvin_range.max, kelvin)) + _LOGGER.debug("Changing color temp to %s", color_tmp) + await self.device.set_color_temp( + color_tmp, brightness=brightness, transition=transition + ) - if ATTR_EFFECT in kwargs: - assert isinstance(self.device, SmartLightStrip) - await self.device.set_effect(kwargs[ATTR_EFFECT]) - return + async def _async_set_hsv( + self, hs_color: tuple[int, int], brightness: int | None, transition: int | None + ) -> None: + # TP-Link requires integers. + hue, sat = tuple(int(val) for val in hs_color) + await self.device.set_hsv(hue, sat, brightness, transition=transition) + async def _async_turn_on_with_brightness( + self, brightness: int | None, transition: int | None + ) -> None: # Fallback to adjusting brightness or turning the bulb on if brightness is not None: await self.device.set_brightness(brightness, transition=transition) + return + await self.device.turn_on(transition=transition) # type: ignore[arg-type] + + @async_refresh_after + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + brightness, transition = self._async_extract_brightness_transition(**kwargs) + if ATTR_COLOR_TEMP in kwargs: + await self._async_set_color_temp( + int(kwargs[ATTR_COLOR_TEMP]), brightness, transition + ) + if ATTR_HS_COLOR in kwargs: + await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition) else: - await self.device.turn_on(transition=transition) + await self._async_turn_on_with_brightness(brightness, transition) @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: @@ -191,6 +212,14 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): device: SmartLightStrip + def __init__( + self, + device: SmartLightStrip, + coordinator: TPLinkDataUpdateCoordinator, + ) -> None: + """Initialize the smart light strip.""" + super().__init__(device, coordinator) + @property def supported_features(self) -> int: """Flag supported features.""" @@ -209,3 +238,30 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb): if (effect := self.device.effect) and effect["enable"]: return cast(str, effect["name"]) return None + + @async_refresh_after + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + brightness, transition = self._async_extract_brightness_transition(**kwargs) + if ATTR_COLOR_TEMP in kwargs: + await self._async_set_color_temp( + int(kwargs[ATTR_COLOR_TEMP]), brightness, transition + ) + elif ATTR_HS_COLOR in kwargs: + await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition) + elif ATTR_EFFECT in kwargs: + await self.device.set_effect(kwargs[ATTR_EFFECT]) + elif ( + self.device.is_off + and self.device.effect + and self.device.effect["enable"] == 0 + and self.device.effect["name"] + ): + if not self.device.effect["custom"]: + await self.device.set_effect(self.device.effect["name"]) + # The device does not remember custom effects + # so we must set a default value or it can never turn back on + else: + await self.device.set_hsv(0, 0, 100, transition=transition) + else: + await self._async_turn_on_with_brightness(brightness, transition) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 32394be790b..3745b5b09a0 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import PropertyMock +from unittest.mock import MagicMock, PropertyMock import pytest @@ -23,7 +23,7 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.tplink.const import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -57,14 +57,17 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF" -@pytest.mark.parametrize("transition", [2.0, None]) -async def test_color_light(hass: HomeAssistant, transition: float | None) -> None: +@pytest.mark.parametrize( + "bulb, transition", [(_mocked_bulb(), 2.0), (_mocked_smart_light_strip(), None)] +) +async def test_color_light( + hass: HomeAssistant, bulb: MagicMock, transition: float | None +) -> None: """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() bulb.color_temp = None with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) @@ -194,14 +197,17 @@ async def test_color_light_no_temp(hass: HomeAssistant) -> None: bulb.set_hsv.reset_mock() -@pytest.mark.parametrize("is_color", [True, False]) -async def test_color_temp_light(hass: HomeAssistant, is_color: bool) -> None: +@pytest.mark.parametrize( + "bulb, is_color", [(_mocked_bulb(), True), (_mocked_smart_light_strip(), False)] +) +async def test_color_temp_light( + hass: HomeAssistant, bulb: MagicMock, is_color: bool +) -> None: """Test a light.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={}, unique_id=MAC_ADDRESS ) already_migrated_config_entry.add_to_hass(hass) - bulb = _mocked_bulb() bulb.is_color = is_color bulb.color_temp = 4000 bulb.is_variable_color_temp = True @@ -415,7 +421,7 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: strip.set_effect.assert_called_once_with("Effect2") strip.set_effect.reset_mock() - strip.effect = {"name": "Effect1", "enable": 0} + strip.effect = {"name": "Effect1", "enable": 0, "custom": 0} async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() @@ -423,10 +429,63 @@ async def test_smart_strip_effects(hass: HomeAssistant) -> None: assert state.state == STATE_ON assert ATTR_EFFECT not in state.attributes - strip.effect_list = None + strip.is_off = True + strip.is_on = False async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert ATTR_EFFECT not in state.attributes + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + strip.set_effect.assert_called_once_with("Effect1") + strip.set_effect.reset_mock() + + strip.is_off = False + strip.is_on = True + strip.effect_list = None + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) assert state.state == STATE_ON assert state.attributes[ATTR_EFFECT_LIST] is None + + +async def test_smart_strip_custom_random_effect_at_start(hass: HomeAssistant) -> None: + """Test smart strip custom random effects at startup.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + strip = _mocked_smart_light_strip() + strip.effect = { + "custom": 1, + "id": "yMwcNpLxijmoKamskHCvvravpbnIqAIN", + "brightness": 100, + "name": "Custom", + "enable": 0, + } + with _patch_discovery(device=strip), _patch_single_discovery(device=strip): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + # fallback to set HSV when custom effect is not known so it does turn back on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + strip.set_hsv.assert_called_with(0, 0, 100, transition=None) + strip.set_hsv.reset_mock() From 1c57e65cea05c182dafacd00edbe017f8cd039e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 10:32:51 +0100 Subject: [PATCH 0639/1054] Exclude hidden entities from homekit (#68552) --- homeassistant/components/homekit/__init__.py | 4 +- .../components/homekit/config_flow.py | 11 ++- tests/components/homekit/test_config_flow.py | 80 ++++++++++++++++++- tests/components/homekit/test_homekit.py | 57 ++++++++++++- 4 files changed, 144 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4cd0940adb7..e2847a597eb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -667,8 +667,8 @@ class HomeKit: if ent_reg_ent := ent_reg.async_get(entity_id): if ( ent_reg_ent.entity_category is not None - and not self._filter.explicitly_included(entity_id) - ): + or ent_reg_ent.hidden_by is not None + ) and not self._filter.explicitly_included(entity_id): continue await self._async_set_device_info_attributes( diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0863b8f583a..d0c0f73ce07 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -538,16 +538,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) ent_reg = entity_registry.async_get(self.hass) - entity_cat_entities = set() + excluded_entities = set() for entity_id in all_supported_entities: if ent_reg_ent := ent_reg.async_get(entity_id): - if ent_reg_ent.entity_category is not None: - entity_cat_entities.add(entity_id) + if ( + ent_reg_ent.entity_category is not None + or ent_reg_ent.hidden_by is not None + ): + excluded_entities.add(entity_id) # Remove entity category entities since we will exclude them anyways all_supported_entities = { k: v for k, v in all_supported_entities.items() - if k not in entity_cat_entities + if k not in excluded_entities } # Strip out entities that no longer exist to prevent error in the UI default_value = [ diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index b4600c3190e..301040e4f88 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -13,7 +13,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import RegistryEntry, RegistryEntryHider from homeassistant.helpers.entityfilter import CONF_INCLUDE_DOMAINS from homeassistant.setup import async_setup_component @@ -1403,3 +1403,81 @@ async def test_options_flow_exclude_mode_skips_category_entities( "include_entities": [], }, } + + +@patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) +async def test_options_flow_exclude_mode_skips_hidden_entities( + port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg +): + """Ensure exclude mode does not offer hidden entities.""" + config_entry = _mock_config_entry_with_options_populated() + await async_init_entry(hass, config_entry) + + hass.states.async_set("media_player.tv", "off") + hass.states.async_set("media_player.sonos", "off") + hass.states.async_set("switch.other", "off") + + sonos_hidden_switch: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "sonos", + "config", + device_id="1234", + hidden_by=RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(sonos_hidden_switch.entity_id, "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["data_schema"]({}) == { + "domains": [ + "fan", + "humidifier", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "mode": "bridge", + "include_exclude_mode": "exclude", + } + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "domains": ["media_player", "switch"], + "mode": "bridge", + "include_exclude_mode": "exclude", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "exclude" + assert _get_schema_default(result2["data_schema"].schema, "entities") == [] + + # sonos_hidden_switch.entity_id is a hidden entity + # so it should not be selectable since it will always be excluded + with pytest.raises(voluptuous.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": [sonos_hidden_switch.entity_id]}, + ) + + result4 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"entities": ["media_player.tv", "switch.other"]}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "mode": "bridge", + "filter": { + "exclude_domains": [], + "exclude_entities": ["media_player.tv", "switch.other"], + "include_domains": ["media_player", "switch"], + "include_entities": [], + }, + } diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 5c79e764af1..8a8c32d3272 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistantError, State -from homeassistant.helpers import device_registry +from homeassistant.helpers import device_registry, entity_registry as er from homeassistant.helpers.entityfilter import ( CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, @@ -485,6 +485,61 @@ async def test_homekit_entity_glob_filter_with_config_entities( assert hass.states.get("select.keep") in filtered_states +async def test_homekit_entity_glob_filter_with_hidden_entities( + hass, mock_async_zeroconf, entity_reg +): + """Test the entity filter with hidden entities.""" + entry = await async_init_integration(hass) + + from homeassistant.helpers.entity_registry import RegistryEntry + + select_config_entity: RegistryEntry = entity_reg.async_get_or_create( + "select", + "any", + "any", + device_id="1234", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(select_config_entity.entity_id, "off") + + switch_config_entity: RegistryEntry = entity_reg.async_get_or_create( + "switch", + "any", + "any", + device_id="1234", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + hass.states.async_set(switch_config_entity.entity_id, "off") + hass.states.async_set("select.keep", "open") + + hass.states.async_set("cover.excluded_test", "open") + hass.states.async_set("light.included_test", "on") + + entity_filter = generate_filter( + ["select"], + ["switch.test", switch_config_entity.entity_id], + [], + [], + ["*.included_*"], + ["*.excluded_*"], + ) + homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE, entity_filter) + + homekit.bridge = Mock() + homekit.bridge.accessories = {} + + filtered_states = await homekit.async_configure_accessories() + assert ( + hass.states.get(switch_config_entity.entity_id) in filtered_states + ) # explicitly included + assert ( + hass.states.get(select_config_entity.entity_id) not in filtered_states + ) # not explicted included and its a hidden entity + assert hass.states.get("cover.excluded_test") not in filtered_states + assert hass.states.get("light.included_test") in filtered_states + assert hass.states.get("select.keep") in filtered_states + + async def test_homekit_start(hass, hk_driver, mock_async_zeroconf, device_reg): """Test HomeKit start method.""" entry = await async_init_integration(hass) From 44d3a7e459e3180022fbbbbc3bf8a3d0f2a8245d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Mar 2022 11:20:04 +0100 Subject: [PATCH 0640/1054] Adjust backup type of Update entity (#68553) --- homeassistant/components/demo/update.py | 5 +---- homeassistant/components/hassio/update.py | 15 +++----------- homeassistant/components/update/__init__.py | 20 +++++-------------- homeassistant/components/wled/update.py | 5 +---- tests/components/update/test_init.py | 4 ++-- .../custom_components/test/update.py | 7 ++----- 6 files changed, 14 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py index a48c4a3cab2..d3b3aacfa68 100644 --- a/homeassistant/components/demo/update.py +++ b/homeassistant/components/demo/update.py @@ -134,10 +134,7 @@ class DemoUpdate(UpdateEntity): self._attr_supported_features |= UpdateEntityFeature.PROGRESS async def async_install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, + self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" if self.supported_features & UpdateEntityFeature.PROGRESS: diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 71f97c62b96..19aee6dad37 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -170,10 +170,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): ) async def async_install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, + self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" try: @@ -214,10 +211,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): return "https://brands.home-assistant.io/hassio/icon.png" async def async_install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, + self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" try: @@ -262,10 +256,7 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity): return f"https://{'rc' if version.beta else 'www'}.home-assistant.io/latest-release-notes/" async def async_install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, + self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" try: diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index 7fb92d8164c..b514553208e 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -82,7 +82,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: SERVICE_INSTALL, { vol.Optional(ATTR_VERSION): cv.string, - vol.Optional(ATTR_BACKUP): cv.boolean, + vol.Optional(ATTR_BACKUP, default=False): cv.boolean, }, async_install, [UpdateEntityFeature.INSTALL], @@ -128,7 +128,7 @@ async def async_install(entity: UpdateEntity, service_call: ServiceCall) -> None # If backup is requested, but not supported by the entity. if ( - backup := service_call.data.get(ATTR_BACKUP) + backup := service_call.data[ATTR_BACKUP] ) and not entity.supported_features & UpdateEntityFeature.BACKUP: raise HomeAssistantError(f"Backup is not supported for {entity.name}") @@ -245,10 +245,7 @@ class UpdateEntity(RestoreEntity): self.async_write_ha_state() async def async_install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, + self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update. @@ -260,12 +257,7 @@ class UpdateEntity(RestoreEntity): """ await self.hass.async_add_executor_job(self.install, version, backup) - def install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, - ) -> None: + def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: """Install an update. Version can be specified to install a specific version. When `None`, the @@ -323,9 +315,7 @@ class UpdateEntity(RestoreEntity): @final async def async_install_with_progress( - self, - version: str | None = None, - backup: bool | None = None, + self, version: str | None, backup: bool ) -> None: """Install update and handle progress if needed. diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index 197d5f4c580..098f2ca3831 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -80,10 +80,7 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity): @wled_exception_handler async def async_install( - self, - version: str | None = None, - backup: bool | None = None, - **kwargs: Any, + self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" if version is None: diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 35366217a3d..7d175337d13 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -117,10 +117,10 @@ async def test_update(hass: HomeAssistant) -> None: assert update.entity_category is EntityCategory.DIAGNOSTIC with pytest.raises(NotImplementedError): - await update.async_install() + await update.async_install(version=None, backup=True) with pytest.raises(NotImplementedError): - update.install() + update.install(version=None, backup=False) update.install = MagicMock() await update.async_install(version="1.0.1", backup=True) diff --git a/tests/testing_config/custom_components/test/update.py b/tests/testing_config/custom_components/test/update.py index aeac37d198e..83b2c065b2e 100644 --- a/tests/testing_config/custom_components/test/update.py +++ b/tests/testing_config/custom_components/test/update.py @@ -6,6 +6,7 @@ Call init before using it in your tests to ensure clean test data. from __future__ import annotations import logging +from typing import Any from homeassistant.components.update import UpdateEntity, UpdateEntityFeature @@ -49,11 +50,7 @@ class MockUpdateEntity(MockEntity, UpdateEntity): """Title of the software.""" return self._handle("title") - def install( - self, - version: str | None = None, - backup: bool | None = None, - ) -> None: + def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: """Install an update.""" if backup: _LOGGER.info("Creating backup before installing update") From 04843a975ea2158432f742b5650967938116f7b3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 23 Mar 2022 12:10:42 +0100 Subject: [PATCH 0641/1054] Improve reload helper typing (#68558) --- homeassistant/helpers/reload.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 5a826d4129e..42fcf1032bb 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -8,13 +8,15 @@ from typing import Any from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from . import config_per_platform +from .entity_component import EntityComponent from .entity_platform import EntityPlatform, async_get_platforms +from .service import async_register_admin_service from .typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -64,7 +66,7 @@ async def _resetup_platform( if not conf: return - root_config: dict[str, Any] = {integration_platform: []} + root_config: dict[str, list[ConfigType]] = {integration_platform: []} # Extract only the config for template, ignore the rest. for p_type, p_config in config_per_platform(conf, integration_platform): if p_type != integration_name: @@ -113,7 +115,7 @@ async def _async_setup_platform( ) return - entity_component = hass.data[integration_platform] + entity_component: EntityComponent = hass.data[integration_platform] tasks = [ entity_component.async_setup_platform(integration_name, p_config) for p_config in platform_configs @@ -163,14 +165,12 @@ async def async_setup_reload_service( if hass.services.has_service(domain, SERVICE_RELOAD): return - async def _reload_config(call: Event) -> None: + async def _reload_config(call: ServiceCall) -> None: """Reload the platforms.""" await async_reload_integration_platforms(hass, domain, platforms) hass.bus.async_fire(f"event_{domain}_reloaded", context=call.context) - hass.helpers.service.async_register_admin_service( - domain, SERVICE_RELOAD, _reload_config - ) + async_register_admin_service(hass, domain, SERVICE_RELOAD, _reload_config) def setup_reload_service( From dc8e87a6f70439f9830d93d03c53d6ff098a4861 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 12:40:28 +0100 Subject: [PATCH 0642/1054] Exclude hidden entities from alexa (#68555) --- .../components/alexa/smart_home_http.py | 5 +- tests/components/alexa/__init__.py | 39 +++++------ tests/components/alexa/test_capabilities.py | 10 +-- tests/components/alexa/test_entities.py | 66 ++++++++++++++++++- tests/components/alexa/test_smart_home.py | 36 +++++----- tests/components/alexa/test_state_report.py | 26 ++++---- 6 files changed, 123 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 95e13adfbd9..6a953a9f9d4 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -68,7 +68,10 @@ class AlexaConfig(AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False return not auxiliary_entity diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 053100d2e00..1f0854c5102 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -3,8 +3,10 @@ import re from unittest.mock import Mock from uuid import uuid4 -from homeassistant.components.alexa import config, smart_home +from homeassistant.components.alexa import config, smart_home, smart_home_http +from homeassistant.components.alexa.const import CONF_ENDPOINT, CONF_FILTER, CONF_LOCALE from homeassistant.core import Context, callback +from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -13,7 +15,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" TEST_LOCALE = "en-US" -class MockConfig(config.AbstractConfig): +class MockConfig(smart_home_http.AlexaConfig): """Mock Alexa config.""" entity_config = { @@ -26,7 +28,14 @@ class MockConfig(config.AbstractConfig): def __init__(self, hass): """Mock Alexa config.""" - super().__init__(hass) + super().__init__( + hass, + { + CONF_ENDPOINT: TEST_URL, + CONF_FILTER: entityfilter.FILTER_SCHEMA({}), + CONF_LOCALE: TEST_LOCALE, + }, + ) self._store = Mock(spec_set=config.AlexaConfigStore) @property @@ -34,25 +43,11 @@ class MockConfig(config.AbstractConfig): """Return if config supports auth.""" return True - @property - def endpoint(self): - """Endpoint for report state.""" - return TEST_URL - - @property - def locale(self): - """Return config locale.""" - return TEST_LOCALE - @callback def user_identifier(self): """Return an identifier for the user that represents this config.""" return "mock-user-id" - def should_expose(self, entity_id): - """If an entity should be exposed.""" - return True - @callback def async_invalidate_access_token(self): """Invalidate access token.""" @@ -65,9 +60,9 @@ class MockConfig(config.AbstractConfig): """Accept a grant.""" -def get_default_config(): +def get_default_config(hass): """Return a MockConfig instance.""" - return MockConfig(None) + return MockConfig(hass) def get_new_request(namespace, name, endpoint=None): @@ -117,7 +112,7 @@ async def assert_request_calls_service( calls = async_mock_service(hass, domain, service_name) msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) await hass.async_block_till_done() @@ -142,7 +137,7 @@ async def assert_request_fails( domain, service_name = service_not_called.split(".") call = async_mock_service(hass, domain, service_name) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert not call @@ -201,7 +196,7 @@ async def reported_properties(hass, endpoint, return_full_response=False): assertions about the properties. """ request = get_new_request("Alexa", "ReportState", endpoint) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() if return_full_response: return msg diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d24849e1006..3f76c6bee04 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -55,7 +55,7 @@ async def test_api_adjust_brightness(hass, adjust): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -85,7 +85,7 @@ async def test_api_set_color_rgb(hass): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -111,7 +111,7 @@ async def test_api_set_color_temperature(hass): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -139,7 +139,7 @@ async def test_api_decrease_color_temp(hass, result, initial): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -167,7 +167,7 @@ async def test_api_increase_color_temp(hass, result, initial): call_light = async_mock_service(hass, "light", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 54e48df8e8e..7b2a455be92 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -3,6 +3,8 @@ from unittest.mock import patch from homeassistant.components.alexa import smart_home from homeassistant.const import __version__ +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityCategory from . import get_default_config, get_new_request @@ -13,7 +15,63 @@ async def test_unsupported_domain(hass): hass.states.async_set("woz.boop", "on", {"friendly_name": "Boop Woz"}) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) + + assert "event" in msg + msg = msg["event"] + + assert not msg["payload"]["endpoints"] + + +async def test_categorized_hidden_entities(hass): + """Discovery ignores hidden and categorized entities.""" + entity_registry = er.async_get(hass) + request = get_new_request("Alexa.Discovery", "Discover") + + entity_entry1 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_config_id", + suggested_object_id="config_switch", + entity_category=EntityCategory.CONFIG, + ) + entity_entry2 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_diagnostic_id", + suggested_object_id="diagnostic_switch", + entity_category=EntityCategory.DIAGNOSTIC, + ) + entity_entry3 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_system_id", + suggested_object_id="system_switch", + entity_category=EntityCategory.SYSTEM, + ) + entity_entry4 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_integration_id", + suggested_object_id="hidden_integration_switch", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_user_id", + suggested_object_id="hidden_user_switch", + hidden_by=er.RegistryEntryHider.USER, + ) + + # These should not show up in the sync request + hass.states.async_set(entity_entry1.entity_id, "on") + hass.states.async_set(entity_entry2.entity_id, "something_else") + hass.states.async_set(entity_entry3.entity_id, "blah") + hass.states.async_set(entity_entry4.entity_id, "foo") + hass.states.async_set(entity_entry5.entity_id, "bar") + + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -27,7 +85,7 @@ async def test_serialize_discovery(hass): hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -51,7 +109,9 @@ async def test_serialize_discovery_recovers(hass, caplog): "homeassistant.components.alexa.capabilities.AlexaPowerController.serialize_discovery", side_effect=TypeError, ): - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message( + hass, get_default_config(hass), request + ) assert "event" in msg msg = msg["event"] diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7ebba26113d..9fb6584fae3 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -121,7 +121,7 @@ async def test_wrong_version(hass): msg["directive"]["header"]["payloadVersion"] = "2" with pytest.raises(AssertionError): - await smart_home.async_handle_message(hass, get_default_config(), msg) + await smart_home.async_handle_message(hass, get_default_config(hass), msg) async def discovery_test(device, hass, expected_endpoints=1): @@ -131,7 +131,7 @@ async def discovery_test(device, hass, expected_endpoints=1): # setup test devices hass.states.async_set(*device) - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -2308,7 +2308,7 @@ async def test_api_entity_not_exists(hass): call_switch = async_mock_service(hass, "switch", "turn_on") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -2323,7 +2323,7 @@ async def test_api_entity_not_exists(hass): async def test_api_function_not_implemented(hass): """Test api call that is not implemented to us.""" request = get_new_request("Alexa.HAHAAH", "Sweet") - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) assert "event" in msg msg = msg["event"] @@ -2347,7 +2347,7 @@ async def test_api_accept_grant(hass): } # setup test devices - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) await hass.async_block_till_done() assert "event" in msg @@ -2400,7 +2400,9 @@ async def test_logging_request(hass, events): """Test that we log requests.""" context = Context() request = get_new_request("Alexa.Discovery", "Discover") - await smart_home.async_handle_message(hass, get_default_config(), request, context) + await smart_home.async_handle_message( + hass, get_default_config(hass), request, context + ) # To trigger event listener await hass.async_block_till_done() @@ -2420,7 +2422,9 @@ async def test_logging_request_with_entity(hass, events): """Test that we log requests.""" context = Context() request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") - await smart_home.async_handle_message(hass, get_default_config(), request, context) + await smart_home.async_handle_message( + hass, get_default_config(hass), request, context + ) # To trigger event listener await hass.async_block_till_done() @@ -2446,7 +2450,7 @@ async def test_disabled(hass): call_switch = async_mock_service(hass, "switch", "turn_on") msg = await smart_home.async_handle_message( - hass, get_default_config(), request, enabled=False + hass, get_default_config(hass), request, enabled=False ) await hass.async_block_till_done() @@ -2630,7 +2634,7 @@ async def test_range_unsupported_domain(hass): request["directive"]["header"]["instance"] = "switch.speed" msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -2651,7 +2655,7 @@ async def test_mode_unsupported_domain(hass): request["directive"]["header"]["instance"] = "switch.direction" msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3393,7 +3397,7 @@ async def test_media_player_eq_bands_not_supported(hass): ) request["directive"]["payload"] = {"bands": [{"name": "BASS", "value": -2}]} msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3410,7 +3414,7 @@ async def test_media_player_eq_bands_not_supported(hass): "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] } msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3427,7 +3431,7 @@ async def test_media_player_eq_bands_not_supported(hass): "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] } msg = await smart_home.async_handle_message( - hass, get_default_config(), request, context + hass, get_default_config(hass), request, context ) assert "event" in msg @@ -3928,7 +3932,9 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="rtsp://example.local", ): - msg = await smart_home.async_handle_message(hass, get_default_config(), request) + msg = await smart_home.async_handle_message( + hass, get_default_config(hass), request + ) await hass.async_block_till_done() assert "event" in msg @@ -3982,7 +3988,7 @@ async def test_api_message_sets_authorized(hass): msg = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") async_mock_service(hass, "switch", "turn_on") - config = get_default_config() + config = get_default_config(hass) config._store.set_authorized.assert_not_called() await smart_home.async_handle_message(hass, config, msg) config._store.set_authorized.assert_called_once_with(True) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 06c7d051798..bd47a80c18c 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -21,7 +21,7 @@ async def test_report_state(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -66,7 +66,7 @@ async def test_report_state_fail(hass, aioclient_mock, caplog): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -100,7 +100,7 @@ async def test_report_state_timeout(hass, aioclient_mock, caplog): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -134,7 +134,7 @@ async def test_report_state_retry(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_contact", @@ -162,7 +162,7 @@ async def test_report_state_unsets_authorized_on_error(hass, aioclient_mock): {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - config = get_default_config() + config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) hass.states.async_set( @@ -191,7 +191,7 @@ async def test_report_state_unsets_authorized_on_access_token_error( {"friendly_name": "Test Contact Sensor", "device_class": "door"}, ) - config = get_default_config() + config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) @@ -226,7 +226,7 @@ async def test_report_state_instance(hass, aioclient_mock): }, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "fan.test_fan", @@ -296,7 +296,7 @@ async def test_send_add_or_update_message(hass, aioclient_mock): "zwave.bla", # Unsupported ] await state_report.async_send_add_or_update_message( - hass, get_default_config(), entities + hass, get_default_config(hass), entities ) assert len(aioclient_mock.mock_calls) == 1 @@ -323,7 +323,7 @@ async def test_send_delete_message(hass, aioclient_mock): ) await state_report.async_send_delete_message( - hass, get_default_config(), ["binary_sensor.test_contact", "zwave.bla"] + hass, get_default_config(hass), ["binary_sensor.test_contact", "zwave.bla"] ) assert len(aioclient_mock.mock_calls) == 1 @@ -349,7 +349,7 @@ async def test_doorbell_event(hass, aioclient_mock): {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_doorbell", @@ -407,7 +407,7 @@ async def test_doorbell_event_fail(hass, aioclient_mock, caplog): {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_doorbell", @@ -441,7 +441,7 @@ async def test_doorbell_event_timeout(hass, aioclient_mock, caplog): {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, ) - await state_report.async_enable_proactive_mode(hass, get_default_config()) + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) hass.states.async_set( "binary_sensor.test_doorbell", @@ -464,7 +464,7 @@ async def test_doorbell_event_timeout(hass, aioclient_mock, caplog): async def test_proactive_mode_filter_states(hass, aioclient_mock): """Test all the cases that filter states.""" aioclient_mock.post(TEST_URL, text="", status=202) - config = get_default_config() + config = get_default_config(hass) await state_report.async_enable_proactive_mode(hass, config) # First state should report From ff7d5c92d5d28771b5e7f3d899223ffcf2db760e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 12:42:45 +0100 Subject: [PATCH 0643/1054] Exclude hidden entities from cloud (#68557) --- .../components/cloud/alexa_config.py | 5 ++- .../components/cloud/google_config.py | 5 ++- tests/components/cloud/test_alexa_config.py | 34 +++++++++++++---- tests/components/cloud/test_google_config.py | 38 +++++++++++++++---- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 11b122e2b5a..cf52b458a28 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -133,7 +133,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 7988a648901..e2b21ffc56d 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -124,7 +124,10 @@ class CloudGoogleConfig(AbstractConfig): entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get(entity_id): - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 9e24eaa764d..ced2a664763 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -6,8 +6,8 @@ import pytest from homeassistant.components.alexa import errors from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -44,6 +44,20 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): suggested_object_id="system_light", entity_category="system", ) + entity_entry4 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_integration_id", + suggested_object_id="hidden_integration_light", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_user_id", + suggested_object_id="hidden_user_light", + hidden_by=er.RegistryEntryHider.USER, + ) entity_conf = {"should_expose": False} await cloud_prefs.async_update( @@ -61,20 +75,26 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) + assert not conf.should_expose(entity_entry4.entity_id) + assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) + assert not conf.should_expose(entity_entry4.entity_id) + assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not conf.should_expose(entity_entry1.entity_id) assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) + assert not conf.should_expose(entity_entry4.entity_id) + assert not conf.should_expose(entity_entry5.entity_id) assert "alexa" not in hass.config.components await cloud_prefs.async_update( @@ -324,7 +344,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -334,7 +354,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -344,7 +364,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, { "action": "update", "entity_id": "light.kitchen", @@ -359,7 +379,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]}, ) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 478fa22f66c..1fbff368602 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -9,7 +9,7 @@ from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, State -from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -141,7 +141,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): ): # Created entity hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -150,7 +150,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # Removed entity hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "remove", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -159,7 +159,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # Entity registry updated with relevant changes hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, { "action": "update", "entity_id": "light.kitchen", @@ -172,7 +172,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # Entity registry updated with non-relevant changes hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "update", "entity_id": "light.kitchen", "changes": ["icon"]}, ) await hass.async_block_till_done() @@ -182,7 +182,7 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): # When hass is not started yet we wait till started hass.state = CoreState.starting hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": "light.kitchen"}, ) await hass.async_block_till_done() @@ -243,6 +243,20 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): suggested_object_id="system_light", entity_category="system", ) + entity_entry4 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_integration_id", + suggested_object_id="hidden_integration_light", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "light", + "test", + "light_hidden_user_id", + suggested_object_id="hidden_user_light", + hidden_by=er.RegistryEntryHider.USER, + ) entity_conf = {"should_expose": False} await cloud_prefs.async_update( @@ -254,25 +268,33 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): state_config = State(entity_entry1.entity_id, "on") state_diagnostic = State(entity_entry2.entity_id, "on") state_system = State(entity_entry3.entity_id, "on") + state_hidden_integration = State(entity_entry4.entity_id, "on") + state_hidden_user = State(entity_entry5.entity_id, "on") assert not mock_conf.should_expose(state) assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_system) + assert not mock_conf.should_expose(state_hidden_integration) + assert not mock_conf.should_expose(state_hidden_user) entity_conf["should_expose"] = True assert mock_conf.should_expose(state) - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_system) + assert not mock_conf.should_expose(state_hidden_integration) + assert not mock_conf.should_expose(state_hidden_user) entity_conf["should_expose"] = None assert mock_conf.should_expose(state) - # config and diagnostic entities should not be exposed + # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) assert not mock_conf.should_expose(state_system) + assert not mock_conf.should_expose(state_hidden_integration) + assert not mock_conf.should_expose(state_hidden_user) await cloud_prefs.async_update( google_default_expose=["sensor"], From dc0c3a4d2dde52c4bb485e8b9758d517e1141703 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 12:46:53 +0100 Subject: [PATCH 0644/1054] Exclude hidden entities from google_assistant (#68554) --- .../components/google_assistant/http.py | 5 ++++- .../google_assistant/test_google_assistant.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index f068dabe47a..3f3db1f2b5b 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -126,7 +126,10 @@ class GoogleConfig(AbstractConfig): entity_registry = er.async_get(self.hass) registry_entry = entity_registry.async_get(state.entity_id) if registry_entry: - auxiliary_entity = registry_entry.entity_category is not None + auxiliary_entity = ( + registry_entry.entity_category is not None + or registry_entry.hidden_by is not None + ) else: auxiliary_entity = False diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 0a916c1e184..fe86aae4b1a 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -21,6 +21,7 @@ from homeassistant.components import ( from homeassistant.components.climate import const as climate from homeassistant.components.humidifier import const as humidifier from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.helpers import entity_registry as er from . import DEMO_DEVICES @@ -151,11 +152,27 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): suggested_object_id="system_switch", entity_category="system", ) + entity_entry4 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_integration_id", + suggested_object_id="hidden_integration_switch", + hidden_by=er.RegistryEntryHider.INTEGRATION, + ) + entity_entry5 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_hidden_user_id", + suggested_object_id="hidden_user_switch", + hidden_by=er.RegistryEntryHider.USER, + ) # These should not show up in the sync request hass_fixture.states.async_set(entity_entry1.entity_id, "on") hass_fixture.states.async_set(entity_entry2.entity_id, "something_else") hass_fixture.states.async_set(entity_entry3.entity_id, "blah") + hass_fixture.states.async_set(entity_entry4.entity_id, "foo") + hass_fixture.states.async_set(entity_entry5.entity_id, "bar") reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} From 790cab2f95645c7f082609d94d1dc5bd8f72a373 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 23 Mar 2022 12:50:32 +0100 Subject: [PATCH 0645/1054] Remove useless async_setup from AndroidTV (#68561) --- homeassistant/components/androidtv/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 0c76b4454f1..c1875a883e3 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -3,7 +3,7 @@ import os from adb_shell.auth.keygen import keygen from androidtv.adb_manager.adb_manager_sync import ADBPythonSync -from androidtv.setup_async import setup +from androidtv.setup_async import setup as async_androidtv_setup from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,7 +18,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.typing import ConfigType from .const import ( ANDROID_DEV, @@ -83,7 +82,7 @@ async def async_connect_androidtv( _setup_androidtv, hass, config ) - aftv = await setup( + aftv = await async_androidtv_setup( config[CONF_HOST], config[CONF_PORT], adbkey, @@ -110,11 +109,6 @@ async def async_connect_androidtv( return aftv, None -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Android TV integration.""" - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android TV platform.""" From 4fd0ed2474f24e8467750dd5a1d10af4d3225d51 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 13:03:14 +0100 Subject: [PATCH 0646/1054] Bump aiohue to 4.4.0 (#68556) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/fixtures/v2_resources.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index bf6e7f06abd..e8f91539b5c 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.3.0"], + "requirements": ["aiohue==4.4.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 4047f66c811..6fd3829ee94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.3.0 +aiohue==4.4.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1c79379eb8..906d3b95e67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -147,7 +147,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.3.0 +aiohue==4.4.0 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index 806dcecfacf..c3f03d8f48a 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -460,7 +460,7 @@ "model_id": "BSB002", "product_archetype": "bridge_v2", "product_name": "Philips hue", - "software_version": "1.48.1948086000" + "software_version": "1.50.1950111030" }, "services": [ { From cdc78ee129060e98b99cebb7549c79d19ec727bf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Mar 2022 14:47:43 +0100 Subject: [PATCH 0647/1054] Update isort to 5.10.1 (#68564) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07c0f4295ff..7e7df964082 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,7 +45,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.10.0 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index f14739e1f04..90405b873dd 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,7 +7,7 @@ flake8-comprehensions==3.8.0 flake8-docstrings==1.6.0 flake8-noqa==1.2.1 flake8==4.0.1 -isort==5.10.0 +isort==5.10.1 mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 From d3809e4a09af7fa273ae56da66e718998ce23c43 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 15:36:50 +0100 Subject: [PATCH 0648/1054] Update group strings (#68571) --- homeassistant/components/group/strings.json | 13 ++++++--- .../components/group/translations/en.json | 29 +++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index fb343f9006a..0b7388b293b 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -14,36 +14,41 @@ "data": { "all": "All entities", "entities": "Members", + "hide_members": "Hide members", "name": "Name" } }, "cover": { + "title": "[%key:component::group::config::step::user::title%]", "data": { - "title": "[%key:component::group::config::step::user::title%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, "fan": { + "title": "[%key:component::group::config::step::user::title%]", "data": { - "title": "[%key:component::group::config::step::user::title%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, "light": { "description": "[%key:component::group::config::step::binary_sensor::description%]", + "title": "[%key:component::group::config::step::user::title%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", - "title": "[%key:component::group::config::step::user::title%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, "media_player": { + "title": "[%key:component::group::config::step::user::title%]", "data": { - "title": "[%key:component::group::config::step::user::title%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } } diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index a8c9e09dd0b..eec26861897 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -5,6 +5,7 @@ "data": { "all": "All entities", "entities": "Members", + "hide_members": "Hide members", "name": "Name" }, "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", @@ -13,32 +14,36 @@ "cover": { "data": { "entities": "Members", - "name": "Name", - "title": "New Group" - } + "hide_members": "Hide members", + "name": "Name" + }, + "title": "New Group" }, "fan": { "data": { "entities": "Members", - "name": "Name", - "title": "New Group" - } + "hide_members": "Hide members", + "name": "Name" + }, + "title": "New Group" }, "light": { "data": { "all": "All entities", "entities": "Members", - "name": "Name", - "title": "New Group" + "hide_members": "Hide members", + "name": "Name" }, - "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", + "title": "New Group" }, "media_player": { "data": { "entities": "Members", - "name": "Name", - "title": "New Group" - } + "hide_members": "Hide members", + "name": "Name" + }, + "title": "New Group" }, "user": { "data": { From a50bac5cc2bfcde48d1d7cde99f8261786dfc6ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 16:34:44 +0100 Subject: [PATCH 0649/1054] Make initial group config flow step a menu (#68565) --- .../components/derivative/config_flow.py | 9 +- homeassistant/components/group/config_flow.py | 60 +++++------ homeassistant/components/group/strings.json | 9 +- .../components/group/translations/en.json | 9 +- .../components/integration/config_flow.py | 9 +- .../components/switch_as_x/config_flow.py | 22 ++--- .../components/threshold/config_flow.py | 5 +- homeassistant/data_entry_flow.py | 2 +- .../helpers/helper_config_entry_flow.py | 99 ++++++++++++++----- .../integration/config_flow.py | 9 +- tests/components/group/test_config_flow.py | 22 ++--- 11 files changed, 164 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index ccb44b1963b..6d38a3cd4a2 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -77,9 +78,13 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep(CONFIG_SCHEMA) +} -OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(OPTIONS_SCHEMA) +} class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index dafb43924a7..ca356ee70f3 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Group integration.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Callable, Mapping from typing import Any, cast import voluptuous as vol @@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -61,43 +62,44 @@ LIGHT_CONFIG_SCHEMA = vol.Schema( ).extend(LIGHT_OPTIONS_SCHEMA.schema) -INITIAL_STEP_SCHEMA = vol.Schema( - { - vol.Required("group_type"): selector.selector( - { - "select": { - "options": [ - "binary_sensor", - "cover", - "fan", - "light", - "media_player", - ] - } - } - ) - } -) +GROUP_TYPES = ["binary_sensor", "cover", "fan", "light", "media_player"] @callback -def choose_config_step(options: dict[str, Any]) -> str: - """Return next step_id when group_type is selected.""" +def choose_options_step(options: dict[str, Any]) -> str: + """Return next step_id for options flow according to group_type.""" return cast(str, options["group_type"]) -CONFIG_FLOW = { - "user": HelperFlowStep(INITIAL_STEP_SCHEMA, next_step=choose_config_step), - "binary_sensor": HelperFlowStep(BINARY_SENSOR_CONFIG_SCHEMA), - "cover": HelperFlowStep(basic_group_config_schema("cover")), - "fan": HelperFlowStep(basic_group_config_schema("fan")), - "light": HelperFlowStep(LIGHT_CONFIG_SCHEMA), - "media_player": HelperFlowStep(basic_group_config_schema("media_player")), +def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]]: + """Set group type.""" + + @callback + def _set_group_type(user_input: dict[str, Any]) -> dict[str, Any]: + """Add group type to user input.""" + return {"group_type": group_type, **user_input} + + return _set_group_type + + +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowMenuStep(GROUP_TYPES), + "binary_sensor": HelperFlowStep( + BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor") + ), + "cover": HelperFlowStep( + basic_group_config_schema("cover"), set_group_type("cover") + ), + "fan": HelperFlowStep(basic_group_config_schema("fan"), set_group_type("fan")), + "light": HelperFlowStep(LIGHT_CONFIG_SCHEMA, set_group_type("light")), + "media_player": HelperFlowStep( + basic_group_config_schema("media_player"), set_group_type("media_player") + ), } -OPTIONS_FLOW = { - "init": HelperFlowStep(None, next_step=choose_config_step), +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(None, next_step=choose_options_step), "binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA), "cover": HelperFlowStep(basic_group_options_schema("cover")), "fan": HelperFlowStep(basic_group_options_schema("fan")), diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 0b7388b293b..440ec7740ab 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -4,8 +4,13 @@ "step": { "user": { "title": "New Group", - "data": { - "group_type": "Group type" + "description": "Select group type", + "menu_options": { + "binary_sensor": "Binary sensor group", + "cover": "Cover group", + "fan": "Fan group", + "light": "Light group", + "media_player": "Media player group" } }, "binary_sensor": { diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index eec26861897..57a24b0bdba 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -46,8 +46,13 @@ "title": "New Group" }, "user": { - "data": { - "group_type": "Group type" + "description": "Select group type", + "menu_options": { + "binary_sensor": "Binary sensor group", + "cover": "Cover group", + "fan": "Fan group", + "light": "Light group", + "media_player": "Media player group" }, "title": "New Group" } diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index 76379a89002..bf9ad853205 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -87,9 +88,13 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep(CONFIG_SCHEMA) +} -OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(OPTIONS_SCHEMA) +} class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 800e056cd26..6425d212f10 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -7,16 +7,18 @@ from typing import Any import voluptuous as vol from homeassistant.const import CONF_ENTITY_ID, Platform -from homeassistant.helpers import ( - entity_registry as er, - helper_config_entry_flow, - selector, +from homeassistant.helpers import entity_registry as er, selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowMenuStep, + HelperFlowStep, + wrapped_entity_config_entry_title, ) from .const import CONF_TARGET_DOMAIN, DOMAIN -CONFIG_FLOW = { - "user": helper_config_entry_flow.HelperFlowStep( +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep( vol.Schema( { vol.Required(CONF_ENTITY_ID): selector.selector( @@ -41,9 +43,7 @@ CONFIG_FLOW = { } -class SwitchAsXConfigFlowHandler( - helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN -): +class SwitchAsXConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): """Handle a config flow for Switch as X.""" config_flow = CONFIG_FLOW @@ -58,6 +58,4 @@ class SwitchAsXConfigFlowHandler( options[CONF_ENTITY_ID], hidden_by=er.RegistryEntryHider.INTEGRATION ) - return helper_config_entry_flow.wrapped_entity_config_entry_title( - self.hass, options[CONF_ENTITY_ID] - ) + return wrapped_entity_config_entry_title(self.hass, options[CONF_ENTITY_ID]) diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 01e5364284a..36d4c5e6239 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, HelperFlowError, + HelperFlowMenuStep, HelperFlowStep, ) @@ -43,11 +44,11 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW = { +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { "user": HelperFlowStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) } -OPTIONS_FLOW = { +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { "init": HelperFlowStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) } diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 080ef86fac6..628a89dd89b 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -83,7 +83,7 @@ class FlowResult(TypedDict, total=False): result: Any last_step: bool | None options: Mapping[str, Any] - menu_options: list[str] | Mapping[str, Any] + menu_options: list[str] | dict[str, str] @callback diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index b9e35d9d336..f64b2463bdd 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -5,7 +5,8 @@ from abc import abstractmethod from collections.abc import Callable, Mapping import copy from dataclasses import dataclass -from typing import Any +import types +from typing import Any, cast import voluptuous as vol @@ -42,13 +43,21 @@ class HelperFlowStep: next_step: Callable[[dict[str, Any]], str | None] = lambda _: None +@dataclass +class HelperFlowMenuStep: + """Define a helper config or options flow menu step.""" + + # Menu options + options: list[str] | dict[str, str] + + class HelperCommonFlowHandler: """Handle a config or options flow for helper.""" def __init__( self, handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, - flow: dict[str, HelperFlowStep], + flow: dict[str, HelperFlowStep | HelperFlowMenuStep], config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" @@ -60,24 +69,31 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a step.""" - next_step_id: str = step_id + if isinstance(self._flow[step_id], HelperFlowStep): + return await self._async_form_step(step_id, user_input) + return await self._async_menu_step(step_id, user_input) - if user_input is not None and self._flow[next_step_id].schema is not None: + async def _async_form_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a form step.""" + form_step: HelperFlowStep = cast(HelperFlowStep, self._flow[step_id]) + + if user_input is not None and form_step.schema is not None: # Do extra validation of user input try: - user_input = self._flow[next_step_id].validate_user_input(user_input) + user_input = form_step.validate_user_input(user_input) except HelperFlowError as exc: - return self._show_next_step(next_step_id, exc, user_input) + return self._show_next_step(step_id, exc, user_input) if user_input is not None: # User input was validated successfully, update options self._options.update(user_input) - if self._flow[next_step_id].next_step and ( - user_input is not None or self._flow[next_step_id].schema is None - ): + next_step_id: str = step_id + if form_step.next_step and (user_input is not None or form_step.schema is None): # Get next step - next_step_id_or_end_flow = self._flow[next_step_id].next_step(self._options) + next_step_id_or_end_flow = form_step.next_step(self._options) if next_step_id_or_end_flow is None: # Flow done, create entry or update config entry options return self._handler.async_create_entry(data=self._options) @@ -92,11 +108,13 @@ class HelperCommonFlowHandler: error: HelperFlowError | None = None, user_input: dict[str, Any] | None = None, ) -> FlowResult: - """Show step for next step.""" + """Show form for next step.""" + form_step: HelperFlowStep = cast(HelperFlowStep, self._flow[next_step_id]) + options = dict(self._options) if user_input: options.update(user_input) - if (data_schema := self._flow[next_step_id].schema) and data_schema.schema: + if (data_schema := form_step.schema) and data_schema.schema: # Make a copy of the schema with suggested values set to saved options schema = {} for key, val in data_schema.schema.items(): @@ -115,12 +133,22 @@ class HelperCommonFlowHandler: step_id=next_step_id, data_schema=data_schema, errors=errors ) + async def _async_menu_step( + self, step_id: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a menu step.""" + form_step: HelperFlowMenuStep = cast(HelperFlowMenuStep, self._flow[step_id]) + return self._handler.async_show_menu( + step_id=step_id, + menu_options=form_step.options, + ) + class HelperConfigFlowHandler(config_entries.ConfigFlow): """Handle a config flow for helper integrations.""" - config_flow: dict[str, HelperFlowStep] - options_flow: dict[str, HelperFlowStep] | None = None + config_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] + options_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] | None = None VERSION = 1 @@ -146,7 +174,7 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): # Create flow step methods for each step defined in the flow schema for step in cls.config_flow: - setattr(cls, f"async_step_{step}", cls._async_step) + setattr(cls, f"async_step_{step}", cls._async_step(step)) def __init__(self) -> None: """Initialize config flow.""" @@ -160,12 +188,19 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): """Return options flow support for this handler.""" return cls.options_flow is not None - async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle a config flow step.""" - step_id = self.cur_step["step_id"] if self.cur_step else "user" - result = await self._common_handler.async_step(step_id, user_input) + @staticmethod + def _async_step(step_id: str) -> Callable: + """Generate a step handler.""" - return result + async def _async_step( + self: HelperConfigFlowHandler, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a config flow step.""" + # pylint: disable-next=protected-access + result = await self._common_handler.async_step(step_id, user_input) + return result + + return _async_step # pylint: disable-next=no-self-use @abstractmethod @@ -224,13 +259,25 @@ class HelperOptionsFlowHandler(config_entries.OptionsFlow): self._async_options_flow_finished = async_options_flow_finished for step in options_flow: - setattr(self, f"async_step_{step}", self._async_step) + setattr( + self, + f"async_step_{step}", + types.MethodType(self._async_step(step), self), + ) - async def _async_step(self, user_input: dict[str, Any] | None = None) -> FlowResult: - """Handle an options flow step.""" - # pylint: disable-next=unsubscriptable-object # self.cur_step is a dict - step_id = self.cur_step["step_id"] if self.cur_step else "init" - return await self._common_handler.async_step(step_id, user_input) + @staticmethod + def _async_step(step_id: str) -> Callable: + """Generate a step handler.""" + + async def _async_step( + self: HelperConfigFlowHandler, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle an options flow step.""" + # pylint: disable-next=protected-access + result = await self._common_handler.async_step(step_id, user_input) + return result + + return _async_step @callback def async_create_entry( # pylint: disable=arguments-differ diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index ad0e8a3eb90..8f4db82c77c 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowMenuStep, HelperFlowStep, ) @@ -29,9 +30,13 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW = {"user": HelperFlowStep(CONFIG_SCHEMA)} +CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "user": HelperFlowStep(CONFIG_SCHEMA) +} -OPTIONS_FLOW = {"init": HelperFlowStep(OPTIONS_SCHEMA)} +OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { + "init": HelperFlowStep(OPTIONS_SCHEMA) +} class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index f5a9fb22222..9c2657f0001 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -6,7 +6,11 @@ import pytest from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, + RESULT_TYPE_MENU, +) from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -42,12 +46,11 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None + assert result["type"] == RESULT_TYPE_MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"group_type": group_type}, + {"next_step_id": group_type}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM @@ -130,12 +133,11 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None + assert result["type"] == RESULT_TYPE_MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"group_type": group_type}, + {"next_step_id": group_type}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM @@ -251,13 +253,11 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - assert get_suggested(result["data_schema"].schema, "group_type") is None + assert result["type"] == RESULT_TYPE_MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"group_type": group_type}, + {"next_step_id": group_type}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM From c3f0bd45a43cd9f4bf544a59975f3accaed47ff1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 23 Mar 2022 19:34:22 +0100 Subject: [PATCH 0650/1054] Bump motionblinds to 0.6.2 (#68570) --- homeassistant/components/motion_blinds/cover.py | 4 +++- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index ffb2a03ddc6..1812aa6fba2 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -49,6 +49,7 @@ POSITION_DEVICE_MAP = { BlindType.Curtain: CoverDeviceClass.CURTAIN, BlindType.CurtainLeft: CoverDeviceClass.CURTAIN, BlindType.CurtainRight: CoverDeviceClass.CURTAIN, + BlindType.SkylightBlind: CoverDeviceClass.SHADE, } TILT_DEVICE_MAP = { @@ -57,6 +58,7 @@ TILT_DEVICE_MAP = { BlindType.DoubleRoller: CoverDeviceClass.SHADE, BlindType.VerticalBlind: CoverDeviceClass.BLIND, BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND, + BlindType.VerticalBlindRight: CoverDeviceClass.BLIND, } TDBU_DEVICE_MAP = { @@ -138,7 +140,7 @@ async def async_setup_entry( else: _LOGGER.warning( - "Blind type '%s' not yet supported, " "assuming RollerBlind", + "Blind type '%s' not yet supported, assuming RollerBlind", blind.blind_type, ) entities.append( diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 40fd9cafad4..adfe7722ca4 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.1"], + "requirements": ["motionblinds==0.6.2"], "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 6fd3829ee94..20326d84d1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,7 +1014,7 @@ mitemp_bt==0.0.5 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.6.1 +motionblinds==0.6.2 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 906d3b95e67..31862584662 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -677,7 +677,7 @@ minio==5.0.10 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.6.1 +motionblinds==0.6.2 # homeassistant.components.motioneye motioneye-client==0.3.12 From df6cc94b258f5f9923362112a6af6d48f322a870 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 23 Mar 2022 19:35:58 +0100 Subject: [PATCH 0651/1054] Cleanup SamsungTV following dependency bump (#68562) * send_command -> send_commands * Remove TODO Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 9 +---- .../components/samsungtv/test_media_player.py | 38 +++++++++---------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index b6f9aebd86b..a135ca27db5 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -468,7 +468,7 @@ class SamsungTVWSBridge(SamsungTVBridge): for _ in range(retry_count + 1): try: if remote := await self._async_get_remote(): - await remote.send_command(commands) + await remote.send_commands(commands) break except ( BrokenPipeError, @@ -712,12 +712,7 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): timeout=TIMEOUT_WEBSOCKET, ) try: - # pylint:disable=[fixme] - # TODO: remove secondary timeout when library is bumped - # See https://github.com/xchwarze/samsung-tv-ws-api/pull/82 - await asyncio.wait_for( - self._remote.start_listening(), TIMEOUT_WEBSOCKET - ) + await self._remote.start_listening() except (WebSocketException, AsyncioTimeoutError, OSError) as err: LOGGER.debug( "Failed to get remote for %s: %s", self.host, err.__repr__() diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index dac0c43bb1b..c62c20f2f36 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -593,7 +593,7 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command = Mock(side_effect=WebSocketException("Boom")) + remotews.send_commands = Mock(side_effect=WebSocketException("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -619,7 +619,7 @@ async def test_send_key_websocketexception_encrypted( async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command = Mock(side_effect=OSError("Boom")) + remotews.send_commands = Mock(side_effect=OSError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -744,20 +744,20 @@ async def test_turn_off_websocket( ): await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command.reset_mock() + remotews.send_commands.reset_mock() assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remotews.send_command.call_count == 1 - commands = remotews.send_command.call_args_list[0].args[0] + assert remotews.send_commands.call_count == 1 + commands = remotews.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], SendRemoteKey) assert commands[0].params["DataOfCmd"] == "KEY_POWER" # commands not sent : power off in progress - remotews.send_command.reset_mock() + remotews.send_commands.reset_mock() assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -769,7 +769,7 @@ async def test_turn_off_websocket( True, ) assert "TV is powering off, not sending launch_app command" in caplog.text - remotews.send_command.assert_not_called() + remotews.send_commands.assert_not_called() async def test_turn_off_websocket_frame( @@ -783,14 +783,14 @@ async def test_turn_off_websocket_frame( ): await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command.reset_mock() + remotews.send_commands.reset_mock() assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key called - assert remotews.send_command.call_count == 1 - commands = remotews.send_command.call_args_list[0].args[0] + assert remotews.send_commands.call_count == 1 + commands = remotews.send_commands.call_args_list[0].args[0] assert len(commands) == 3 assert isinstance(commands[0], SendRemoteKey) assert commands[0].params["Cmd"] == "Press" @@ -1157,7 +1157,7 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for play_media.""" await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command.reset_mock() + remotews.send_commands.reset_mock() assert await hass.services.async_call( DOMAIN, @@ -1169,8 +1169,8 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: }, True, ) - assert remotews.send_command.call_count == 1 - commands = remotews.send_command.call_args_list[0].args[0] + assert remotews.send_commands.call_count == 1 + commands = remotews.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], ChannelEmitCommand) assert commands[0].params["data"]["appId"] == "3201608010191" @@ -1179,7 +1179,7 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for select_source.""" await setup_samsungtv(hass, MOCK_CONFIGWS) - remotews.send_command.reset_mock() + remotews.send_commands.reset_mock() assert await hass.services.async_call( DOMAIN, @@ -1187,8 +1187,8 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "Deezer"}, True, ) - assert remotews.send_command.call_count == 1 - commands = remotews.send_command.call_args_list[0].args[0] + assert remotews.send_commands.call_count == 1 + commands = remotews.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], ChannelEmitCommand) assert commands[0].params["data"]["appId"] == "3201608010191" @@ -1203,7 +1203,7 @@ async def test_websocket_unsupported_remote_control( assert entry.data[CONF_METHOD] == METHOD_WEBSOCKET assert entry.data[CONF_PORT] == 8001 - remotews.send_command.reset_mock() + remotews.send_commands.reset_mock() assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True @@ -1217,8 +1217,8 @@ async def test_websocket_unsupported_remote_control( ) # key called - assert remotews.send_command.call_count == 1 - commands = remotews.send_command.call_args_list[0].args[0] + assert remotews.send_commands.call_count == 1 + commands = remotews.send_commands.call_args_list[0].args[0] assert len(commands) == 1 assert isinstance(commands[0], SendRemoteKey) assert commands[0].params["DataOfCmd"] == "KEY_POWER" From bcfd9eeff2d7155086ea18b6c882eaa1d1ea9d88 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 23 Mar 2022 14:37:15 -0400 Subject: [PATCH 0652/1054] Remove deprecated yaml config from Awair (#68572) --- homeassistant/components/awair/config_flow.py | 17 ------ homeassistant/components/awair/sensor.py | 39 +----------- tests/components/awair/test_config_flow.py | 60 +------------------ 3 files changed, 4 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 4c4ccad8f52..1eff98dd78d 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -17,23 +17,6 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, conf: dict): - """Import a configuration from config.yaml.""" - if self._async_current_entries(): - return self.async_abort(reason="already_setup") - - user, error = await self._check_connection(conf[CONF_ACCESS_TOKEN]) - if error is not None: - return self.async_abort(reason=error) - - await self.async_set_unique_id(user.email) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=f"{user.email} ({user.user_id})", - data={CONF_ACCESS_TOKEN: conf[CONF_ACCESS_TOKEN]}, - ) - async def async_step_user(self, user_input: dict | None = None): """Handle a flow initialized by the user.""" errors = {} diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 1859ace1d4c..ddf76c0e93d 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -3,22 +3,14 @@ from __future__ import annotations from python_awair.air_data import AirData from python_awair.devices import AwairDevice -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_CONNECTIONS, - ATTR_NAME, - CONF_ACCESS_TOKEN, -) +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_CONNECTIONS, ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import AwairDataUpdateCoordinator, AwairResult @@ -31,37 +23,12 @@ from .const import ( ATTRIBUTION, DOMAIN, DUST_ALIASES, - LOGGER, SENSOR_TYPE_SCORE, SENSOR_TYPES, SENSOR_TYPES_DUST, AwairSensorEntityDescription, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ACCESS_TOKEN): cv.string}, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Awair configuration from YAML.""" - LOGGER.warning( - "Loading Awair via platform setup is deprecated; Please remove it from your configuration" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 84b92229161..8afe9a1c701 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -6,7 +6,7 @@ from python_awair.exceptions import AuthError, AwairError from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE @@ -82,64 +82,6 @@ async def test_no_devices_error(hass): assert result["reason"] == "no_devices_found" -async def test_import(hass): - """Test config.yaml import.""" - - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_ACCESS_TOKEN: CONFIG[CONF_ACCESS_TOKEN]}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "foo@bar.com (32406)" - assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] - assert result["result"].unique_id == UNIQUE_ID - - -async def test_import_aborts_on_api_error(hass): - """Test config.yaml imports on api error.""" - - with patch("python_awair.AwairClient.query", side_effect=AwairError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_ACCESS_TOKEN: CONFIG[CONF_ACCESS_TOKEN]}, - ) - - assert result["type"] == "abort" - assert result["reason"] == "unknown" - - -async def test_import_aborts_if_configured(hass): - """Test config import doesn't re-import unnecessarily.""" - - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, - ): - MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG).add_to_hass( - hass - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_ACCESS_TOKEN: CONFIG[CONF_ACCESS_TOKEN]}, - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_setup" - - async def test_reauth(hass): """Test reauth flow.""" with patch( From b4bb35d4de348bc682c4aeba63d5d69858bdc1df Mon Sep 17 00:00:00 2001 From: JonasClever <47122456+JonasClever@users.noreply.github.com> Date: Wed, 23 Mar 2022 19:45:54 +0100 Subject: [PATCH 0653/1054] Fronius - change the unit of capacity from Ah to Wh (#68543) --- homeassistant/components/fronius/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 0e00bbd135e..0f2a819818c 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -631,13 +631,13 @@ STORAGE_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="capacity_maximum", name="Capacity maximum", - native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + native_unit_of_measurement=ENERGY_WATT_HOUR, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="capacity_designed", name="Capacity designed", - native_unit_of_measurement=ELECTRIC_CHARGE_AMPERE_HOURS, + native_unit_of_measurement=ENERGY_WATT_HOUR, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( From de3d402930b2c447a2497e055c45dbe3b4589f16 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Wed, 23 Mar 2022 19:50:28 +0100 Subject: [PATCH 0654/1054] Add Lock platform to wallbox (#68414) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/wallbox/__init__.py | 37 ++++-- homeassistant/components/wallbox/const.py | 1 + homeassistant/components/wallbox/lock.py | 76 +++++++++++ tests/components/wallbox/__init__.py | 2 + tests/components/wallbox/const.py | 1 + tests/components/wallbox/test_lock.py | 129 +++++++++++++++++++ 6 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/wallbox/lock.py create mode 100644 tests/components/wallbox/test_lock.py diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 2a3da958cb9..af1c59e42c1 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -22,6 +22,7 @@ from ...helpers.entity import DeviceInfo from .const import ( CONF_CURRENT_VERSION_KEY, CONF_DATA_KEY, + CONF_LOCKED_UNLOCKED_KEY, CONF_MAX_CHARGING_CURRENT_KEY, CONF_NAME_KEY, CONF_PART_NUMBER_KEY, @@ -33,7 +34,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SENSOR, Platform.NUMBER] +PLATFORMS = [Platform.SENSOR, Platform.NUMBER, Platform.LOCK] UPDATE_INTERVAL = 30 @@ -70,6 +71,10 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): raise InvalidAuth from wallbox_connection_error raise ConnectionError from wallbox_connection_error + async def async_validate_input(self) -> None: + """Get new sensor data for Wallbox component.""" + await self.hass.async_add_executor_job(self._validate) + def _get_data(self) -> dict[str, Any]: """Get new sensor data for Wallbox component.""" try: @@ -78,12 +83,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): data[CONF_MAX_CHARGING_CURRENT_KEY] = data[CONF_DATA_KEY][ CONF_MAX_CHARGING_CURRENT_KEY ] + data[CONF_LOCKED_UNLOCKED_KEY] = data[CONF_DATA_KEY][ + CONF_LOCKED_UNLOCKED_KEY + ] return data except requests.exceptions.HTTPError as wallbox_connection_error: raise ConnectionError from wallbox_connection_error + async def _async_update_data(self) -> dict[str, Any]: + """Get new sensor data for Wallbox component.""" + return await self.hass.async_add_executor_job(self._get_data) + def _set_charging_current(self, charging_current: float) -> None: """Set maximum charging current for Wallbox.""" try: @@ -101,14 +113,23 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]): ) await self.async_request_refresh() - async def _async_update_data(self) -> dict[str, Any]: - """Get new sensor data for Wallbox component.""" - data = await self.hass.async_add_executor_job(self._get_data) - return data + def _set_lock_unlock(self, lock: bool) -> None: + """Set wallbox to locked or unlocked.""" + try: + self._authenticate() + if lock: + self._wallbox.lockCharger(self._station) + else: + self._wallbox.unlockCharger(self._station) + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 403: + raise InvalidAuth from wallbox_connection_error + raise ConnectionError from wallbox_connection_error - async def async_validate_input(self) -> None: - """Get new sensor data for Wallbox component.""" - await self.hass.async_add_executor_job(self._validate) + async def async_set_lock_unlock(self, lock: bool) -> None: + """Set wallbox to locked or unlocked.""" + await self.hass.async_add_executor_job(self._set_lock_unlock, lock) + await self.async_request_refresh() async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 263df7b4924..c5be4d1606d 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -18,6 +18,7 @@ CONF_PART_NUMBER_KEY = "part_number" CONF_SOFTWARE_KEY = "software" CONF_MAX_AVAILABLE_POWER_KEY = "max_available_power" CONF_MAX_CHARGING_CURRENT_KEY = "max_charging_current" +CONF_LOCKED_UNLOCKED_KEY = "locked" CONF_NAME_KEY = "name" CONF_STATE_OF_CHARGE_KEY = "state_of_charge" CONF_STATUS_DESCRIPTION_KEY = "status_description" diff --git a/homeassistant/components/wallbox/lock.py b/homeassistant/components/wallbox/lock.py new file mode 100644 index 00000000000..1d12f086abe --- /dev/null +++ b/homeassistant/components/wallbox/lock.py @@ -0,0 +1,76 @@ +"""Home Assistant component for accessing the Wallbox Portal API. The lock component creates a lock entity.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity, LockEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import InvalidAuth, WallboxCoordinator, WallboxEntity +from .const import ( + CONF_DATA_KEY, + CONF_LOCKED_UNLOCKED_KEY, + CONF_SERIAL_NUMBER_KEY, + DOMAIN, +) + +LOCK_TYPES: dict[str, LockEntityDescription] = { + CONF_LOCKED_UNLOCKED_KEY: LockEntityDescription( + key=CONF_LOCKED_UNLOCKED_KEY, + name="Locked/Unlocked", + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Create wallbox lock entities in HASS.""" + coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] + # Check if the user is authorized to lock, if so, add lock component + try: + await coordinator.async_set_lock_unlock( + coordinator.data[CONF_LOCKED_UNLOCKED_KEY] + ) + except InvalidAuth: + return + + async_add_entities( + [ + WallboxLock(coordinator, entry, description) + for ent in coordinator.data + if (description := LOCK_TYPES.get(ent)) + ] + ) + + +class WallboxLock(WallboxEntity, LockEntity): + """Representation of a wallbox lock.""" + + def __init__( + self, + coordinator: WallboxCoordinator, + entry: ConfigEntry, + description: LockEntityDescription, + ) -> None: + """Initialize a Wallbox lock.""" + + super().__init__(coordinator) + self.entity_description = description + self._attr_name = f"{entry.title} {description.name}" + self._attr_unique_id = f"{description.key}-{coordinator.data[CONF_DATA_KEY][CONF_SERIAL_NUMBER_KEY]}" + + @property + def is_locked(self) -> bool: + """Return the status of the lock.""" + return self.coordinator.data[CONF_LOCKED_UNLOCKED_KEY] # type: ignore[no-any-return] + + async def async_lock(self, **kwargs: Any) -> None: + """Lock charger.""" + await self.coordinator.async_set_lock_unlock(True) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock charger.""" + await self.coordinator.async_set_lock_unlock(False) diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 5effb103d7f..fe9aa1ef3d6 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -12,6 +12,7 @@ from homeassistant.components.wallbox.const import ( CONF_CHARGING_SPEED_KEY, CONF_CURRENT_VERSION_KEY, CONF_DATA_KEY, + CONF_LOCKED_UNLOCKED_KEY, CONF_MAX_AVAILABLE_POWER_KEY, CONF_MAX_CHARGING_CURRENT_KEY, CONF_NAME_KEY, @@ -38,6 +39,7 @@ test_response = json.loads( CONF_NAME_KEY: "WallboxName", CONF_DATA_KEY: { CONF_MAX_CHARGING_CURRENT_KEY: 24, + CONF_LOCKED_UNLOCKED_KEY: False, CONF_SERIAL_NUMBER_KEY: "20000", CONF_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E", CONF_SOFTWARE_KEY: {CONF_CURRENT_VERSION_KEY: "5.5.10"}, diff --git a/tests/components/wallbox/const.py b/tests/components/wallbox/const.py index 9777602f6c9..0647af09884 100644 --- a/tests/components/wallbox/const.py +++ b/tests/components/wallbox/const.py @@ -6,6 +6,7 @@ CONF_ERROR = "error" CONF_STATUS = "status" CONF_MOCK_NUMBER_ENTITY_ID = "number.mock_title_max_charging_current" +CONF_MOCK_LOCK_ENTITY_ID = "lock.mock_title_locked_unlocked" CONF_MOCK_SENSOR_CHARGING_SPEED_ID = "sensor.mock_title_charging_speed" CONF_MOCK_SENSOR_CHARGING_POWER_ID = "sensor.mock_title_charging_power" CONF_MOCK_SENSOR_MAX_AVAILABLE_POWER = "sensor.mock_title_max_available_power" diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py new file mode 100644 index 00000000000..7e24f997825 --- /dev/null +++ b/tests/components/wallbox/test_lock.py @@ -0,0 +1,129 @@ +"""Test Wallbox Lock component.""" +import json + +import pytest +import requests_mock + +from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK +from homeassistant.components.wallbox import CONF_LOCKED_UNLOCKED_KEY +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.components.wallbox import ( + entry, + setup_integration, + setup_integration_read_only, +) +from tests.components.wallbox.const import ( + CONF_ERROR, + CONF_JWT, + CONF_MOCK_LOCK_ENTITY_ID, + CONF_STATUS, + CONF_TTL, + CONF_USER_ID, +) + +authorisation_response = json.loads( + json.dumps( + { + CONF_JWT: "fakekeyhere", + CONF_USER_ID: 12345, + CONF_TTL: 145656758, + CONF_ERROR: "false", + CONF_STATUS: 200, + } + ) +) + + +async def test_wallbox_lock_class(hass: HomeAssistant): + """Test wallbox lock class.""" + + await setup_integration(hass) + + state = hass.states.get(CONF_MOCK_LOCK_ENTITY_ID) + assert state + assert state.state == "unlocked" + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + json=authorisation_response, + status_code=200, + ) + mock_request.put( + "https://api.wall-box.com/v2/charger/12345", + json=json.loads(json.dumps({CONF_LOCKED_UNLOCKED_KEY: False})), + status_code=200, + ) + + await hass.services.async_call( + "lock", + SERVICE_LOCK, + { + ATTR_ENTITY_ID: CONF_MOCK_LOCK_ENTITY_ID, + }, + blocking=True, + ) + + await hass.services.async_call( + "lock", + SERVICE_UNLOCK, + { + ATTR_ENTITY_ID: CONF_MOCK_LOCK_ENTITY_ID, + }, + blocking=True, + ) + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_lock_class_connection_error(hass: HomeAssistant): + """Test wallbox lock class connection error.""" + + await setup_integration(hass) + + with requests_mock.Mocker() as mock_request: + mock_request.get( + "https://api.wall-box.com/auth/token/user", + json=authorisation_response, + status_code=200, + ) + mock_request.put( + "https://api.wall-box.com/v2/charger/12345", + json=json.loads(json.dumps({CONF_LOCKED_UNLOCKED_KEY: False})), + status_code=404, + ) + + with pytest.raises(ConnectionError): + await hass.services.async_call( + "lock", + SERVICE_LOCK, + { + ATTR_ENTITY_ID: CONF_MOCK_LOCK_ENTITY_ID, + }, + blocking=True, + ) + with pytest.raises(ConnectionError): + await hass.services.async_call( + "lock", + SERVICE_UNLOCK, + { + ATTR_ENTITY_ID: CONF_MOCK_LOCK_ENTITY_ID, + }, + blocking=True, + ) + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_wallbox_lock_class_authentication_error(hass: HomeAssistant): + """Test wallbox lock not loaded on authentication error.""" + + await setup_integration_read_only(hass) + + state = hass.states.get(CONF_MOCK_LOCK_ENTITY_ID) + + assert state is None + + await hass.config_entries.async_unload(entry.entry_id) From 83983bc875445d7147cb98e70f1214c6ed270da9 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 23 Mar 2022 19:59:53 +0100 Subject: [PATCH 0655/1054] Motion request update till stop (#68580) * update untill stop * fixes * fix spelling --- .../components/motion_blinds/const.py | 1 + .../components/motion_blinds/cover.py | 59 ++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index 696a00ff79e..c9346a3b86e 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -25,3 +25,4 @@ SERVICE_SET_ABSOLUTE_POSITION = "set_absolute_position" UPDATE_INTERVAL = 600 UPDATE_INTERVAL_FAST = 60 +UPDATE_INTERVAL_MOVING = 5 diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 1812aa6fba2..a2cbaabedd0 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,4 +1,5 @@ """Support for Motion Blinds using their WLAN API.""" +from datetime import timedelta import logging from motionblinds import DEVICE_TYPES_WIFI, BlindType @@ -11,7 +12,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -19,7 +20,9 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_point_in_time, track_point_in_time from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util from .const import ( ATTR_ABSOLUTE_POSITION, @@ -31,6 +34,7 @@ from .const import ( KEY_VERSION, MANUFACTURER, SERVICE_SET_ABSOLUTE_POSITION, + UPDATE_INTERVAL_MOVING, ) _LOGGER = logging.getLogger(__name__) @@ -172,6 +176,8 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._blind = blind self._config_entry = config_entry + self._requesting_position = False + self._previous_positions = [] if blind.device_type in DEVICE_TYPES_WIFI: via_device = () @@ -236,23 +242,69 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._blind.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() + async def async_scheduled_update_request(self, *_): + """Request a state update from the blind at a scheduled point in time.""" + # add the last position to the list and keep the list at max 2 items + self._previous_positions.append(self.current_cover_position) + if len(self._previous_positions) > 2: + del self._previous_positions[: len(self._previous_positions) - 2] + + await self.hass.async_add_executor_job(self._blind.Update_trigger) + self.async_write_ha_state() + + if len(self._previous_positions) < 2 or not all( + self.current_cover_position == prev_position + for prev_position in self._previous_positions + ): + # keep updating the position @UPDATE_INTERVAL_MOVING until the position does not change. + async_track_point_in_time( + self.hass, + self.async_scheduled_update_request, + dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL_MOVING), + ) + else: + self._previous_positions = [] + self._requesting_position = False + + @callback + def async_scheduled_update_request_callback(self, now): + """Request a state update from the blind at a scheduled point in time using async_scheduled_update_request.""" + self.hass.loop.create_task(self.async_scheduled_update_request()) + + def request_position_till_stop(self): + """Request the position of the blind every UPDATE_INTERVAL_MOVING seconds until it stops moving.""" + self._previous_positions = [] + if self._requesting_position or self.current_cover_position is None: + return + + self._requesting_position = True + track_point_in_time( + self.hass, + self.async_scheduled_update_request_callback, + dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL_MOVING), + ) + def open_cover(self, **kwargs): """Open the cover.""" self._blind.Open() + self.request_position_till_stop() def close_cover(self, **kwargs): """Close cover.""" self._blind.Close() + self.request_position_till_stop() def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] self._blind.Set_position(100 - position) + self.request_position_till_stop() def set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position (see TDBU).""" position = kwargs[ATTR_ABSOLUTE_POSITION] self._blind.Set_position(100 - position) + self.request_position_till_stop() def stop_cover(self, **kwargs): """Stop the cover.""" @@ -345,15 +397,18 @@ class MotionTDBUDevice(MotionPositionDevice): def open_cover(self, **kwargs): """Open the cover.""" self._blind.Open(motor=self._motor_key) + self.request_position_till_stop() def close_cover(self, **kwargs): """Close cover.""" self._blind.Close(motor=self._motor_key) + self.request_position_till_stop() def set_cover_position(self, **kwargs): """Move the cover to a specific scaled position.""" position = kwargs[ATTR_POSITION] self._blind.Set_scaled_position(100 - position, motor=self._motor_key) + self.request_position_till_stop() def set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position.""" @@ -364,6 +419,8 @@ class MotionTDBUDevice(MotionPositionDevice): 100 - position, motor=self._motor_key, width=target_width ) + self.request_position_till_stop() + def stop_cover(self, **kwargs): """Stop the cover.""" self._blind.Stop(motor=self._motor_key) From 871b7a4a96f031e76c345c2a244c753ed228118b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 20:18:06 +0100 Subject: [PATCH 0656/1054] Bump aiohue to version 4.4.1 (#68579) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index e8f91539b5c..d3b492f3b9e 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.0"], + "requirements": ["aiohue==4.4.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 20326d84d1c..8beb41ff1cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.0 +aiohue==4.4.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31862584662..3b44280da5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -147,7 +147,7 @@ aiohomekit==0.7.16 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.0 +aiohue==4.4.1 # homeassistant.components.homewizard aiohwenergy==0.8.0 From d2dc9e6cbe24c088faaf1d9e728c30c7573c22aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Mar 2022 09:38:34 -1000 Subject: [PATCH 0657/1054] Filter IPv6 addresses from AppleTV zeroconf discovery (#68530) --- .../components/apple_tv/config_flow.py | 3 +++ .../components/apple_tv/strings.json | 1 + .../components/apple_tv/translations/en.json | 7 ++----- tests/components/apple_tv/test_config_flow.py | 19 +++++++++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index c4853359cc6..8e8e6006895 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util.network import is_ipv6_address from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN @@ -166,6 +167,8 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle device found via zeroconf.""" host = discovery_info.host + if is_ipv6_address(host): + return self.async_abort(reason="ipv6_not_supported") self._async_abort_entries_match({CONF_ADDRESS: host}) service_type = discovery_info.type[:-1] # Remove leading . name = discovery_info.name.replace(f".{service_type}.", "") diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json index 3ea47ba3d8a..e25c596f786 100644 --- a/homeassistant/components/apple_tv/strings.json +++ b/homeassistant/components/apple_tv/strings.json @@ -48,6 +48,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "ipv6_not_supported": "IPv6 is not supported.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index db16129ca2a..f455d590d79 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -2,13 +2,12 @@ "config": { "abort": { "already_configured": "Device is already configured", - "already_configured_device": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", "device_not_found": "Device was not found during discovery, please try adding it again.", "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.", - "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", + "ipv6_not_supported": "IPv6 is not supported.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful", "setup_failed": "Failed to set up device.", @@ -18,7 +17,6 @@ "already_configured": "Device is already configured", "invalid_auth": "Invalid authentication", "no_devices_found": "No devices found on the network", - "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", "unknown": "Unexpected error" }, "flow_title": "{name} ({type})", @@ -72,6 +70,5 @@ "description": "Configure general device settings" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index ca617026d94..6efb4820564 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1066,3 +1066,22 @@ async def test_option_start_off(hass): assert result2["type"] == "create_entry" assert config_entry.options[CONF_START_OFF] + + +async def test_zeroconf_rejects_ipv6(hass): + """Test zeroconf discovery rejects ipv6.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + port=None, + type="_touch-able._tcp.local.", + name="dmapid._touch-able._tcp.local.", + properties={"CtlN": "Apple TV"}, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "ipv6_not_supported" From ccd8c7d5f8fd8b461fa79d5987141ead1be2e742 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 21:06:10 +0100 Subject: [PATCH 0658/1054] Hue aggregated control for grouped lights (#68566) --- homeassistant/components/hue/v2/group.py | 78 ++++++------------------ tests/components/hue/test_light_v2.py | 53 ++++++---------- 2 files changed, 38 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 162ef58d320..4816ce55231 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -1,7 +1,6 @@ """Support for Hue groups (room/zone).""" from __future__ import annotations -import asyncio from typing import Any from aiohue.v2 import HueBridgeV2 @@ -146,7 +145,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): } async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the light on.""" + """Turn the grouped_light on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) xy_color = kwargs.get(ATTR_XY_COLOR) color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) @@ -155,38 +154,16 @@ class GroupedHueLight(HueBaseEntity, LightEntity): if flash is not None: await self.async_set_flash(flash) - # flash can not be sent with other commands at the same time return - # NOTE: a grouped_light can only handle turn on/off - # To set other features, you'll have to control the attached lights - if ( - brightness is None - and xy_color is None - and color_temp is None - and transition is None - and flash is None - ): - await self.bridge.async_request_call( - self.controller.set_state, id=self.resource.id, on=True - ) - return - - # redirect all other feature commands to underlying lights - # note that this silently ignores params sent to light that are not supported - await asyncio.gather( - *[ - self.bridge.async_request_call( - self.api.lights.set_state, - light.id, - on=True, - brightness=brightness if light.supports_dimming else None, - color_xy=xy_color if light.supports_color else None, - color_temp=color_temp if light.supports_color_temperature else None, - transition_time=transition, - ) - for light in self.controller.get_lights(self.resource.id) - ] + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=True, + brightness=brightness, + color_xy=xy_color, + color_temp=color_temp, + transition_time=transition, ) async def async_turn_off(self, **kwargs: Any) -> None: @@ -199,38 +176,19 @@ class GroupedHueLight(HueBaseEntity, LightEntity): # flash can not be sent with other commands at the same time return - # NOTE: a grouped_light can only handle turn on/off - # To set other features, you'll have to control the attached lights - if transition is None: - await self.bridge.async_request_call( - self.controller.set_state, id=self.resource.id, on=False - ) - return - - # redirect all other feature commands to underlying lights - await asyncio.gather( - *[ - self.bridge.async_request_call( - self.api.lights.set_state, - light.id, - on=False, - transition_time=transition, - ) - for light in self.controller.get_lights(self.resource.id) - ] + await self.bridge.async_request_call( + self.controller.set_state, + id=self.resource.id, + on=False, + transition_time=transition, ) async def async_set_flash(self, flash: str) -> None: """Send flash command to light.""" - await asyncio.gather( - *[ - self.bridge.async_request_call( - self.api.lights.set_flash, - id=light.id, - short=flash == FLASH_SHORT, - ) - for light in self.controller.get_lights(self.resource.id) - ] + await self.bridge.async_request_call( + self.controller.set_flash, + id=self.resource.id, + short=flash == FLASH_SHORT, ) @callback diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index f0265233e4e..eba5726229e 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -372,31 +372,24 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): blocking=True, ) - # PUT request should have been sent to ALL group lights with correct params - assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): - assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is True - assert ( - mock_bridge_v2.mock_requests[index]["json"]["dimming"]["brightness"] == 100 - ) - assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["x"] == 0.123 - assert mock_bridge_v2.mock_requests[index]["json"]["color"]["xy"]["y"] == 0.123 - assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 - ) + # PUT request should have been sent to group_light with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is True + assert mock_bridge_v2.mock_requests[0]["json"]["dimming"]["brightness"] == 100 + assert mock_bridge_v2.mock_requests[0]["json"]["color"]["xy"]["x"] == 0.123 + assert mock_bridge_v2.mock_requests[0]["json"]["color"]["xy"]["y"] == 0.123 + assert mock_bridge_v2.mock_requests[0]["json"]["dynamics"]["duration"] == 200 # Now generate update events by emitting the json we've sent as incoming events - for index, light_id in enumerate( - [ - "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "8015b17f-8336-415b-966a-b364bd082397", - ] - ): + for light_id in [ + "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "8015b17f-8336-415b-966a-b364bd082397", + ]: event = { "id": light_id, "type": "light", - **mock_bridge_v2.mock_requests[index]["json"], + **mock_bridge_v2.mock_requests[0]["json"], } mock_bridge_v2.api.emit_event("update", event) await hass.async_block_till_done() @@ -452,13 +445,10 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): blocking=True, ) - # PUT request should have been sent to ALL group lights with correct params - assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): - assert mock_bridge_v2.mock_requests[index]["json"]["on"]["on"] is False - assert ( - mock_bridge_v2.mock_requests[index]["json"]["dynamics"]["duration"] == 200 - ) + # PUT request should have been sent to group_light with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"]["on"]["on"] is False + assert mock_bridge_v2.mock_requests[0]["json"]["dynamics"]["duration"] == 200 # Test sending short flash effect to a grouped light mock_bridge_v2.mock_requests.clear() @@ -494,12 +484,9 @@ async def test_grouped_lights(hass, mock_bridge_v2, v2_resources_test_data): blocking=True, ) - # PUT request should have been sent to ALL group lights with correct params - assert len(mock_bridge_v2.mock_requests) == 3 - for index in range(0, 3): - assert ( - mock_bridge_v2.mock_requests[index]["json"]["alert"]["action"] == "breathe" - ) + # PUT request should have been sent to grouped_light with correct params + assert len(mock_bridge_v2.mock_requests) == 1 + assert mock_bridge_v2.mock_requests[0]["json"]["alert"]["action"] == "breathe" # Test sending flash effect in turn_off call mock_bridge_v2.mock_requests.clear() From 8293430e250c60ff2f832e5fef305e3c68e01b0b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 23 Mar 2022 16:13:27 -0400 Subject: [PATCH 0659/1054] Redact user codes from zwave_js diagnostics (#68515) * Redact user codes from zwave_js diagnostics * simplify test * Remove unused logic * revert change and make all inputs to ZwaveValueID optional * revert change and make all inputs to ZwaveValueID optional * Remove unused diagnostics data from fixture and test location redaction * Add empty ZwaveValueID check * Improve coverage * Simplify post_init check * Use dataclasses.astuple for checks instead --- .../components/zwave_js/diagnostics.py | 56 +- .../components/zwave_js/discovery.py | 48 +- .../zwave_js/discovery_data_template.py | 13 +- homeassistant/components/zwave_js/entity.py | 2 +- homeassistant/components/zwave_js/helpers.py | 16 + tests/components/zwave_js/conftest.py | 6 + .../fixtures/config_entry_diagnostics.json | 1936 +++++++++++++++++ tests/components/zwave_js/test_diagnostics.py | 21 +- tests/components/zwave_js/test_helpers.py | 10 + 9 files changed, 2064 insertions(+), 44 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/config_entry_diagnostics.json create mode 100644 tests/components/zwave_js/test_helpers.py diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index dd88f2b6d07..f323ee0f5f9 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -1,12 +1,16 @@ """Provides diagnostics for Z-Wave JS.""" from __future__ import annotations +from dataclasses import astuple from typing import Any from zwave_js_server.client import Client +from zwave_js_server.const import CommandClass from zwave_js_server.dump import dump_msgs from zwave_js_server.model.node import Node, NodeDataType +from zwave_js_server.model.value import ValueDataType +from homeassistant.components.diagnostics.const import REDACTED from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL @@ -17,9 +21,43 @@ from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity_registry import async_entries_for_device, async_get from .const import DATA_CLIENT, DOMAIN -from .helpers import get_home_and_node_id_from_device_entry +from .helpers import ZwaveValueID, get_home_and_node_id_from_device_entry -TO_REDACT = {"homeId", "location"} +KEYS_TO_REDACT = {"homeId", "location"} + +VALUES_TO_REDACT = ( + ZwaveValueID(property_="userCode", command_class=CommandClass.USER_CODE), +) + + +def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: + """Redact value of a Z-Wave value.""" + for value_to_redact in VALUES_TO_REDACT: + zwave_value_id = ZwaveValueID( + property_=zwave_value["property"], + command_class=CommandClass(zwave_value["commandClass"]), + endpoint=zwave_value["endpoint"], + property_key=zwave_value.get("propertyKey"), + ) + if all( + redacted_field_val is None or redacted_field_val == zwave_value_field_val + for redacted_field_val, zwave_value_field_val in zip( + astuple(value_to_redact), astuple(zwave_value_id) + ) + ): + return {**zwave_value, "value": REDACTED} + return zwave_value + + +def redact_node_state(node_state: NodeDataType) -> NodeDataType: + """Redact node state.""" + return { + **node_state, + "values": [ + redact_value_of_zwave_value(zwave_value) + for zwave_value in node_state["values"] + ], + } def get_device_entities( @@ -79,10 +117,16 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> list[dict]: """Return diagnostics for a config entry.""" - msgs: list[dict] = await dump_msgs( - config_entry.data[CONF_URL], async_get_clientsession(hass) + msgs: list[dict] = async_redact_data( + await dump_msgs(config_entry.data[CONF_URL], async_get_clientsession(hass)), + KEYS_TO_REDACT, ) - return async_redact_data(msgs, TO_REDACT) + handshake_msgs = msgs[:-1] + network_state = msgs[-1] + network_state["result"]["state"]["nodes"] = [ + redact_node_state(node) for node in network_state["result"]["state"]["nodes"] + ] + return [*handshake_msgs, network_state] async def async_get_device_diagnostics( @@ -104,5 +148,5 @@ async def async_get_device_diagnostics( "maxSchemaVersion": client.version.max_schema_version, }, "entities": entities, - "state": async_redact_data(node.data, TO_REDACT), + "state": redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)), } diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 7d9c235c00a..e11b01dab2b 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -55,8 +55,8 @@ from .discovery_data_template import ( FanValueMapping, FixedFanValueMappingDataTemplate, NumericSensorDataTemplate, - ZwaveValueID, ) +from .helpers import ZwaveValueID class DataclassMustHaveAtLeastOne: @@ -307,7 +307,7 @@ DISCOVERY_SCHEMAS = [ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, data_template=ConfigurableFanValueMappingDataTemplate( configuration_option=ZwaveValueID( - 5, CommandClass.CONFIGURATION, endpoint=0 + property_=5, command_class=CommandClass.CONFIGURATION, endpoint=0 ), configuration_value_to_fan_value_mapping={ 0: FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]), @@ -325,8 +325,8 @@ DISCOVERY_SCHEMAS = [ primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, data_template=CoverTiltDataTemplate( tilt_value_id=ZwaveValueID( - "fibaro", - CommandClass.MANUFACTURER_PROPRIETARY, + property_="fibaro", + command_class=CommandClass.MANUFACTURER_PROPRIETARY, endpoint=0, property_key="venetianBlindsTilt", ) @@ -391,34 +391,36 @@ DISCOVERY_SCHEMAS = [ lookup_table={ # Internal Sensor "A": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), "AF": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), # External Sensor "A2": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=3, ), "A2F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=3, ), # Floor sensor "F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=4, ), }, - dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + dependent_value=ZwaveValueID( + property_=2, command_class=CommandClass.CONFIGURATION, endpoint=0 + ), ), ), # Heatit Z-TRM2fx @@ -438,23 +440,25 @@ DISCOVERY_SCHEMAS = [ lookup_table={ # External Sensor "A2": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), "A2F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=2, ), # Floor sensor "F": ZwaveValueID( - THERMOSTAT_CURRENT_TEMP_PROPERTY, - CommandClass.SENSOR_MULTILEVEL, + property_=THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, endpoint=3, ), }, - dependent_value=ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + dependent_value=ZwaveValueID( + property_=2, command_class=CommandClass.CONFIGURATION, endpoint=0 + ), ), ), # FortrezZ SSA1/SSA2/SSA3 diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index bfcc1ed87a5..622a8222fa7 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -148,6 +148,7 @@ from .const import ( ENTITY_DESC_KEY_TOTAL_INCREASING, ENTITY_DESC_KEY_VOLTAGE, ) +from .helpers import ZwaveValueID METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = { ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES, @@ -226,16 +227,6 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { _LOGGER = logging.getLogger(__name__) -@dataclass -class ZwaveValueID: - """Class to represent a value ID.""" - - property_: str | int - command_class: int - endpoint: int | None = None - property_key: str | int | None = None - - @dataclass class BaseDiscoverySchemaDataTemplate: """Base class for discovery schema data templates.""" @@ -486,7 +477,7 @@ class ConfigurableFanValueMappingDataTemplate( ... data_template=ConfigurableFanValueMappingDataTemplate( configuration_option=ZwaveValueID( - 5, CommandClass.CONFIGURATION, endpoint=0 + property_=5, command_class=CommandClass.CONFIGURATION, endpoint=0 ), configuration_value_to_fan_value_mapping={ 0: FanValueMapping(speeds=[(1,33), (34,66), (67,99)]), diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index a61fc3765c7..c6ed1902568 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -223,7 +223,7 @@ class ZWaveBaseEntity(Entity): value_property: str | int, command_class: int | None = None, endpoint: int | None = None, - value_property_key: int | None = None, + value_property_key: int | str | None = None, add_to_watched_value_ids: bool = True, check_all_endpoints: bool = False, ) -> ZwaveValue | None: diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index a84ddee300f..35f278d4571 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import astuple, dataclass from typing import Any, cast import voluptuous as vol @@ -41,6 +42,21 @@ from .const import ( ) +@dataclass +class ZwaveValueID: + """Class to represent a value ID.""" + + property_: str | int | None = None + command_class: int | None = None + endpoint: int | None = None + property_key: str | int | None = None + + def __post_init__(self) -> None: + """Post initialization check.""" + if all(val is None for val in astuple(self)): + raise ValueError("At least one of the fields must be set.") + + @callback def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None: """Return the value of a ZwaveValue.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a36e3870253..b5ca9f5e611 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -210,6 +210,12 @@ def log_config_state_fixture(): } +@pytest.fixture(name="config_entry_diagnostics", scope="session") +def config_entry_diagnostics_fixture(): + """Load the config entry diagnostics fixture data.""" + return json.loads(load_fixture("zwave_js/config_entry_diagnostics.json")) + + @pytest.fixture(name="multisensor_6_state", scope="session") def multisensor_6_state_fixture(): """Load the multisensor 6 node state fixture data.""" diff --git a/tests/components/zwave_js/fixtures/config_entry_diagnostics.json b/tests/components/zwave_js/fixtures/config_entry_diagnostics.json new file mode 100644 index 00000000000..bdd8f615c27 --- /dev/null +++ b/tests/components/zwave_js/fixtures/config_entry_diagnostics.json @@ -0,0 +1,1936 @@ +[ + { + "type": "version", + "driverVersion": "8.11.6", + "serverVersion": "1.15.0", + "homeId": 1234567890, + "minSchemaVersion": 0, + "maxSchemaVersion": 15 + }, + { + "type": "result", + "success": true, + "messageId": "api-schema-id", + "result": {} + }, + { + "type": "result", + "success": true, + "messageId": "listen-id", + "result": { + "state": { + "driver": { + "logConfig": { + "enabled": true, + "level": "info", + "logToFile": false, + "filename": "/data/store/zwavejs_%DATE%.log", + "forceConsole": true + }, + "statisticsEnabled": true + }, + "controller": { + "libraryVersion": "Z-Wave 6.07", + "type": 1, + "homeId": 1234567890, + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.2", + "manufacturerId": 134, + "productType": 1, + "productId": 90, + "supportedFunctionTypes": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 28, + 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 56, 57, + 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, + 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, 99, 102, 103, + 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, 184, 185, 186, + 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 + ], + "sucNodeId": 1, + "supportsTimers": false, + "isHealNetworkActive": false, + "statistics": { + "messagesTX": 10, + "messagesRX": 734, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0 + }, + "inclusionState": 0 + }, + "nodes": [ + { + "nodeId": 1, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": false, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 90, + "productType": 1, + "firmwareVersion": "1.2", + "deviceConfig": { + "filename": "/data/db/devices/0x0086/zw090.json", + "isEmbedded": true, + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW090", + "description": "Z‐Stick Gen5 USB Controller", + "devices": [ + { + "productType": 1, + "productId": 90 + }, + { + "productType": 257, + "productId": 90 + }, + { + "productType": 513, + "productId": 90 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "reset": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable.\n\nPress and hold the Action Button on Z-Stick for 20 seconds and then release", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/1345/Z%20Stick%20Gen5%20manual%201.pdf" + } + }, + "label": "ZW090", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 1, + "index": 0, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "commandClasses": [] + } + ], + "values": [], + "isFrequentListening": false, + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "deviceClass": { + "basic": { + "key": 2, + "label": "Static Controller" + }, + "generic": { + "key": 2, + "label": "Static Controller" + }, + "specific": { + "key": 1, + "label": "PC Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [32] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0001:0x005a:1.2", + "statistics": { + "commandsTX": 0, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "isControllerNode": true, + "keepAwake": false + }, + { + "nodeId": 29, + "index": 0, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "firmwareVersion": "113.22", + "name": "Front Door Lock", + "location": "Foyer", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 29, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 98, + "name": "Door Lock", + "version": 1, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 1, + "isSecure": true + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 1, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 1, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 1, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**********" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**********" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**********" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + }, + "value": 16 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 89 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "statistics": { + "commandsTX": 25, + "commandsRX": 42, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": 7, + "isControllerNode": false, + "keepAwake": false + } + ] + } + } + } +] diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 84fb401e31b..99475cd63ec 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from zwave_js_server.event import Event +from homeassistant.components.diagnostics.const import REDACTED from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics from homeassistant.components.zwave_js.discovery import async_discover_node_values from homeassistant.components.zwave_js.helpers import get_device_id @@ -17,15 +18,27 @@ from tests.components.diagnostics import ( ) -async def test_config_entry_diagnostics(hass, hass_client, integration): +async def test_config_entry_diagnostics( + hass, hass_client, integration, config_entry_diagnostics +): """Test the config entry level diagnostics data dump.""" with patch( "homeassistant.components.zwave_js.diagnostics.dump_msgs", - return_value=[{"hello": "world"}, {"second": "msg"}], + return_value=config_entry_diagnostics, ): - assert await get_diagnostics_for_config_entry( + diagnostics = await get_diagnostics_for_config_entry( hass, hass_client, integration - ) == [{"hello": "world"}, {"second": "msg"}] + ) + assert len(diagnostics) == 3 + assert diagnostics[0]["homeId"] == REDACTED + nodes = diagnostics[2]["result"]["state"]["nodes"] + for node in nodes: + assert "location" not in node or node["location"] == REDACTED + for value in node["values"]: + if value["commandClass"] == 99 and value["property"] == "userCode": + assert value["value"] == REDACTED + else: + assert value.get("value") != REDACTED async def test_device_diagnostics( diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py new file mode 100644 index 00000000000..290c93fa084 --- /dev/null +++ b/tests/components/zwave_js/test_helpers.py @@ -0,0 +1,10 @@ +"""Test Z-Wave JS helpers module.""" +import pytest + +from homeassistant.components.zwave_js.helpers import ZwaveValueID + + +async def test_empty_zwave_value_id(): + """Test empty ZwaveValueID is invalid.""" + with pytest.raises(ValueError): + ZwaveValueID() From 9ba0475644a54e822d02eff503154180b44d9ab9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 23 Mar 2022 22:12:12 +0100 Subject: [PATCH 0660/1054] Use callback to get app_list in SamsungTV (#68506) Co-authored-by: epenet --- homeassistant/components/samsungtv/bridge.py | 69 +++++++++++-------- .../components/samsungtv/media_player.py | 39 ++++++++--- tests/components/samsungtv/conftest.py | 29 +++++++- tests/components/samsungtv/const.py | 56 ++++++++------- .../components/samsungtv/test_config_flow.py | 3 - .../components/samsungtv/test_media_player.py | 6 +- 6 files changed, 133 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index a135ca27db5..73764d51dc0 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -14,7 +14,11 @@ from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.command import SamsungTVCommand from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote -from samsungtvws.event import MS_ERROR_EVENT +from samsungtvws.event import ( + ED_INSTALLED_APP_EVENT, + MS_ERROR_EVENT, + parse_installed_app, +) from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException @@ -128,6 +132,7 @@ class SamsungTVBridge(ABC): self._reauth_callback: CALLBACK_TYPE | None = None self._reload_callback: CALLBACK_TYPE | None = None self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None + self._app_list_callback: Callable[[dict[str, str]], None] | None = None def register_reauth_callback(self, func: CALLBACK_TYPE) -> None: """Register a callback function.""" @@ -143,6 +148,12 @@ class SamsungTVBridge(ABC): """Register a callback function.""" self._update_config_entry = func + def register_app_list_callback( + self, func: Callable[[dict[str, str]], None] + ) -> None: + """Register app_list callback function.""" + self._app_list_callback = func + @abstractmethod async def async_try_connect(self) -> str: """Try to connect to the TV.""" @@ -151,9 +162,15 @@ class SamsungTVBridge(ABC): async def async_device_info(self) -> dict[str, Any] | None: """Try to gather infos of this TV.""" - @abstractmethod - async def async_get_app_list(self) -> dict[str, str] | None: - """Get installed app list.""" + async def async_request_app_list(self) -> None: + """Request app list.""" + # Overridden in SamsungTVWSBridge + LOGGER.debug( + "App list request is not supported on %s TV: %s", + self.method, + self.host, + ) + self._notify_app_list_callback({}) @abstractmethod async def async_is_on(self) -> bool: @@ -186,6 +203,11 @@ class SamsungTVBridge(ABC): if self._update_config_entry is not None: self._update_config_entry(updates) + def _notify_app_list_callback(self, app_list: dict[str, str]) -> None: + """Notify update config callback.""" + if self._app_list_callback is not None: + self._app_list_callback(app_list) + class SamsungTVLegacyBridge(SamsungTVBridge): """The Bridge for Legacy TVs.""" @@ -206,10 +228,6 @@ class SamsungTVLegacyBridge(SamsungTVBridge): } self._remote: Remote | None = None - async def async_get_app_list(self) -> dict[str, str]: - """Get installed app list.""" - return {} - async def async_is_on(self) -> bool: """Tells if the TV is on.""" return await self.hass.async_add_executor_job(self._is_on) @@ -345,26 +363,10 @@ class SamsungTVWSBridge(SamsungTVBridge): if entry_data: self.token = entry_data.get(CONF_TOKEN) self._rest_api: SamsungTVAsyncRest | None = None - self._app_list: dict[str, str] | None = None self._device_info: dict[str, Any] | None = None self._remote: SamsungTVWSAsyncRemote | None = None self._remote_lock = asyncio.Lock() - async def async_get_app_list(self) -> dict[str, str] | None: - """Get installed app list.""" - if self._app_list is None: - if remote := await self._async_get_remote(): - raw_app_list = await remote.app_list() - self._app_list = { - app["name"]: app["appId"] - for app in sorted( - raw_app_list or [], - key=lambda app: cast(str, app["name"]), - ) - } - LOGGER.debug("Generated app list: %s", self._app_list) - return self._app_list - def _get_device_spec(self, key: str) -> Any | None: """Check if a flag exists in latest device info.""" if not ((info := self._device_info) and (device := info.get("device"))): @@ -456,6 +458,10 @@ class SamsungTVWSBridge(SamsungTVBridge): """Send the launch_app command using websocket protocol.""" await self._async_send_commands([ChannelEmitCommand.launch_app(app_id)]) + async def async_request_app_list(self) -> None: + """Get installed app list.""" + await self._async_send_commands([ChannelEmitCommand.get_installed_app()]) + async def async_send_keys(self, keys: list[str]) -> None: """Send a list of keys using websocket protocol.""" await self._async_send_commands([SendRemoteKey.click(key) for key in keys]) @@ -546,6 +552,17 @@ class SamsungTVWSBridge(SamsungTVBridge): def _remote_event(self, event: str, response: Any) -> None: """Received event from remote websocket.""" + if event == ED_INSTALLED_APP_EVENT: + self._notify_app_list_callback( + { + app["name"]: app["appId"] + for app in sorted( + parse_installed_app(response), + key=lambda app: cast(str, app["name"]), + ) + } + ) + return if event == MS_ERROR_EVENT: # { 'event': 'ms.error', # 'data': {'message': 'unrecognized method value : ms.remote.control'}} @@ -608,10 +625,6 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): self._remote: SamsungTVEncryptedWSAsyncRemote | None = None self._remote_lock = asyncio.Lock() - async def async_get_app_list(self) -> dict[str, str]: - """Get installed app list.""" - return {} - async def async_is_on(self) -> bool: """Tells if the TV is on.""" LOGGER.debug("Checking if TV %s is on using websocket", self.host) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a293321d311..677dbfab66a 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -1,6 +1,7 @@ """Support for interface with an Samsung TV.""" from __future__ import annotations +import asyncio from datetime import datetime, timedelta from typing import Any @@ -75,6 +76,9 @@ SCAN_INTERVAL_PLUS_OFF_TIME = entity_component.DEFAULT_SCAN_INTERVAL + timedelta seconds=5 ) +# Max delay waiting for app_list to return, as some TVs simply ignore the request +APP_LIST_DELAY = 3 + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -119,6 +123,7 @@ class SamsungTVDevice(MediaPlayerEntity): self._attr_device_class = MediaPlayerDeviceClass.TV self._attr_source_list = list(SOURCES) self._app_list: dict[str, str] | None = None + self._app_list_event: asyncio.Event = asyncio.Event() if config_entry.data.get(CONF_METHOD) != METHOD_ENCRYPTED_WEBSOCKET: # Encrypted websockets currently only support ON/OFF status @@ -146,6 +151,18 @@ class SamsungTVDevice(MediaPlayerEntity): self._bridge = bridge self._auth_failed = False self._bridge.register_reauth_callback(self.access_denied) + self._bridge.register_app_list_callback(self._app_list_callback) + + def _update_sources(self) -> None: + self._attr_source_list = list(SOURCES) + if app_list := self._app_list: + self._attr_source_list.extend(app_list) + + def _app_list_callback(self, app_list: dict[str, str]) -> None: + """App list callback.""" + self._app_list = app_list + self._update_sources() + self._app_list_event.set() def access_denied(self) -> None: """Access denied callback.""" @@ -173,14 +190,20 @@ class SamsungTVDevice(MediaPlayerEntity): STATE_ON if await self._bridge.async_is_on() else STATE_OFF ) - if self._attr_state == STATE_ON and self._app_list is None: - self._app_list = {} # Ensure that we don't update it twice in parallel - await self._async_update_app_list() - - async def _async_update_app_list(self) -> None: - self._app_list = await self._bridge.async_get_app_list() - if self._app_list is not None: - self._attr_source_list.extend(self._app_list) + if self._attr_state == STATE_ON and not self._app_list_event.is_set(): + await self._bridge.async_request_app_list() + if self._app_list_event.is_set(): + # The try+wait_for is a bit expensive so we should try not to + # enter it unless we have to (Python 3.11 will have zero cost try) + return + try: + await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY) + except asyncio.TimeoutError as err: + # No need to try again + self._app_list_event.set() + LOGGER.debug( + "Failed to load app list from %s: %s", self._host, err.__repr__() + ) async def _async_launch_app(self, app_id: str) -> None: """Send launch_app to the tv.""" diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 3d9ea74e346..3fc11d7c07a 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -9,11 +9,14 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from samsungctl import Remote from samsungtvws.async_remote import SamsungTVWSAsyncRemote +from samsungtvws.command import SamsungTVCommand from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote +from samsungtvws.event import ED_INSTALLED_APP_EVENT +from samsungtvws.remote import ChannelEmitCommand import homeassistant.util.dt as dt_util -from .const import SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_WIFI +from .const import SAMPLE_DEVICE_INFO_WIFI @pytest.fixture(autouse=True) @@ -26,6 +29,13 @@ def fake_host_fixture() -> None: yield +@pytest.fixture(autouse=True) +def app_list_delay_fixture() -> None: + """Patch APP_LIST_DELAY.""" + with patch("homeassistant.components.samsungtv.media_player.APP_LIST_DELAY", 0): + yield + + @pytest.fixture(name="remote") def remote_fixture() -> Mock: """Patch the samsungctl Remote.""" @@ -56,19 +66,32 @@ def remotews_fixture() -> Mock: remotews = Mock(SamsungTVWSAsyncRemote) remotews.__aenter__ = AsyncMock(return_value=remotews) remotews.__aexit__ = AsyncMock() - remotews.app_list.return_value = SAMPLE_APP_LIST remotews.token = "FAKE_TOKEN" + remotews.app_list_data = None - def _start_listening( + async def _start_listening( ws_event_callback: Callable[[str, Any], Awaitable[None] | None] | None = None ): remotews.ws_event_callback = ws_event_callback + async def _send_commands(commands: list[SamsungTVCommand]): + if ( + len(commands) == 1 + and isinstance(commands[0], ChannelEmitCommand) + and commands[0].params["event"] == "ed.installedApp.get" + and remotews.app_list_data is not None + ): + remotews.raise_mock_ws_event_callback( + ED_INSTALLED_APP_EVENT, + remotews.app_list_data, + ) + def _mock_ws_event_callback(event: str, response: Any): if remotews.ws_event_callback: remotews.ws_event_callback(event, response) remotews.start_listening.side_effect = _start_listening + remotews.send_commands.side_effect = _send_commands remotews.raise_mock_ws_event_callback = Mock(side_effect=_mock_ws_event_callback) with patch( diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 11e92481f21..64c3c6add8e 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -1,4 +1,6 @@ """Constants for the samsungtv tests.""" +from samsungtvws.event import ED_INSTALLED_APP_EVENT + from homeassistant.components.samsungtv.const import CONF_SESSION_ID from homeassistant.const import ( CONF_HOST, @@ -24,30 +26,6 @@ MOCK_ENTRYDATA_ENCRYPTED_WS = { CONF_SESSION_ID: "2", } -SAMPLE_APP_LIST = [ - { - "appId": "111299001912", - "app_type": 2, - "icon": "/opt/share/webappservice/apps_icon/FirstScreen/111299001912/250x250.png", - "is_lock": 0, - "name": "YouTube", - }, - { - "appId": "3201608010191", - "app_type": 2, - "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201608010191/250x250.png", - "is_lock": 0, - "name": "Deezer", - }, - { - "appId": "3201606009684", - "app_type": 2, - "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201606009684/250x250.png", - "is_lock": 0, - "name": "Spotify - Music and Podcasts", - }, -] - SAMPLE_DEVICE_INFO_WIFI = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", "device": { @@ -127,3 +105,33 @@ SAMPLE_DEVICE_INFO_UE48JU6400 = { "type": "Samsung SmartTV", "uri": "https://1.2.3.4:8002/api/v2/", } + +SAMPLE_EVENT_ED_INSTALLED_APP = { + "event": ED_INSTALLED_APP_EVENT, + "from": "host", + "data": { + "data": [ + { + "appId": "111299001912", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/111299001912/250x250.png", + "is_lock": 0, + "name": "YouTube", + }, + { + "appId": "3201608010191", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201608010191/250x250.png", + "is_lock": 0, + "name": "Deezer", + }, + { + "appId": "3201606009684", + "app_type": 2, + "icon": "/opt/share/webappservice/apps_icon/FirstScreen/3201606009684/250x250.png", + "is_lock": 0, + "name": "Spotify - Music and Podcasts", + }, + ] + }, +} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 72f23b01350..567b53646d7 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -55,7 +55,6 @@ from . import setup_samsungtv_entry from .const import ( MOCK_CONFIG_ENCRYPTED_WS, MOCK_ENTRYDATA_ENCRYPTED_WS, - SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_FRAME, ) @@ -910,7 +909,6 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock(return_value=False) - remote.app_list.return_value = SAMPLE_APP_LIST rest_api_class.return_value.rest_device_info = AsyncMock( return_value={ "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", @@ -957,7 +955,6 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock(return_value=False) - remote.app_list.return_value = SAMPLE_APP_LIST rest_api_class.return_value.rest_device_info = AsyncMock( return_value={ "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c62c20f2f36..96b939d08b9 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -72,8 +72,8 @@ import homeassistant.util.dt as dt_util from . import setup_samsungtv_entry from .const import ( MOCK_ENTRYDATA_ENCRYPTED_WS, - SAMPLE_APP_LIST, SAMPLE_DEVICE_INFO_FRAME, + SAMPLE_EVENT_ED_INSTALLED_APP, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -175,7 +175,6 @@ async def test_setup_websocket(hass: HomeAssistant) -> None: remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock() - remote.app_list.return_value = SAMPLE_APP_LIST remote.token = "123456789" remote_class.return_value = remote @@ -213,7 +212,6 @@ async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> Non remote = Mock(SamsungTVWSAsyncRemote) remote.__aenter__ = AsyncMock(return_value=remote) remote.__aexit__ = AsyncMock() - remote.app_list.return_value = SAMPLE_APP_LIST remote.token = "987654321" remote_class.return_value = remote assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) @@ -738,6 +736,7 @@ async def test_turn_off_websocket( hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off.""" + remotews.app_list_data = SAMPLE_EVENT_ED_INSTALLED_APP with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[OSError("Boom"), DEFAULT_MOCK], @@ -1178,6 +1177,7 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for select_source.""" + remotews.app_list_data = SAMPLE_EVENT_ED_INSTALLED_APP await setup_samsungtv(hass, MOCK_CONFIGWS) remotews.send_commands.reset_mock() From 29a43cef0b54785f30223cc5bad202e2ee588ff3 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Mar 2022 16:20:20 -0500 Subject: [PATCH 0661/1054] Add cooldown timer before Sonos resubscriptions (#68521) --- homeassistant/components/sonos/speaker.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 98c6d3bba26..ba4fec0cf57 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -63,6 +63,7 @@ from .media import SonosMedia from .statistics import ActivityStatistics, EventStatistics NEVER_TIME = -1200.0 +RESUB_COOLDOWN_SECONDS = 10.0 EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, @@ -126,6 +127,7 @@ class SonosSpeaker: self._last_event_cache: dict[str, Any] = {} self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name) self.event_stats: EventStatistics = EventStatistics(self.zone_name) + self._resub_cooldown_expires_at: float | None = None # Scheduled callback handles self._poll_timer: Callable | None = None @@ -503,6 +505,16 @@ class SonosSpeaker: @callback def speaker_activity(self, source): """Track the last activity on this speaker, set availability and resubscribe.""" + if self._resub_cooldown_expires_at: + if time.monotonic() < self._resub_cooldown_expires_at: + _LOGGER.debug( + "Activity on %s from %s while in cooldown, ignoring", + self.zone_name, + source, + ) + return + self._resub_cooldown_expires_at = None + _LOGGER.debug("Activity on %s from %s", self.zone_name, source) self._last_activity = time.monotonic() self.activity_stats.activity(source, self._last_activity) @@ -543,6 +555,10 @@ class SonosSpeaker: if not self.available: return + if self._resub_cooldown_expires_at is None and not self.hass.is_stopping: + self._resub_cooldown_expires_at = time.monotonic() + RESUB_COOLDOWN_SECONDS + _LOGGER.debug("Starting resubscription cooldown for %s", self.zone_name) + self.available = False self.async_write_entity_states() From c44d7205cf31d7699ee2b97aa391e07aa58d44b8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 23 Mar 2022 22:43:11 +0100 Subject: [PATCH 0662/1054] Rename HelperFlowStep to HelperFlowFormStep (#68583) --- .../components/derivative/config_flow.py | 10 +++---- homeassistant/components/group/config_flow.py | 28 +++++++++---------- .../components/integration/config_flow.py | 10 +++---- .../components/switch_as_x/config_flow.py | 6 ++-- .../components/threshold/config_flow.py | 10 +++---- .../helpers/helper_config_entry_flow.py | 16 ++++++----- .../integration/config_flow.py | 10 +++---- 7 files changed, 46 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index 6d38a3cd4a2..1447f943a13 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -18,8 +18,8 @@ from homeassistant.const import ( from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowFormStep, HelperFlowMenuStep, - HelperFlowStep, ) from .const import ( @@ -78,12 +78,12 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "user": HelperFlowStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "init": HelperFlowStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) } diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index ca356ee70f3..8b4bf20b0e5 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowFormStep, HelperFlowMenuStep, - HelperFlowStep, ) from . import DOMAIN @@ -82,29 +82,29 @@ def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any] return _set_group_type -CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { "user": HelperFlowMenuStep(GROUP_TYPES), - "binary_sensor": HelperFlowStep( + "binary_sensor": HelperFlowFormStep( BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor") ), - "cover": HelperFlowStep( + "cover": HelperFlowFormStep( basic_group_config_schema("cover"), set_group_type("cover") ), - "fan": HelperFlowStep(basic_group_config_schema("fan"), set_group_type("fan")), - "light": HelperFlowStep(LIGHT_CONFIG_SCHEMA, set_group_type("light")), - "media_player": HelperFlowStep( + "fan": HelperFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")), + "light": HelperFlowFormStep(LIGHT_CONFIG_SCHEMA, set_group_type("light")), + "media_player": HelperFlowFormStep( basic_group_config_schema("media_player"), set_group_type("media_player") ), } -OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "init": HelperFlowStep(None, next_step=choose_options_step), - "binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA), - "cover": HelperFlowStep(basic_group_options_schema("cover")), - "fan": HelperFlowStep(basic_group_options_schema("fan")), - "light": HelperFlowStep(LIGHT_OPTIONS_SCHEMA), - "media_player": HelperFlowStep(basic_group_options_schema("media_player")), +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(None, next_step=choose_options_step), + "binary_sensor": HelperFlowFormStep(BINARY_SENSOR_OPTIONS_SCHEMA), + "cover": HelperFlowFormStep(basic_group_options_schema("cover")), + "fan": HelperFlowFormStep(basic_group_options_schema("fan")), + "light": HelperFlowFormStep(LIGHT_OPTIONS_SCHEMA), + "media_player": HelperFlowFormStep(basic_group_options_schema("media_player")), } diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index bf9ad853205..c9e51fd4f9a 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -18,8 +18,8 @@ from homeassistant.const import ( from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowFormStep, HelperFlowMenuStep, - HelperFlowStep, ) from .const import ( @@ -88,12 +88,12 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "user": HelperFlowStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "init": HelperFlowStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) } diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 6425d212f10..4cf7a001679 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -10,15 +10,15 @@ from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowFormStep, HelperFlowMenuStep, - HelperFlowStep, wrapped_entity_config_entry_title, ) from .const import CONF_TARGET_DOMAIN, DOMAIN -CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "user": HelperFlowStep( +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep( vol.Schema( { vol.Required(CONF_ENTITY_ID): selector.selector( diff --git a/homeassistant/components/threshold/config_flow.py b/homeassistant/components/threshold/config_flow.py index 36d4c5e6239..35a32604334 100644 --- a/homeassistant/components/threshold/config_flow.py +++ b/homeassistant/components/threshold/config_flow.py @@ -11,8 +11,8 @@ from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, HelperFlowError, + HelperFlowFormStep, HelperFlowMenuStep, - HelperFlowStep, ) from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN @@ -44,12 +44,12 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "user": HelperFlowStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode) } -OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "init": HelperFlowStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode) } diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index f64b2463bdd..87716dcbccf 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -22,7 +22,7 @@ class HelperFlowError(Exception): @dataclass -class HelperFlowStep: +class HelperFlowFormStep: """Define a helper config or options flow step.""" # Optional schema for requesting and validating user input. If schema validation @@ -57,7 +57,7 @@ class HelperCommonFlowHandler: def __init__( self, handler: HelperConfigFlowHandler | HelperOptionsFlowHandler, - flow: dict[str, HelperFlowStep | HelperFlowMenuStep], + flow: dict[str, HelperFlowFormStep | HelperFlowMenuStep], config_entry: config_entries.ConfigEntry | None, ) -> None: """Initialize a common handler.""" @@ -69,7 +69,7 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a step.""" - if isinstance(self._flow[step_id], HelperFlowStep): + if isinstance(self._flow[step_id], HelperFlowFormStep): return await self._async_form_step(step_id, user_input) return await self._async_menu_step(step_id, user_input) @@ -77,7 +77,7 @@ class HelperCommonFlowHandler: self, step_id: str, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a form step.""" - form_step: HelperFlowStep = cast(HelperFlowStep, self._flow[step_id]) + form_step: HelperFlowFormStep = cast(HelperFlowFormStep, self._flow[step_id]) if user_input is not None and form_step.schema is not None: # Do extra validation of user input @@ -109,7 +109,9 @@ class HelperCommonFlowHandler: user_input: dict[str, Any] | None = None, ) -> FlowResult: """Show form for next step.""" - form_step: HelperFlowStep = cast(HelperFlowStep, self._flow[next_step_id]) + form_step: HelperFlowFormStep = cast( + HelperFlowFormStep, self._flow[next_step_id] + ) options = dict(self._options) if user_input: @@ -147,8 +149,8 @@ class HelperCommonFlowHandler: class HelperConfigFlowHandler(config_entries.ConfigFlow): """Handle a config flow for helper integrations.""" - config_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] - options_flow: dict[str, HelperFlowStep | HelperFlowMenuStep] | None = None + config_flow: dict[str, HelperFlowFormStep | HelperFlowMenuStep] + options_flow: dict[str, HelperFlowFormStep | HelperFlowMenuStep] | None = None VERSION = 1 diff --git a/script/scaffold/templates/config_flow_helper/integration/config_flow.py b/script/scaffold/templates/config_flow_helper/integration/config_flow.py index 8f4db82c77c..cc81d0a22b1 100644 --- a/script/scaffold/templates/config_flow_helper/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_helper/integration/config_flow.py @@ -10,8 +10,8 @@ from homeassistant.const import CONF_ENTITY_ID from homeassistant.helpers import selector from homeassistant.helpers.helper_config_entry_flow import ( HelperConfigFlowHandler, + HelperFlowFormStep, HelperFlowMenuStep, - HelperFlowStep, ) from .const import DOMAIN @@ -30,12 +30,12 @@ CONFIG_SCHEMA = vol.Schema( } ).extend(OPTIONS_SCHEMA.schema) -CONFIG_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "user": HelperFlowStep(CONFIG_SCHEMA) +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep(CONFIG_SCHEMA) } -OPTIONS_FLOW: dict[str, HelperFlowStep | HelperFlowMenuStep] = { - "init": HelperFlowStep(OPTIONS_SCHEMA) +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) } From 8c10963bc0350bfe82f211531c412a249ea5a22b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Mar 2022 12:12:37 -1000 Subject: [PATCH 0663/1054] Small cleanups for recorder (#68551) --- homeassistant/components/recorder/__init__.py | 108 ++++++++++-------- homeassistant/components/recorder/repack.py | 8 +- .../components/recorder/statistics.py | 12 +- 3 files changed, 73 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index d046fe30d5c..d68b993444b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -16,6 +16,7 @@ from typing import Any, TypeVar from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select +from sqlalchemy.engine import Engine from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.orm.session import Session @@ -33,7 +34,14 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback +from homeassistant.core import ( + CALLBACK_TYPE, + CoreState, + Event, + HomeAssistant, + ServiceCall, + callback, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -306,7 +314,7 @@ async def _process_recorder_platform(hass, domain, platform): @callback -def _async_register_services(hass, instance): +def _async_register_services(hass: HomeAssistant, instance: Recorder) -> None: """Register recorder services.""" async def async_handle_purge_service(service: ServiceCall) -> None: @@ -524,9 +532,9 @@ class StopTask(RecorderTask): @dataclass class EventTask(RecorderTask): - """An object to insert into the recorder queue to stop the event handler.""" + """An event to be processed.""" - event: bool + event: Event commit_before = False def run(self, instance: Recorder) -> None: @@ -567,7 +575,7 @@ class Recorder(threading.Thread): self.async_db_ready: asyncio.Future = asyncio.Future() self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() - self.engine: Any = None + self.engine: Engine | None = None self.run_info: Any = None self.entity_filter = entity_filter @@ -580,13 +588,13 @@ class Recorder(threading.Thread): self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) self._pending_state_attributes: dict[str, StateAttributes] = {} self._pending_expunge: list[States] = [] - self.event_session = None - self.get_session = None - self._completed_first_database_setup = None - self._event_listener = None + self.event_session: Session | None = None + self.get_session: Callable[[], Session] | None = None + self._completed_first_database_setup: bool | None = None + self._event_listener: CALLBACK_TYPE | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False - self._queue_watcher = None + self._queue_watcher: CALLBACK_TYPE | None = None self._db_supports_row_number = True self._database_lock_task: DatabaseLockTask | None = None self._db_executor: DBInterruptibleThreadPoolExecutor | None = None @@ -651,7 +659,7 @@ class Recorder(threading.Thread): self._async_stop_queue_watcher_and_event_listener() @callback - def _async_stop_queue_watcher_and_event_listener(self): + def _async_stop_queue_watcher_and_event_listener(self) -> None: """Stop watching the queue and listening for events.""" if self._queue_watcher: self._queue_watcher() @@ -661,7 +669,7 @@ class Recorder(threading.Thread): self._event_listener = None @callback - def _async_event_filter(self, event) -> bool: + def _async_event_filter(self, event: Event) -> bool: """Filter events.""" if event.event_type in self.exclude_t: return False @@ -702,7 +710,9 @@ class Recorder(threading.Thread): self.queue.put(StatisticsTask(start)) @callback - def async_register(self, shutdown_task, hass_started): + def async_register( + self, shutdown_task: object, hass_started: concurrent.futures.Future + ) -> None: """Post connection initialize.""" def _empty_queue(event): @@ -746,7 +756,7 @@ class Recorder(threading.Thread): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, async_hass_started) @callback - def async_connection_failed(self): + def async_connection_failed(self) -> None: """Connect failed tasks.""" self.async_db_ready.set_result(False) persistent_notification.async_create( @@ -757,19 +767,19 @@ class Recorder(threading.Thread): self._async_stop_queue_watcher_and_event_listener() @callback - def async_connection_success(self): + def async_connection_success(self) -> None: """Connect success tasks.""" self.async_db_ready.set_result(True) self.async_start_executor() @callback - def _async_recorder_ready(self): + def _async_recorder_ready(self) -> None: """Finish start and mark recorder ready.""" self._async_setup_periodic_tasks() self.async_recorder_ready.set() @callback - def async_nightly_tasks(self, now): + def async_nightly_tasks(self, now: datetime) -> None: """Trigger the purge.""" if self.auto_purge: # Purge will schedule the perodic cleanups @@ -781,7 +791,7 @@ class Recorder(threading.Thread): self.queue.put(PerodicCleanupTask()) @callback - def async_periodic_statistics(self, now): + def async_periodic_statistics(self, now: datetime) -> None: """Trigger the hourly statistics run.""" start = statistics.get_start_time() self.queue.put(StatisticsTask(start)) @@ -807,7 +817,7 @@ class Recorder(threading.Thread): self.queue.put(ExternalStatisticsTask(metadata, stats)) @callback - def _async_setup_periodic_tasks(self): + def _async_setup_periodic_tasks(self) -> None: """Prepare periodic tasks.""" if self.hass.is_stopping or not self.get_session: # Home Assistant is shutting down @@ -823,10 +833,10 @@ class Recorder(threading.Thread): self.hass, self.async_periodic_statistics, minute=range(0, 60, 5), second=10 ) - def run(self): + def run(self) -> None: """Start processing events to save.""" shutdown_task = object() - hass_started = concurrent.futures.Future() + hass_started: concurrent.futures.Future = concurrent.futures.Future() self.hass.add_job(self.async_register, shutdown_task, hass_started) @@ -875,7 +885,7 @@ class Recorder(threading.Thread): self.hass.add_job(self._async_recorder_ready) self._run_event_loop() - def _run_event_loop(self): + def _run_event_loop(self) -> None: """Run the event loop for the recorder.""" # Use a session for the event read loop # with a commit every time the event time @@ -890,7 +900,7 @@ class Recorder(threading.Thread): self._shutdown() - def _process_one_task_or_recover(self, task: RecorderTask): + def _process_one_task_or_recover(self, task: RecorderTask) -> None: """Process an event, reconnect, or recover a malformed database.""" try: # If its not an event, commit everything @@ -931,11 +941,11 @@ class Recorder(threading.Thread): return None @callback - def _async_migration_started(self): + def _async_migration_started(self) -> None: """Set the migration started event.""" self.async_migration_event.set() - def _migrate_schema_and_setup_run(self, current_version) -> bool: + def _migrate_schema_and_setup_run(self, current_version: int) -> bool: """Migrate schema to the latest version.""" persistent_notification.create( self.hass, @@ -962,7 +972,7 @@ class Recorder(threading.Thread): self.migration_in_progress = False persistent_notification.dismiss(self.hass, "recorder_database_migration") - def _lock_database(self, task: DatabaseLockTask): + def _lock_database(self, task: DatabaseLockTask) -> None: @callback def _async_set_database_locked(task: DatabaseLockTask): task.database_locked.set() @@ -985,7 +995,7 @@ class Recorder(threading.Thread): self.queue.qsize(), ) - def _process_one_event(self, event): + def _process_one_event(self, event: Event) -> None: if event.event_type == EVENT_TIME_CHANGED: self._keepalive_count += 1 if self._keepalive_count >= KEEPALIVE_TIME: @@ -1000,6 +1010,7 @@ class Recorder(threading.Thread): if not self.enabled: return + assert self.event_session is not None try: if event.event_type == EVENT_STATE_CHANGED: @@ -1071,7 +1082,7 @@ class Recorder(threading.Thread): if not self.commit_interval: self._commit_event_session_or_retry() - def _handle_database_error(self, err): + def _handle_database_error(self, err: Exception) -> bool: """Handle a database error that may result in moving away the corrupt db.""" if isinstance(err.__cause__, sqlite3.DatabaseError): _LOGGER.exception( @@ -1081,7 +1092,7 @@ class Recorder(threading.Thread): return True return False - def _commit_event_session_or_retry(self): + def _commit_event_session_or_retry(self) -> None: """Commit the event session if there is work to do.""" if not self.event_session or ( not self.event_session.new and not self.event_session.dirty @@ -1105,7 +1116,8 @@ class Recorder(threading.Thread): tries += 1 time.sleep(self.db_retry_wait) - def _commit_event_session(self): + def _commit_event_session(self) -> None: + assert self.event_session is not None self._commits_without_expire += 1 if self._pending_expunge: @@ -1120,7 +1132,7 @@ class Recorder(threading.Thread): # We just committed the state attributes to the database # and we now know the attributes_ids. We can save - # a many selects for matching attributes by loading them + # many selects for matching attributes by loading them # into the LRU cache now. for state_attr in self._pending_state_attributes.values(): self._state_attributes_ids[ @@ -1135,7 +1147,7 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 self.event_session.expire_all() - def _handle_sqlite_corruption(self): + def _handle_sqlite_corruption(self) -> None: """Handle the sqlite3 database being corrupt.""" self._close_event_session() self._close_connection() @@ -1143,7 +1155,7 @@ class Recorder(threading.Thread): self._setup_recorder() self._setup_run() - def _close_event_session(self): + def _close_event_session(self) -> None: """Close the event session.""" self._old_states = {} self._state_attributes_ids = {} @@ -1160,27 +1172,29 @@ class Recorder(threading.Thread): "Error while rolling back and closing the event session: %s", err ) - def _reopen_event_session(self): + def _reopen_event_session(self) -> None: """Rollback the event session and reopen it after a failure.""" self._close_event_session() self._open_event_session() - def _open_event_session(self): + def _open_event_session(self) -> None: """Open the event session.""" + assert self.get_session is not None self.event_session = self.get_session() self.event_session.expire_on_commit = False - def _send_keep_alive(self): + def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" + assert self.event_session is not None _LOGGER.debug("Sending keepalive") self.event_session.connection().scalar(select([1])) @callback - def event_listener(self, event): + def event_listener(self, event: Event) -> None: """Listen for new events and put them in the process queue.""" self.queue.put(EventTask(event)) - def block_till_done(self): + def block_till_done(self) -> None: """Block till all events processed. This is only called in tests. @@ -1244,9 +1258,9 @@ class Recorder(threading.Thread): return success - def _setup_connection(self): + def _setup_connection(self) -> None: """Ensure database is ready to fly.""" - kwargs = {} + kwargs: dict[str, Any] = {} self._completed_first_database_setup = False def setup_recorder_connection(dbapi_connection, connection_record): @@ -1280,20 +1294,22 @@ class Recorder(threading.Thread): _LOGGER.debug("Connected to recorder database") @property - def _using_file_sqlite(self): + def _using_file_sqlite(self) -> bool: """Short version to check if we are using sqlite3 as a file.""" return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( SQLITE_URL_PREFIX ) - def _close_connection(self): + def _close_connection(self) -> None: """Close the connection.""" + assert self.engine is not None self.engine.dispose() self.engine = None self.get_session = None - def _setup_run(self): + def _setup_run(self) -> None: """Log the start of the current run and schedule any needed jobs.""" + assert self.get_session is not None with session_scope(session=self.get_session()) as session: start = self.recording_start end_incomplete_runs(session, start) @@ -1324,7 +1340,7 @@ class Recorder(threading.Thread): self.queue.put(StatisticsTask(start)) start = end - def _end_session(self): + def _end_session(self) -> None: """End the recorder session.""" if self.event_session is None: return @@ -1338,7 +1354,7 @@ class Recorder(threading.Thread): self.run_info = None - def _shutdown(self): + def _shutdown(self) -> None: """Save end time for current run.""" self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) self._stop_executor() @@ -1346,6 +1362,6 @@ class Recorder(threading.Thread): self._close_connection() @property - def recording(self): + def recording(self) -> bool: """Return if the recorder is recording.""" return self._event_listener is not None diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py index 68d7d5954c9..95df0681ddb 100644 --- a/homeassistant/components/recorder/repack.py +++ b/homeassistant/components/recorder/repack.py @@ -12,15 +12,17 @@ _LOGGER = logging.getLogger(__name__) def repack_database(instance: Recorder) -> None: """Repack based on engine type.""" + assert instance.engine is not None + dialect_name = instance.engine.dialect.name # Execute sqlite command to free up space on disk - if instance.engine.dialect.name == "sqlite": + if dialect_name == "sqlite": _LOGGER.debug("Vacuuming SQL DB to free space") instance.engine.execute("VACUUM") return # Execute postgresql vacuum command to free up space on disk - if instance.engine.dialect.name == "postgresql": + if dialect_name == "postgresql": _LOGGER.debug("Vacuuming SQL DB to free space") with instance.engine.connect().execution_options( isolation_level="AUTOCOMMIT" @@ -29,7 +31,7 @@ def repack_database(instance: Recorder) -> None: return # Optimize mysql / mariadb tables to free up space on disk - if instance.engine.dialect.name == "mysql": + if dialect_name == "mysql": _LOGGER.debug("Optimizing SQL DB to free space") instance.engine.execute("OPTIMIZE TABLE states, events, recorder_runs") return diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index b27a08f489c..50e987a5533 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1247,19 +1247,19 @@ def _filter_unique_constraint_integrity_error( if not isinstance(err, StatementError): return False + assert instance.engine is not None + dialect_name = instance.engine.dialect.name + ignore = False - if ( - instance.engine.dialect.name == "sqlite" - and "UNIQUE constraint failed" in str(err) - ): + if dialect_name == "sqlite" and "UNIQUE constraint failed" in str(err): ignore = True if ( - instance.engine.dialect.name == "postgresql" + dialect_name == "postgresql" and hasattr(err.orig, "pgcode") and err.orig.pgcode == "23505" ): ignore = True - if instance.engine.dialect.name == "mysql" and hasattr(err.orig, "args"): + if dialect_name == "mysql" and hasattr(err.orig, "args"): with contextlib.suppress(TypeError): if err.orig.args[0] == 1062: ignore = True From dbef90654f3693401a2df88fa00afbbffbdffcd2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Mar 2022 23:13:01 +0100 Subject: [PATCH 0664/1054] Add effects feature to Hue lights (#68567) --- homeassistant/components/hue/v2/light.py | 43 + .../components/hue/fixtures/v2_resources.json | 4037 +++++++++-------- tests/components/hue/test_light_v2.py | 36 + 3 files changed, 2103 insertions(+), 2013 deletions(-) diff --git a/homeassistant/components/hue/v2/light.py b/homeassistant/components/hue/v2/light.py index 28d972b54ec..5b6bb4ed82c 100644 --- a/homeassistant/components/hue/v2/light.py +++ b/homeassistant/components/hue/v2/light.py @@ -6,11 +6,13 @@ from typing import Any from aiohue import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.controllers.lights import LightsController +from aiohue.v2.models.feature import EffectStatus, TimedEffectStatus from aiohue.v2.models.light import Light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_FLASH, ATTR_TRANSITION, ATTR_XY_COLOR, @@ -19,6 +21,7 @@ from homeassistant.components.light import ( COLOR_MODE_ONOFF, COLOR_MODE_XY, FLASH_SHORT, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, LightEntity, @@ -36,6 +39,8 @@ from .helpers import ( normalize_hue_transition, ) +EFFECT_NONE = "None" + async def async_setup_entry( hass: HomeAssistant, @@ -86,6 +91,21 @@ class HueLight(HueBaseEntity, LightEntity): self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) # support transition if brightness control self._attr_supported_features |= SUPPORT_TRANSITION + # get list of supported effects (combine effects and timed_effects) + self._attr_effect_list = [] + if effects := resource.effects: + self._attr_effect_list = [ + x.value for x in effects.status_values if x != EffectStatus.NO_EFFECT + ] + if timed_effects := resource.timed_effects: + self._attr_effect_list += [ + x.value + for x in timed_effects.status_values + if x != TimedEffectStatus.NO_EFFECT + ] + if len(self._attr_effect_list) > 0: + self._attr_effect_list.insert(0, EFFECT_NONE) + self._attr_supported_features |= SUPPORT_EFFECT @property def brightness(self) -> int | None: @@ -155,6 +175,17 @@ class HueLight(HueBaseEntity, LightEntity): "dynamics": self.resource.dynamics.status.value, } + @property + def effect(self) -> str | None: + """Return the current effect.""" + if effects := self.resource.effects: + if effects.status != EffectStatus.NO_EFFECT: + return effects.status.value + if timed_effects := self.resource.timed_effects: + if timed_effects.status != TimedEffectStatus.NO_EFFECT: + return timed_effects.status.value + return EFFECT_NONE + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" transition = normalize_hue_transition(kwargs.get(ATTR_TRANSITION)) @@ -162,6 +193,17 @@ class HueLight(HueBaseEntity, LightEntity): color_temp = normalize_hue_colortemp(kwargs.get(ATTR_COLOR_TEMP)) brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS)) flash = kwargs.get(ATTR_FLASH) + effect = effect_str = kwargs.get(ATTR_EFFECT) + if effect_str == EFFECT_NONE: + effect = EffectStatus.NO_EFFECT + elif effect_str is not None: + # work out if we got a regular effect or timed effect + effect = EffectStatus(effect_str) + if effect == EffectStatus.UNKNOWN: + effect = TimedEffectStatus(effect_str) + if transition is None: + # a transition is required for timed effect, default to 10 minutes + transition = 600000 if flash is not None: await self.async_set_flash(flash) @@ -179,6 +221,7 @@ class HueLight(HueBaseEntity, LightEntity): color_xy=xy_color, color_temp=color_temp, transition_time=transition, + effect=effect, ) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index c3f03d8f48a..be9509f9652 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -1,2107 +1,2118 @@ [ - { - "id": "9c489c26-9e34-4fcd-8324-a57e3a664cc0", - "status": "unpaired", - "status_values": ["pairing", "paired", "unpaired"], - "type": "homekit" - }, - { - "actions": [ - { - "action": { - "color": { - "xy": { - "x": 0.5058, - "y": 0.4477 - } - }, - "dimming": { - "brightness": 46.85 - }, - "on": { - "on": true + { + "id": "9c489c26-9e34-4fcd-8324-a57e3a664cc0", + "status": "unpaired", + "status_values": ["pairing", "paired", "unpaired"], + "type": "homekit" + }, + { + "actions": [ + { + "action": { + "color": { + "xy": { + "x": 0.5058, + "y": 0.4477 } }, - "target": { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" + "dimming": { + "brightness": 46.85 + }, + "on": { + "on": true } }, - { - "action": { - "dimming": { - "brightness": 46.85 - }, - "gradient": { - "points": [ - { - "color": { - "xy": { - "x": 0.4808, - "y": 0.4485 - } - } - }, - { - "color": { - "xy": { - "x": 0.4958, - "y": 0.443 - } - } - }, - { - "color": { - "xy": { - "x": 0.5058, - "y": 0.4477 - } - } - }, - { - "color": { - "xy": { - "x": 0.5586, - "y": 0.4081 - } - } - }, - { - "color": { - "xy": { - "x": 0.569, - "y": 0.4003 - } + "target": { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + } + }, + { + "action": { + "dimming": { + "brightness": 46.85 + }, + "gradient": { + "points": [ + { + "color": { + "xy": { + "x": 0.4808, + "y": 0.4485 } } - ] - }, - "on": { - "on": true - } + }, + { + "color": { + "xy": { + "x": 0.4958, + "y": 0.443 + } + } + }, + { + "color": { + "xy": { + "x": 0.5058, + "y": 0.4477 + } + } + }, + { + "color": { + "xy": { + "x": 0.5586, + "y": 0.4081 + } + } + }, + { + "color": { + "xy": { + "x": 0.569, + "y": 0.4003 + } + } + } + ] }, - "target": { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" + "on": { + "on": true } }, - { - "action": { - "color": { - "xy": { - "x": 0.5586, - "y": 0.4081 - } - }, - "dimming": { - "brightness": 46.85 - }, - "on": { - "on": true - } - }, - "target": { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - } + "target": { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" } - ], - "group": { - "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", - "rtype": "zone" }, - "id": "fce5eabb-2f51-461b-b112-5362da301236", - "id_v1": "/scenes/qYDehk7EfGoRvkj", - "metadata": { - "image": { - "rid": "93984a4f-2d1b-4554-b972-b60fa8e476c5", - "rtype": "public_image" + { + "action": { + "color": { + "xy": { + "x": 0.5586, + "y": 0.4081 + } + }, + "dimming": { + "brightness": 46.85 + }, + "on": { + "on": true + } }, - "name": "Dynamic Test Scene" - }, - "palette": { - "color": [ - { - "color": { - "xy": { - "x": 0.4808, - "y": 0.4485 - } - }, - "dimming": { - "brightness": 74.02 - } - }, - { - "color": { - "xy": { - "x": 0.5023, - "y": 0.4467 - } - }, - "dimming": { - "brightness": 100.0 - } - }, - { - "color": { - "xy": { - "x": 0.5615, - "y": 0.4059 - } - }, - "dimming": { - "brightness": 100.0 - } - } - ], - "color_temperature": [ - { - "color_temperature": { - "mirek": 451 - }, - "dimming": { - "brightness": 31.1 - } - } - ], - "dimming": [] - }, - "speed": 0.6269841194152832, - "type": "scene" + "target": { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + } + } + ], + "group": { + "rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "rtype": "zone" }, - { - "actions": [ + "id": "fce5eabb-2f51-461b-b112-5362da301236", + "id_v1": "/scenes/qYDehk7EfGoRvkj", + "metadata": { + "image": { + "rid": "93984a4f-2d1b-4554-b972-b60fa8e476c5", + "rtype": "public_image" + }, + "name": "Dynamic Test Scene" + }, + "palette": { + "color": [ { - "action": { - "color_temperature": { - "mirek": 156 - }, - "dimming": { - "brightness": 100.0 - }, - "on": { - "on": true + "color": { + "xy": { + "x": 0.4808, + "y": 0.4485 } }, - "target": { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" + "dimming": { + "brightness": 74.02 } }, { - "action": { - "on": { - "on": true + "color": { + "xy": { + "x": 0.5023, + "y": 0.4467 } }, - "target": { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" + "dimming": { + "brightness": 100.0 + } + }, + { + "color": { + "xy": { + "x": 0.5615, + "y": 0.4059 + } + }, + "dimming": { + "brightness": 100.0 } } ], - "group": { + "color_temperature": [ + { + "color_temperature": { + "mirek": 451 + }, + "dimming": { + "brightness": 31.1 + } + } + ], + "dimming": [] + }, + "speed": 0.6269841194152832, + "type": "scene" + }, + { + "actions": [ + { + "action": { + "color_temperature": { + "mirek": 156 + }, + "dimming": { + "brightness": 100.0 + }, + "on": { + "on": true + } + }, + "target": { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + } + }, + { + "action": { + "on": { + "on": true + } + }, + "target": { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + } + } + ], + "group": { + "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", + "rtype": "room" + }, + "id": "cdbf3740-7977-4a11-8275-8c78636ad4bd", + "id_v1": "/scenes/LwgmWgRnaRUxg6K", + "metadata": { + "image": { + "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", + "rtype": "public_image" + }, + "name": "Regular Test Scene" + }, + "palette": { + "color": [], + "color_temperature": [], + "dimming": [] + }, + "speed": 0.5, + "type": "scene" + }, + { + "id": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "id_v1": "/sensors/50", + "metadata": { + "archetype": "unknown_archetype", + "name": "Wall switch with 2 controls" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "RDM001", + "product_archetype": "unknown_archetype", + "product_name": "Hue wall switch module", + "software_version": "1.0.3" + }, + "services": [ + { + "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "rtype": "button" + }, + { + "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "rtype": "button" + }, + { + "rid": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", + "rtype": "device_power" + }, + { + "rid": "af520f40-e080-43b0-9bb5-41a4d5251b2b", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "id_v1": "/lights/29", + "metadata": { + "archetype": "floor_shade", + "name": "Hue light with color and color temperature 1" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "4080248P9", + "product_archetype": "floor_shade", + "product_name": "Hue color floor", + "software_version": "1.88.1" + }, + "services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "rtype": "zigbee_connectivity" + }, + { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "id_v1": "/lights/4", + "metadata": { + "archetype": "ceiling_round", + "name": "Hue light with color temperature only" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LTC001", + "product_archetype": "ceiling_round", + "product_name": "Hue ambiance ceiling", + "software_version": "1.88.1" + }, + "services": [ + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" + }, + { + "rid": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "id_v1": "/sensors/10", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue Dimmer switch with 4 controls" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "RWL021", + "product_archetype": "unknown_archetype", + "product_name": "Hue dimmer switch", + "software_version": "1.1.28573" + }, + "services": [ + { + "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "rtype": "button" + }, + { + "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "rtype": "button" + }, + { + "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "rtype": "button" + }, + { + "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "rtype": "button" + }, + { + "rid": "0bb058bc-2139-43d9-8c9b-edfb4570953b", + "rtype": "device_power" + }, + { + "rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "b9e76da7-ac22-476a-986d-e466e62e962f", + "id_v1": "/lights/16", + "metadata": { + "archetype": "hue_lightstrip", + "name": "Hue light with color and color temperature 2" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LST002", + "product_archetype": "hue_lightstrip", + "product_name": "Hue lightstrip plus", + "software_version": "67.88.1" + }, + "services": [ + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "717afeb6-b1ce-426e-96de-48e8fe037fb0", + "rtype": "zigbee_connectivity" + }, + { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "id_v1": "/lights/23", + "metadata": { + "archetype": "classic_bulb", + "name": "Hue on/off light" + }, + "product_data": { + "certified": false, + "manufacturer_name": "eWeLink", + "model_id": "SA-003-Zigbee", + "product_archetype": "classic_bulb", + "product_name": "On/Off light", + "software_version": "1.0.2" + }, + "services": [ + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "id_v1": "/sensors/5", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue Smart button 1 control" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "ROM001", + "product_archetype": "unknown_archetype", + "product_name": "Hue Smart button", + "software_version": "2.47.8" + }, + "services": [ + { + "rid": "31cffcda-efc2-401f-a152-e10db3eed232", + "rtype": "button" + }, + { + "rid": "3f219f5a-ad6c-484f-b976-769a9c267a72", + "rtype": "device_power" + }, + { + "rid": "bba44861-8222-45c9-9e6b-d7f3a6543829", + "rtype": "zigbee_connectivity" + } + ], + "type": "device" + }, + { + "id": "4a507550-8742-4087-8bf5-c2334f29891c", + "id_v1": "", + "metadata": { + "archetype": "bridge_v2", + "name": "Philips hue" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "BSB002", + "product_archetype": "bridge_v2", + "product_name": "Philips hue", + "software_version": "1.50.1950111030" + }, + "services": [ + { + "rid": "07dd5849-abcd-efgh-b9b9-eb540408ce00", + "rtype": "bridge" + }, + { + "rid": "6c898412-ed25-4402-9807-a0c326616b0f", + "rtype": "zigbee_connectivity" + }, + { + "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "id_v1": "/lights/11", + "metadata": { + "archetype": "hue_bloom", + "name": "Hue light with color only" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LLC011", + "product_archetype": "hue_bloom", + "product_name": "Hue bloom", + "software_version": "67.91.1" + }, + "services": [ + { + "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "rtype": "light" + }, + { + "rid": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", + "rtype": "zigbee_connectivity" + }, + { + "rid": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "id_v1": "/lights/24", + "metadata": { + "archetype": "hue_lightstrip_tv", + "name": "Hue light with color and color temperature gradient" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "LCX003", + "product_archetype": "hue_lightstrip_tv", + "product_name": "Hue play gradient lightstrip", + "software_version": "1.86.7" + }, + "services": [ + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", + "rtype": "zigbee_connectivity" + }, + { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + ], + "type": "device" + }, + { + "id": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "id_v1": "/sensors/66", + "metadata": { + "archetype": "unknown_archetype", + "name": "Hue motion sensor" + }, + "product_data": { + "certified": true, + "manufacturer_name": "Signify Netherlands B.V.", + "model_id": "SML001", + "product_archetype": "unknown_archetype", + "product_name": "Hue motion sensor", + "software_version": "1.1.27575" + }, + "services": [ + { + "rid": "b6896534-016d-4052-8cb4-ef04454df62c", + "rtype": "motion" + }, + { + "rid": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", + "rtype": "device_power" + }, + { + "rid": "ec9b5ad7-2471-4356-b757-d00537828963", + "rtype": "zigbee_connectivity" + }, + { + "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "rtype": "light_level" + }, + { + "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "rtype": "temperature" + } + ], + "type": "device" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5614, + "y": 0.4058 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "effects": { + "status_values": ["no_effect", "candle", "fire"], + "status": "no_effect" + }, + "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "id_v1": "/lights/29", + "metadata": { + "archetype": "floor_shade", + "name": "Hue light with color and color temperature 1" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color_temperature": { + "mirek": 369, + "mirek_schema": { + "mirek_maximum": 454, + "mirek_minimum": 153 + }, + "mirek_valid": true + }, + "dimming": { + "brightness": 59.45, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.0, + "speed_valid": false, + "status": "none", + "status_values": ["none"] + }, + "effects": { + "status_values": ["no_effect", "candle"], + "status": "no_effect" + }, + "timed_effects": { + "status_values": ["no_effect", "sunrise"], + "status": "no_effect" + }, + "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "id_v1": "/lights/4", + "metadata": { + "archetype": "ceiling_round", + "name": "Hue light with color temperature only" + }, + "mode": "normal", + "on": { + "on": false + }, + "owner": { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5022, + "y": 0.4466 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.02500000037252903 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "id_v1": "/lights/16", + "metadata": { + "archetype": "hue_lightstrip", + "name": "Hue light with color and color temperature 2" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "dynamics": { + "speed": 0.0, + "speed_valid": false, + "status": "none", + "status_values": ["none"] + }, + "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "id_v1": "/lights/23", + "metadata": { + "archetype": "classic_bulb", + "name": "Hue on/off light" + }, + "mode": "normal", + "on": { + "on": false + }, + "owner": { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.138, + "y": 0.08 + }, + "green": { + "x": 0.2151, + "y": 0.7106 + }, + "red": { + "x": 0.704, + "y": 0.296 + } + }, + "gamut_type": "A", + "xy": { + "x": 0.4849, + "y": 0.3895 + } + }, + "dimming": { + "brightness": 50.0, + "min_dim_level": 10.0 + }, + "dynamics": { + "speed": 0.6389, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "id_v1": "/lights/11", + "metadata": { + "archetype": "hue_bloom", + "name": "Hue light with color only" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "type": "light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "color": { + "gamut": { + "blue": { + "x": 0.1532, + "y": 0.0475 + }, + "green": { + "x": 0.17, + "y": 0.7 + }, + "red": { + "x": 0.6915, + "y": 0.3083 + } + }, + "gamut_type": "C", + "xy": { + "x": 0.5022, + "y": 0.4466 + } + }, + "color_temperature": { + "mirek": null, + "mirek_schema": { + "mirek_maximum": 500, + "mirek_minimum": 153 + }, + "mirek_valid": false + }, + "dimming": { + "brightness": 46.85, + "min_dim_level": 0.10000000149011612 + }, + "dynamics": { + "speed": 0.627, + "speed_valid": true, + "status": "dynamic_palette", + "status_values": ["none", "dynamic_palette"] + }, + "gradient": { + "points": [ + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + }, + { + "color": { + "xy": { + "x": 0.4806, + "y": 0.4484 + } + } + }, + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + }, + { + "color": { + "xy": { + "x": 0.5614, + "y": 0.4058 + } + } + }, + { + "color": { + "xy": { + "x": 0.5022, + "y": 0.4466 + } + } + } + ], + "points_capable": 5 + }, + "id": "8015b17f-8336-415b-966a-b364bd082397", + "id_v1": "/lights/24", + "metadata": { + "archetype": "hue_lightstrip_tv", + "name": "Hue light with color and color temperature gradient" + }, + "mode": "normal", + "on": { + "on": true + }, + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "type": "light" + }, + { + "id": "af520f40-e080-43b0-9bb5-41a4d5251b2b", + "id_v1": "/sensors/50", + "mac_address": "00:17:88:01:0b:aa:bb:99", + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "1987ba66-c21d-48d0-98fb-121d939a71f3", + "id_v1": "/lights/29", + "mac_address": "00:17:88:01:09:aa:bb:65", + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", + "id_v1": "/lights/4", + "mac_address": "00:17:88:01:06:aa:bb:58", + "owner": { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", + "id_v1": "/sensors/10", + "mac_address": "00:17:88:01:08:aa:cc:60", + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "717afeb6-b1ce-426e-96de-48e8fe037fb0", + "id_v1": "/lights/16", + "mac_address": "00:17:88:aa:aa:bb:0d:ab", + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", + "id_v1": "/lights/23", + "mac_address": "00:12:4b:00:1f:aa:bb:f3", + "owner": { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "bba44861-8222-45c9-9e6b-d7f3a6543829", + "id_v1": "/sensors/5", + "mac_address": "00:17:88:01:aa:cc:87:b6", + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "6c898412-ed25-4402-9807-a0c326616b0f", + "id_v1": "", + "mac_address": "00:17:88:01:aa:bb:fd:c7", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "d2ae969a-add5-41b1-afbd-f2837b2eb551", + "id_v1": "/lights/34", + "mac_address": "00:17:88:01:aa:bb:cc:ed", + "owner": { + "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", + "id_v1": "/lights/11", + "mac_address": "00:17:88:aa:bb:1e:cc:b2", + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", + "id_v1": "/lights/24", + "mac_address": "00:17:88:01:aa:bb:cc:3d", + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "ec9b5ad7-2471-4356-b757-d00537828963", + "id_v1": "/sensors/66", + "mac_address": "00:17:aa:bb:cc:09:ac:c3", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "status": "connected", + "type": "zigbee_connectivity" + }, + { + "id": "5d7b3979-b936-47ff-8458-554f8a2921db", + "id_v1": "/lights/29", + "owner": { + "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "id_v1": "/lights/16", + "owner": { + "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "id_v1": "", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "proxy": true, + "renderer": false, + "type": "entertainment" + }, + { + "id": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", + "id_v1": "/lights/11", + "owner": { + "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", + "rtype": "device" + }, + "proxy": false, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 1, + "segments": [ + { + "length": 1, + "start": 0 + } + ] + }, + "type": "entertainment" + }, + { + "id": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "id_v1": "/lights/24", + "owner": { + "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", + "rtype": "device" + }, + "proxy": true, + "renderer": true, + "segments": { + "configurable": false, + "max_segments": 10, + "segments": [ + { + "length": 2, + "start": 0 + }, + { + "length": 3, + "start": 2 + }, + { + "length": 5, + "start": 5 + }, + { + "length": 4, + "start": 10 + }, + { + "length": 5, + "start": 14 + }, + { + "length": 3, + "start": 19 + }, + { + "length": 2, + "start": 22 + } + ] + }, + "type": "entertainment" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "id_v1": "/sensors/50", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "id_v1": "/sensors/50", + "metadata": { + "control_id": 2 + }, + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 2 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 3 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "id_v1": "/sensors/10", + "metadata": { + "control_id": 4 + }, + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "type": "button" + }, + { + "button": { + "last_event": "short_release" + }, + "id": "31cffcda-efc2-401f-a152-e10db3eed232", + "id_v1": "/sensors/5", + "metadata": { + "control_id": 1 + }, + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "type": "button" + }, + { + "id": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", + "id_v1": "/sensors/50", + "owner": { + "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", + "rtype": "device" + }, + "power_state": { + "battery_level": 100, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "0bb058bc-2139-43d9-8c9b-edfb4570953b", + "id_v1": "/sensors/10", + "owner": { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + }, + "power_state": { + "battery_level": 83, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "3f219f5a-ad6c-484f-b976-769a9c267a72", + "id_v1": "/sensors/5", + "owner": { + "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", + "rtype": "device" + }, + "power_state": { + "battery_level": 91, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "id": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", + "id_v1": "/sensors/66", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "power_state": { + "battery_level": 100, + "battery_state": "normal" + }, + "type": "device_power" + }, + { + "children": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + ], + "grouped_services": [ + { + "rid": "f2416154-9607-43ab-a684-4453108a200e", + "rtype": "grouped_light" + } + ], + "id": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", + "id_v1": "/groups/5", + "metadata": { + "archetype": "downstairs", + "name": "Test Zone" + }, + "services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "f2416154-9607-43ab-a684-4453108a200e", + "rtype": "grouped_light" + } + ], + "type": "zone" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "f2416154-9607-43ab-a684-4453108a200e", + "id_v1": "/groups/5", + "on": { + "on": true + }, + "type": "grouped_light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "id_v1": "/groups/0", + "on": { + "on": true + }, + "type": "grouped_light" + }, + { + "alert": { + "action_values": ["breathe"] + }, + "id": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "id_v1": "/groups/3", + "on": { + "on": false + }, + "type": "grouped_light" + }, + { + "children": [ + { "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", "rtype": "room" }, - "id": "cdbf3740-7977-4a11-8275-8c78636ad4bd", - "id_v1": "/scenes/LwgmWgRnaRUxg6K", - "metadata": { - "image": { - "rid": "7fd2ccc5-5749-4142-b7a5-66405a676f03", - "rtype": "public_image" - }, - "name": "Regular Test Scene" - }, - "palette": { - "color": [], - "color_temperature": [], - "dimming": [] - }, - "speed": 0.5, - "type": "scene" - }, - { - "id": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "id_v1": "/sensors/50", - "metadata": { - "archetype": "unknown_archetype", - "name": "Wall switch with 2 controls" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "RDM001", - "product_archetype": "unknown_archetype", - "product_name": "Hue wall switch module", - "software_version": "1.0.3" - }, - "services": [ - { - "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", - "rtype": "button" - }, - { - "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", - "rtype": "button" - }, - { - "rid": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", - "rtype": "device_power" - }, - { - "rid": "af520f40-e080-43b0-9bb5-41a4d5251b2b", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "id_v1": "/lights/29", - "metadata": { - "archetype": "floor_shade", - "name": "Hue light with color and color temperature 1" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "4080248P9", - "product_archetype": "floor_shade", - "product_name": "Hue color floor", - "software_version": "1.88.1" - }, - "services": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "1987ba66-c21d-48d0-98fb-121d939a71f3", - "rtype": "zigbee_connectivity" - }, - { - "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "id_v1": "/lights/4", - "metadata": { - "archetype": "ceiling_round", - "name": "Hue light with color temperature only" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LTC001", - "product_archetype": "ceiling_round", - "product_name": "Hue ambiance ceiling", - "software_version": "1.88.1" - }, - "services": [ - { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" - }, - { - "rid": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "id_v1": "/sensors/10", - "metadata": { - "archetype": "unknown_archetype", - "name": "Hue Dimmer switch with 4 controls" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "RWL021", - "product_archetype": "unknown_archetype", - "product_name": "Hue dimmer switch", - "software_version": "1.1.28573" - }, - "services": [ - { - "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", - "rtype": "button" - }, - { - "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", - "rtype": "button" - }, - { - "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", - "rtype": "button" - }, - { - "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", - "rtype": "button" - }, - { - "rid": "0bb058bc-2139-43d9-8c9b-edfb4570953b", - "rtype": "device_power" - }, - { - "rid": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "b9e76da7-ac22-476a-986d-e466e62e962f", - "id_v1": "/lights/16", - "metadata": { - "archetype": "hue_lightstrip", - "name": "Hue light with color and color temperature 2" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LST002", - "product_archetype": "hue_lightstrip", - "product_name": "Hue lightstrip plus", - "software_version": "67.88.1" - }, - "services": [ - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "717afeb6-b1ce-426e-96de-48e8fe037fb0", - "rtype": "zigbee_connectivity" - }, - { - "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "id_v1": "/lights/23", - "metadata": { - "archetype": "classic_bulb", - "name": "Hue on/off light" - }, - "product_data": { - "certified": false, - "manufacturer_name": "eWeLink", - "model_id": "SA-003-Zigbee", - "product_archetype": "classic_bulb", - "product_name": "On/Off light", - "software_version": "1.0.2" - }, - "services": [ - { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" - }, - { - "rid": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "id_v1": "/sensors/5", - "metadata": { - "archetype": "unknown_archetype", - "name": "Hue Smart button 1 control" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "ROM001", - "product_archetype": "unknown_archetype", - "product_name": "Hue Smart button", - "software_version": "2.47.8" - }, - "services": [ - { - "rid": "31cffcda-efc2-401f-a152-e10db3eed232", - "rtype": "button" - }, - { - "rid": "3f219f5a-ad6c-484f-b976-769a9c267a72", - "rtype": "device_power" - }, - { - "rid": "bba44861-8222-45c9-9e6b-d7f3a6543829", - "rtype": "zigbee_connectivity" - } - ], - "type": "device" - }, - { - "id": "4a507550-8742-4087-8bf5-c2334f29891c", - "id_v1": "", - "metadata": { - "archetype": "bridge_v2", - "name": "Philips hue" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "BSB002", - "product_archetype": "bridge_v2", - "product_name": "Philips hue", - "software_version": "1.50.1950111030" - }, - "services": [ - { - "rid": "07dd5849-abcd-efgh-b9b9-eb540408ce00", - "rtype": "bridge" - }, - { - "rid": "6c898412-ed25-4402-9807-a0c326616b0f", - "rtype": "zigbee_connectivity" - }, - { - "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "id_v1": "/lights/11", - "metadata": { - "archetype": "hue_bloom", - "name": "Hue light with color only" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LLC011", - "product_archetype": "hue_bloom", - "product_name": "Hue bloom", - "software_version": "67.91.1" - }, - "services": [ - { - "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", - "rtype": "light" - }, - { - "rid": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", - "rtype": "zigbee_connectivity" - }, - { - "rid": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "id_v1": "/lights/24", - "metadata": { - "archetype": "hue_lightstrip_tv", - "name": "Hue light with color and color temperature gradient" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "LCX003", - "product_archetype": "hue_lightstrip_tv", - "product_name": "Hue play gradient lightstrip", - "software_version": "1.86.7" - }, - "services": [ - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - }, - { - "rid": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", - "rtype": "zigbee_connectivity" - }, - { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - ], - "type": "device" - }, - { - "id": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "id_v1": "/sensors/66", - "metadata": { - "archetype": "unknown_archetype", - "name": "Hue motion sensor" - }, - "product_data": { - "certified": true, - "manufacturer_name": "Signify Netherlands B.V.", - "model_id": "SML001", - "product_archetype": "unknown_archetype", - "product_name": "Hue motion sensor", - "software_version": "1.1.27575" - }, - "services": [ - { - "rid": "b6896534-016d-4052-8cb4-ef04454df62c", - "rtype": "motion" - }, - { - "rid": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", - "rtype": "device_power" - }, - { - "rid": "ec9b5ad7-2471-4356-b757-d00537828963", - "rtype": "zigbee_connectivity" - }, - { - "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", - "rtype": "light_level" - }, - { - "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", - "rtype": "temperature" - } - ], - "type": "device" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.1532, - "y": 0.0475 - }, - "green": { - "x": 0.17, - "y": 0.7 - }, - "red": { - "x": 0.6915, - "y": 0.3083 - } - }, - "gamut_type": "C", - "xy": { - "x": 0.5614, - "y": 0.4058 - } - }, - "color_temperature": { - "mirek": null, - "mirek_schema": { - "mirek_maximum": 500, - "mirek_minimum": 153 - }, - "mirek_valid": false - }, - "dimming": { - "brightness": 46.85, - "min_dim_level": 0.10000000149011612 - }, - "dynamics": { - "speed": 0.627, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "id_v1": "/lights/29", - "metadata": { - "archetype": "floor_shade", - "name": "Hue light with color and color temperature 1" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { + { "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", "rtype": "device" }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color_temperature": { - "mirek": 369, - "mirek_schema": { - "mirek_maximum": 454, - "mirek_minimum": 153 - }, - "mirek_valid": true - }, - "dimming": { - "brightness": 59.45, - "min_dim_level": 0.10000000149011612 - }, - "dynamics": { - "speed": 0.0, - "speed_valid": false, - "status": "none", - "status_values": ["none"] - }, - "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "id_v1": "/lights/4", - "metadata": { - "archetype": "ceiling_round", - "name": "Hue light with color temperature only" - }, - "mode": "normal", - "on": { - "on": false - }, - "owner": { - "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "rtype": "device" - }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.1532, - "y": 0.0475 - }, - "green": { - "x": 0.17, - "y": 0.7 - }, - "red": { - "x": 0.6915, - "y": 0.3083 - } - }, - "gamut_type": "C", - "xy": { - "x": 0.5022, - "y": 0.4466 - } - }, - "color_temperature": { - "mirek": null, - "mirek_schema": { - "mirek_maximum": 500, - "mirek_minimum": 153 - }, - "mirek_valid": false - }, - "dimming": { - "brightness": 46.85, - "min_dim_level": 0.02500000037252903 - }, - "dynamics": { - "speed": 0.627, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "id_v1": "/lights/16", - "metadata": { - "archetype": "hue_lightstrip", - "name": "Hue light with color and color temperature 2" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { + { "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", "rtype": "device" }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "dynamics": { - "speed": 0.0, - "speed_valid": false, - "status": "none", - "status_values": ["none"] - }, - "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "id_v1": "/lights/23", - "metadata": { - "archetype": "classic_bulb", - "name": "Hue on/off light" - }, - "mode": "normal", - "on": { - "on": false - }, - "owner": { - "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "rtype": "device" - }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.138, - "y": 0.08 - }, - "green": { - "x": 0.2151, - "y": 0.7106 - }, - "red": { - "x": 0.704, - "y": 0.296 - } - }, - "gamut_type": "A", - "xy": { - "x": 0.4849, - "y": 0.3895 - } - }, - "dimming": { - "brightness": 50.0, - "min_dim_level": 10.0 - }, - "dynamics": { - "speed": 0.6389, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", - "id_v1": "/lights/11", - "metadata": { - "archetype": "hue_bloom", - "name": "Hue light with color only" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { - "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "rtype": "device" - }, - "type": "light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "color": { - "gamut": { - "blue": { - "x": 0.1532, - "y": 0.0475 - }, - "green": { - "x": 0.17, - "y": 0.7 - }, - "red": { - "x": 0.6915, - "y": 0.3083 - } - }, - "gamut_type": "C", - "xy": { - "x": 0.5022, - "y": 0.4466 - } - }, - "color_temperature": { - "mirek": null, - "mirek_schema": { - "mirek_maximum": 500, - "mirek_minimum": 153 - }, - "mirek_valid": false - }, - "dimming": { - "brightness": 46.85, - "min_dim_level": 0.10000000149011612 - }, - "dynamics": { - "speed": 0.627, - "speed_valid": true, - "status": "dynamic_palette", - "status_values": ["none", "dynamic_palette"] - }, - "gradient": { - "points": [ - { - "color": { - "xy": { - "x": 0.5022, - "y": 0.4466 - } - } - }, - { - "color": { - "xy": { - "x": 0.4806, - "y": 0.4484 - } - } - }, - { - "color": { - "xy": { - "x": 0.5022, - "y": 0.4466 - } - } - }, - { - "color": { - "xy": { - "x": 0.5614, - "y": 0.4058 - } - } - }, - { - "color": { - "xy": { - "x": 0.5022, - "y": 0.4466 - } - } - } - ], - "points_capable": 5 - }, - "id": "8015b17f-8336-415b-966a-b364bd082397", - "id_v1": "/lights/24", - "metadata": { - "archetype": "hue_lightstrip_tv", - "name": "Hue light with color and color temperature gradient" - }, - "mode": "normal", - "on": { - "on": true - }, - "owner": { - "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "rtype": "device" - }, - "type": "light" - }, - { - "id": "af520f40-e080-43b0-9bb5-41a4d5251b2b", - "id_v1": "/sensors/50", - "mac_address": "00:17:88:01:0b:aa:bb:99", - "owner": { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "1987ba66-c21d-48d0-98fb-121d939a71f3", - "id_v1": "/lights/29", - "mac_address": "00:17:88:01:09:aa:bb:65", - "owner": { - "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "bd878f44-feb7-406e-8af9-6a1796d1ddc9", - "id_v1": "/lights/4", - "mac_address": "00:17:88:01:06:aa:bb:58", - "owner": { - "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "db50a5d9-8cc7-486f-be06-c0b8f0d26c69", - "id_v1": "/sensors/10", - "mac_address": "00:17:88:01:08:aa:cc:60", - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "717afeb6-b1ce-426e-96de-48e8fe037fb0", - "id_v1": "/lights/16", - "mac_address": "00:17:88:aa:aa:bb:0d:ab", - "owner": { - "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "6b00ce2b-a8a5-4bab-bc5e-757a0b0338ff", - "id_v1": "/lights/23", - "mac_address": "00:12:4b:00:1f:aa:bb:f3", - "owner": { - "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "bba44861-8222-45c9-9e6b-d7f3a6543829", - "id_v1": "/sensors/5", - "mac_address": "00:17:88:01:aa:cc:87:b6", - "owner": { - "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "6c898412-ed25-4402-9807-a0c326616b0f", - "id_v1": "", - "mac_address": "00:17:88:01:aa:bb:fd:c7", - "owner": { - "rid": "4a507550-8742-4087-8bf5-c2334f29891c", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "d2ae969a-add5-41b1-afbd-f2837b2eb551", - "id_v1": "/lights/34", - "mac_address": "00:17:88:01:aa:bb:cc:ed", - "owner": { + { "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", "rtype": "device" }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "98baae94-76d9-4bc4-a1d1-d53f1d7b1286", - "id_v1": "/lights/11", - "mac_address": "00:17:88:aa:bb:1e:cc:b2", - "owner": { + { "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", "rtype": "device" }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "ff4e6545-341f-4b0d-9869-b6feb6e6fe87", - "id_v1": "/lights/24", - "mac_address": "00:17:88:01:aa:bb:cc:3d", - "owner": { + { "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", "rtype": "device" }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "ec9b5ad7-2471-4356-b757-d00537828963", - "id_v1": "/sensors/66", - "mac_address": "00:17:aa:bb:cc:09:ac:c3", - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "status": "connected", - "type": "zigbee_connectivity" - }, - { - "id": "5d7b3979-b936-47ff-8458-554f8a2921db", - "id_v1": "/lights/29", - "owner": { - "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "rtype": "device" - }, - "proxy": true, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 1, - "segments": [ - { - "length": 1, - "start": 0 - } - ] - }, - "type": "entertainment" - }, - { - "id": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", - "id_v1": "/lights/16", - "owner": { - "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", - "rtype": "device" - }, - "proxy": true, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 1, - "segments": [ - { - "length": 1, - "start": 0 - } - ] - }, - "type": "entertainment" - }, - { - "id": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", - "id_v1": "", - "owner": { - "rid": "4a507550-8742-4087-8bf5-c2334f29891c", - "rtype": "device" - }, - "proxy": true, - "renderer": false, - "type": "entertainment" - }, - { - "id": "8e6a4ff3-14ca-42f9-8358-9d691b9a4524", - "id_v1": "/lights/11", - "owner": { - "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "rtype": "device" - }, - "proxy": false, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 1, - "segments": [ - { - "length": 1, - "start": 0 - } - ] - }, - "type": "entertainment" - }, - { - "id": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "id_v1": "/lights/24", - "owner": { - "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "rtype": "device" - }, - "proxy": true, - "renderer": true, - "segments": { - "configurable": false, - "max_segments": 10, - "segments": [ - { - "length": 2, - "start": 0 - }, - { - "length": 3, - "start": 2 - }, - { - "length": 5, - "start": 5 - }, - { - "length": 4, - "start": 10 - }, - { - "length": 5, - "start": 14 - }, - { - "length": 3, - "start": 19 - }, - { - "length": 2, - "start": 22 - } - ] - }, - "type": "entertainment" - }, - { - "button": { - "last_event": "short_release" - }, - "id": "c658d3d8-a013-4b81-8ac6-78b248537e70", - "id_v1": "/sensors/50", - "metadata": { - "control_id": 1 - }, - "owner": { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", - "id_v1": "/sensors/50", - "metadata": { - "control_id": 2 - }, - "owner": { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "f92aa267-1387-4f02-9950-210fb7ca1f5a", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 1 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "button": { - "last_event": "short_release" - }, - "id": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 2 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 3 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "id": "40a810bf-3d22-4c56-9334-4a59a00768ab", - "id_v1": "/sensors/10", - "metadata": { - "control_id": 4 - }, - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "type": "button" - }, - { - "button": { - "last_event": "short_release" - }, - "id": "31cffcda-efc2-401f-a152-e10db3eed232", - "id_v1": "/sensors/5", - "metadata": { - "control_id": 1 - }, - "owner": { + { "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", "rtype": "device" }, - "type": "button" - }, - { - "id": "c1cd98a6-6c23-43bb-b6e1-08dda9e168a4", - "id_v1": "/sensors/50", - "owner": { + { "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", "rtype": "device" }, - "power_state": { - "battery_level": 100, - "battery_state": "normal" - }, - "type": "device_power" - }, - { - "id": "0bb058bc-2139-43d9-8c9b-edfb4570953b", - "id_v1": "/sensors/10", - "owner": { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - }, - "power_state": { - "battery_level": 83, - "battery_state": "normal" - }, - "type": "device_power" - }, - { - "id": "3f219f5a-ad6c-484f-b976-769a9c267a72", - "id_v1": "/sensors/5", - "owner": { - "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "rtype": "device" - }, - "power_state": { - "battery_level": 91, - "battery_state": "normal" - }, - "type": "device_power" - }, - { - "id": "669f609d-4860-4f1c-bc25-7a9cec1c3b6c", - "id_v1": "/sensors/66", - "owner": { + { "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", "rtype": "device" }, - "power_state": { - "battery_level": 100, - "battery_state": "normal" + { + "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", + "rtype": "device" + } + ], + "grouped_services": [ + { + "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "rtype": "grouped_light" + } + ], + "id": "a3fbc86a-bf4c-4c69-899d-d6eafc37e288", + "id_v1": "/groups/0", + "services": [ + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" }, - "type": "device_power" + { + "rid": "d0df7249-02c1-4480-ba2c-d61b1e648a58", + "rtype": "light" + }, + { + "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", + "rtype": "light" + }, + { + "rid": "d2d48fac-df99-4f8d-8bdc-bac82d2cfb24", + "rtype": "light" + }, + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" + }, + { + "rid": "1d1ac857-9b89-48aa-a4f3-68302e7d0998", + "rtype": "light" + }, + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" + }, + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + }, + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" + }, + { + "rid": "6a5d8ce8-c0a0-43bb-870e-d7e641cdb063", + "rtype": "button" + }, + { + "rid": "85fa4928-b061-4d19-8458-c5e30d375e39", + "rtype": "button" + }, + { + "rid": "a0640313-0a01-42b9-b236-c5e0a1568ef5", + "rtype": "button" + }, + { + "rid": "50fe978e-117c-4fc5-bb17-f707c1614a11", + "rtype": "button" + }, + { + "rid": "31cffcda-efc2-401f-a152-e10db3eed232", + "rtype": "button" + }, + { + "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", + "rtype": "button" + }, + { + "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", + "rtype": "button" + }, + { + "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", + "rtype": "button" + }, + { + "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", + "rtype": "button" + }, + { + "rid": "487aa265-8ea1-4280-a663-cbf93a79ccd7", + "rtype": "button" + }, + { + "rid": "f6e137cf-8e93-4f6a-be9c-2f820bf6d893", + "rtype": "button" + }, + { + "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", + "rtype": "button" + }, + { + "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", + "rtype": "button" + }, + { + "rid": "b6896534-016d-4052-8cb4-ef04454df62c", + "rtype": "motion" + }, + { + "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "rtype": "light_level" + }, + { + "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "rtype": "temperature" + }, + { + "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", + "rtype": "grouped_light" + } + ], + "type": "bridge_home" + }, + { + "children": [ + { + "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", + "rtype": "device" + }, + { + "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", + "rtype": "device" + } + ], + "grouped_services": [ + { + "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "rtype": "grouped_light" + } + ], + "id": "6ddc9066-7e7d-4a03-a773-c73937968296", + "id_v1": "/groups/3", + "metadata": { + "archetype": "bathroom", + "name": "Test Room" }, - { - "children": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - } - ], - "grouped_services": [ - { - "rid": "f2416154-9607-43ab-a684-4453108a200e", - "rtype": "grouped_light" - } - ], - "id": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6", - "id_v1": "/groups/5", - "metadata": { - "archetype": "downstairs", - "name": "Test Zone" + "services": [ + { + "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", + "rtype": "light" }, - "services": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - }, - { - "rid": "f2416154-9607-43ab-a684-4453108a200e", - "rtype": "grouped_light" - } - ], - "type": "zone" - }, - { - "alert": { - "action_values": ["breathe"] + { + "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", + "rtype": "light" }, - "id": "f2416154-9607-43ab-a684-4453108a200e", - "id_v1": "/groups/5", - "on": { - "on": true - }, - "type": "grouped_light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "id": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", - "id_v1": "/groups/0", - "on": { - "on": true - }, - "type": "grouped_light" - }, - { - "alert": { - "action_values": ["breathe"] - }, - "id": "e937f8db-2f0e-49a0-936e-027e60e15b34", - "id_v1": "/groups/3", - "on": { - "on": false - }, - "type": "grouped_light" - }, - { - "children": [ - { - "rid": "6ddc9066-7e7d-4a03-a773-c73937968296", - "rtype": "room" - }, - { - "rid": "0b216218-d811-4c95-8c55-bbcda50f9d50", - "rtype": "device" - }, - { - "rid": "b9e76da7-ac22-476a-986d-e466e62e962f", - "rtype": "device" - }, - { - "rid": "5ad8326c-e51a-4594-8738-fc700b53fcc4", - "rtype": "device" - }, - { - "rid": "8d07d39c-3c19-47ce-ac7a-8bf3d8e849b9", - "rtype": "device" - }, - { - "rid": "1c8be0d5-a68b-45c2-8f56-530d13b0c128", - "rtype": "device" - }, - { - "rid": "7745ebea-dd33-429c-a900-bae4e7ae1107", - "rtype": "device" - }, - { - "rid": "3ff06175-29e8-44a8-8fe7-af591b0025da", - "rtype": "device" - }, - { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - { - "rid": "342daec9-391b-480b-abdd-87f1aa04ce3b", - "rtype": "device" - } - ], - "grouped_services": [ - { - "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", - "rtype": "grouped_light" - } - ], - "id": "a3fbc86a-bf4c-4c69-899d-d6eafc37e288", - "id_v1": "/groups/0", - "services": [ - { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" - }, - { - "rid": "d0df7249-02c1-4480-ba2c-d61b1e648a58", - "rtype": "light" - }, - { - "rid": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", - "rtype": "light" - }, - { - "rid": "d2d48fac-df99-4f8d-8bdc-bac82d2cfb24", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "1d1ac857-9b89-48aa-a4f3-68302e7d0998", - "rtype": "light" - }, - { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - }, - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "6a5d8ce8-c0a0-43bb-870e-d7e641cdb063", - "rtype": "button" - }, - { - "rid": "85fa4928-b061-4d19-8458-c5e30d375e39", - "rtype": "button" - }, - { - "rid": "a0640313-0a01-42b9-b236-c5e0a1568ef5", - "rtype": "button" - }, - { - "rid": "50fe978e-117c-4fc5-bb17-f707c1614a11", - "rtype": "button" - }, - { - "rid": "31cffcda-efc2-401f-a152-e10db3eed232", - "rtype": "button" - }, - { - "rid": "f92aa267-1387-4f02-9950-210fb7ca1f5a", - "rtype": "button" - }, - { - "rid": "7f1ab9f6-cc2b-4b40-9011-65e2af153f75", - "rtype": "button" - }, - { - "rid": "b4edb2d6-55d0-47f4-bd43-7ae215ef1062", - "rtype": "button" - }, - { - "rid": "40a810bf-3d22-4c56-9334-4a59a00768ab", - "rtype": "button" - }, - { - "rid": "487aa265-8ea1-4280-a663-cbf93a79ccd7", - "rtype": "button" - }, - { - "rid": "f6e137cf-8e93-4f6a-be9c-2f820bf6d893", - "rtype": "button" - }, - { - "rid": "c658d3d8-a013-4b81-8ac6-78b248537e70", - "rtype": "button" - }, - { - "rid": "be1eb834-bdf5-4d26-8fba-7b1feaa83a9d", - "rtype": "button" - }, - { - "rid": "b6896534-016d-4052-8cb4-ef04454df62c", - "rtype": "motion" - }, - { - "rid": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", - "rtype": "light_level" - }, - { - "rid": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", - "rtype": "temperature" - }, - { - "rid": "0a74457c-cb8d-44c2-a5a5-dcb7b3675550", - "rtype": "grouped_light" - } - ], - "type": "bridge_home" - }, - { - "children": [ - { - "rid": "60b849cc-a8b5-4034-8881-ed1cd560fd13", - "rtype": "device" - }, - { - "rid": "fcdfab5d-8e04-4e9c-a999-7f92cb38c4fc", - "rtype": "device" - } - ], - "grouped_services": [ - { - "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", - "rtype": "grouped_light" - } - ], - "id": "6ddc9066-7e7d-4a03-a773-c73937968296", - "id_v1": "/groups/3", - "metadata": { - "archetype": "bathroom", - "name": "Test Room" - }, - "services": [ - { - "rid": "7697ac8a-25aa-4576-bb40-0036c0db15b9", - "rtype": "light" - }, - { - "rid": "3a6710fa-4474-4eba-b533-5e6e72968feb", - "rtype": "light" - }, - { - "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", - "rtype": "grouped_light" - } - ], - "type": "room" - }, - { - "channels": [ - { - "channel_id": 0, - "members": [ - { - "index": 0, - "service": { - "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.8399999737739563, - "y": 0.8999999761581421, - "z": -0.5 - } - }, - { - "channel_id": 1, - "members": [ - { - "index": 0, - "service": { - "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.9399999976158142, - "y": -0.20999999344348907, - "z": -1.0 - } - }, - { - "channel_id": 2, - "members": [ - { - "index": 0, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - } - }, - { - "channel_id": 3, - "members": [ - { - "index": 1, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": 0.0 - } - }, - { - "channel_id": 4, - "members": [ - { - "index": 2, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": 0.4000000059604645 - } - }, - { - "channel_id": 5, - "members": [ - { - "index": 3, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.0, - "y": 0.800000011920929, - "z": 0.4000000059604645 - } - }, - { - "channel_id": 6, - "members": [ - { - "index": 4, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": 0.4000000059604645 - } - }, - { - "channel_id": 7, - "members": [ - { - "index": 5, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": 0.0 - } - }, - { - "channel_id": 8, - "members": [ - { - "index": 6, - "service": { - "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - } - }, - { - "channel_id": 9, - "members": [ - { - "index": 0, - "service": { - "rid": "be321947-0a48-4742-913d-073b3b540c97", - "rtype": "entertainment" - } - } - ], - "position": { - "x": 0.9100000262260437, - "y": 0.8100000023841858, - "z": -0.3799999952316284 - } - } - ], - "configuration_type": "screen", - "id": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", - "id_v1": "/groups/2", - "light_services": [ - { - "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", - "rtype": "light" - }, - { - "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", - "rtype": "light" - }, - { - "rid": "8015b17f-8336-415b-966a-b364bd082397", - "rtype": "light" - } - ], - "locations": { - "service_locations": [ + { + "rid": "e937f8db-2f0e-49a0-936e-027e60e15b34", + "rtype": "grouped_light" + } + ], + "type": "room" + }, + { + "channels": [ + { + "channel_id": 0, + "members": [ { - "position": { - "x": -0.8399999737739563, - "y": 0.8999999761581421, - "z": -0.5 - }, - "positions": [ - { - "x": -0.8399999737739563, - "y": 0.8999999761581421, - "z": -0.5 - } - ], + "index": 0, "service": { "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", "rtype": "entertainment" } - }, + } + ], + "position": { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + } + }, + { + "channel_id": 1, + "members": [ { - "position": { - "x": -0.9399999976158142, - "y": -0.20999999344348907, - "z": -1.0 - }, - "positions": [ - { - "x": -0.9399999976158142, - "y": -0.20999999344348907, - "z": -1.0 - } - ], + "index": 0, "service": { "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", "rtype": "entertainment" } - }, + } + ], + "position": { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + } + }, + { + "channel_id": 2, + "members": [ { - "position": { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - }, - "positions": [ - { - "x": -0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - }, - { - "x": 0.4000000059604645, - "y": 0.800000011920929, - "z": -0.4000000059604645 - } - ], + "index": 0, "service": { "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", "rtype": "entertainment" } - }, + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + }, + { + "channel_id": 3, + "members": [ { - "position": { - "x": 0.9100000262260437, - "y": 0.8100000023841858, - "z": -0.3799999952316284 - }, - "positions": [ - { - "x": 0.9100000262260437, - "y": 0.8100000023841858, - "z": -0.3799999952316284 - } - ], + "index": 1, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": 0.0 + } + }, + { + "channel_id": 4, + "members": [ + { + "index": 2, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 5, + "members": [ + { + "index": 3, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.0, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 6, + "members": [ + { + "index": 4, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": 0.4000000059604645 + } + }, + { + "channel_id": 7, + "members": [ + { + "index": 5, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": 0.0 + } + }, + { + "channel_id": 8, + "members": [ + { + "index": 6, + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + } + ], + "position": { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + }, + { + "channel_id": 9, + "members": [ + { + "index": 0, "service": { "rid": "be321947-0a48-4742-913d-073b3b540c97", "rtype": "entertainment" } } - ] - }, - "metadata": { - "name": "Entertainmentroom 1" - }, - "name": "Entertainmentroom 1", - "status": "inactive", - "stream_proxy": { - "mode": "auto", - "node": { - "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", - "rtype": "entertainment" + ], + "position": { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 } + } + ], + "configuration_type": "screen", + "id": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "id_v1": "/groups/2", + "light_services": [ + { + "rid": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", + "rtype": "light" }, - "type": "entertainment_configuration" - }, - { - "bridge_id": "aabbccddeeffggh", - "id": "07dd5849-abcd-efgh-b9b9-eb540408ce00", - "id_v1": "", - "owner": { - "rid": "4a507550-8742-4087-8bf5-c2334f29891c", - "rtype": "device" + { + "rid": "b3fe71ef-d0ef-48de-9355-d9e604377df0", + "rtype": "light" }, - "time_zone": { - "time_zone": "Europe/Amsterdam" - }, - "type": "bridge" - }, - { - "enabled": true, - "id": "b6896534-016d-4052-8cb4-ef04454df62c", - "id_v1": "/sensors/66", - "motion": { - "motion": false, - "motion_valid": true - }, - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "type": "motion" - }, - { - "enabled": true, - "id": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", - "id_v1": "/sensors/67", - "light": { - "light_level": 18027, - "light_level_valid": true - }, - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "type": "light_level" - }, - { - "enabled": true, - "id": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", - "id_v1": "/sensors/68", - "owner": { - "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", - "rtype": "device" - }, - "temperature": { - "temperature": 18.139999389648438, - "temperature_valid": true - }, - "type": "temperature" - }, - { - "configuration": { - "end_state": "last_state", - "where": [ - { - "group": { - "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", - "rtype": "entertainment_configuration" - } - } - ] - }, - "dependees": [ + { + "rid": "8015b17f-8336-415b-966a-b364bd082397", + "rtype": "light" + } + ], + "locations": { + "service_locations": [ { - "level": "critical", - "target": { + "position": { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + }, + "positions": [ + { + "x": -0.8399999737739563, + "y": 0.8999999761581421, + "z": -0.5 + } + ], + "service": { + "rid": "5d7b3979-b936-47ff-8458-554f8a2921db", + "rtype": "entertainment" + } + }, + { + "position": { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + }, + "positions": [ + { + "x": -0.9399999976158142, + "y": -0.20999999344348907, + "z": -1.0 + } + ], + "service": { + "rid": "d88acc42-259c-43b5-bf5d-90c16cdb8f2f", + "rtype": "entertainment" + } + }, + { + "position": { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + }, + "positions": [ + { + "x": -0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + }, + { + "x": 0.4000000059604645, + "y": 0.800000011920929, + "z": -0.4000000059604645 + } + ], + "service": { + "rid": "7b03eb98-4cfd-4acf-ac11-675f51613e5e", + "rtype": "entertainment" + } + }, + { + "position": { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + }, + "positions": [ + { + "x": 0.9100000262260437, + "y": 0.8100000023841858, + "z": -0.3799999952316284 + } + ], + "service": { + "rid": "be321947-0a48-4742-913d-073b3b540c97", + "rtype": "entertainment" + } + } + ] + }, + "metadata": { + "name": "Entertainmentroom 1" + }, + "name": "Entertainmentroom 1", + "status": "inactive", + "stream_proxy": { + "mode": "auto", + "node": { + "rid": "b8ab0c30-b227-4d35-9c96-7cd16131fcc5", + "rtype": "entertainment" + } + }, + "type": "entertainment_configuration" + }, + { + "bridge_id": "aabbccddeeffggh", + "id": "07dd5849-abcd-efgh-b9b9-eb540408ce00", + "id_v1": "", + "owner": { + "rid": "4a507550-8742-4087-8bf5-c2334f29891c", + "rtype": "device" + }, + "time_zone": { + "time_zone": "Europe/Amsterdam" + }, + "type": "bridge" + }, + { + "enabled": true, + "id": "b6896534-016d-4052-8cb4-ef04454df62c", + "id_v1": "/sensors/66", + "motion": { + "motion": false, + "motion_valid": true + }, + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "type": "motion" + }, + { + "enabled": true, + "id": "d504e7a4-9a18-4854-90fd-c5b6ac102c40", + "id_v1": "/sensors/67", + "light": { + "light_level": 18027, + "light_level_valid": true + }, + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "type": "light_level" + }, + { + "enabled": true, + "id": "66466e14-d2fa-4b96-b2a0-e10de9cd8b8b", + "id_v1": "/sensors/68", + "owner": { + "rid": "2330b45d-6079-4c6e-bba6-1b68afb1a0d6", + "rtype": "device" + }, + "temperature": { + "temperature": 18.139999389648438, + "temperature_valid": true + }, + "type": "temperature" + }, + { + "configuration": { + "end_state": "last_state", + "where": [ + { + "group": { "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", "rtype": "entertainment_configuration" - }, - "type": "ResourceDependee" + } } - ], - "enabled": true, - "id": "0670cfb1-2bd7-4237-a0e3-1827a44d7231", - "last_error": "", - "metadata": { - "name": "state_after_streaming" - }, - "migrated_from": "/resourcelinks/47450", - "script_id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", - "status": "running", - "type": "behavior_instance" + ] }, - { - "configuration_schema": { - "$ref": "leaving_home_config.json#" - }, - "description": "Automatically turn off your lights when you leave", - "id": "0194752a-2d53-4f92-8209-dfdc52745af3", - "metadata": { - "name": "Leaving home" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "dependees": [ + { + "level": "critical", + "target": { + "rid": "c14cf1cf-6c7a-4984-b8bb-c5b71aeb70fc", + "rtype": "entertainment_configuration" + }, + "type": "ResourceDependee" + } + ], + "enabled": true, + "id": "0670cfb1-2bd7-4237-a0e3-1827a44d7231", + "last_error": "", + "metadata": { + "name": "state_after_streaming" }, - { - "configuration_schema": { - "$ref": "schedule_config.json#" - }, - "description": "Schedule turning on and off lights", - "id": "7238c707-8693-4f19-9095-ccdc1444d228", - "metadata": { - "name": "Schedule" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "migrated_from": "/resourcelinks/47450", + "script_id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "status": "running", + "type": "behavior_instance" + }, + { + "configuration_schema": { + "$ref": "leaving_home_config.json#" }, - { - "configuration_schema": { - "$ref": "lights_state_after_streaming_config.json#" - }, - "description": "State of lights in the entertainment group after streaming ends", - "id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", - "metadata": { - "name": "Light state after streaming" - }, - "state_schema": {}, - "trigger_schema": {}, - "type": "behavior_script", - "version": "0.0.1" + "description": "Automatically turn off your lights when you leave", + "id": "0194752a-2d53-4f92-8209-dfdc52745af3", + "metadata": { + "name": "Leaving home" }, - { - "configuration_schema": { - "$ref": "basic_goto_sleep_config.json#" - }, - "description": "Get ready for nice sleep.", - "id": "7e571ac6-f363-42e1-809a-4cbf6523ed72", - "metadata": { - "name": "Basic go to sleep routine" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" }, - { - "configuration_schema": { - "$ref": "coming_home_config.json#" - }, - "description": "Automatically turn your lights to choosen light states, when you arrive at home.", - "id": "fd60fcd1-4809-4813-b510-4a18856a595c", - "metadata": { - "name": "Coming home" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "schedule_config.json#" }, - { - "configuration_schema": { - "$ref": "basic_wake_up_config.json#" - }, - "description": "Get your body in the mood to wake up by fading on the lights in the morning.", - "id": "ff8957e3-2eb9-4699-a0c8-ad2cb3ede704", - "metadata": { - "name": "Basic wake up routine" - }, - "state_schema": {}, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "description": "Schedule turning on and off lights", + "id": "7238c707-8693-4f19-9095-ccdc1444d228", + "metadata": { + "name": "Schedule" }, - { - "configuration_schema": { - "$ref": "natural_light_config.json#" - }, - "description": "Natural light during the day", - "id": "a4260b49-0c69-4926-a29c-417f4a38a352", - "metadata": { - "name": "Natural Light" - }, - "state_schema": { - "$ref": "natural_light_state.json#" - }, - "trigger_schema": { - "$ref": "smart_scene_trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" }, - { - "configuration_schema": { - "$ref": "timer_config.json#" - }, - "description": "Countdown Timer", - "id": "e73bc72d-96b1-46f8-aa57-729861f80c78", - "metadata": { - "name": "Timers" - }, - "state_schema": { - "$ref": "timer_state.json#" - }, - "trigger_schema": { - "$ref": "trigger.json#" - }, - "type": "behavior_script", - "version": "0.0.1" + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "lights_state_after_streaming_config.json#" }, - { - "id": "c6e03a31-4c30-4cef-834f-26ffbb06a593", - "name": "Test geofence client", - "type": "geofence_client" + "description": "State of lights in the entertainment group after streaming ends", + "id": "7719b841-6b3d-448d-a0e7-601ae9edb6a2", + "metadata": { + "name": "Light state after streaming" }, - { - "id": "52612630-841e-4d39-9763-60346a0da759", - "is_configured": true, - "type": "geolocation" - } - ] - \ No newline at end of file + "state_schema": {}, + "trigger_schema": {}, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_goto_sleep_config.json#" + }, + "description": "Get ready for nice sleep.", + "id": "7e571ac6-f363-42e1-809a-4cbf6523ed72", + "metadata": { + "name": "Basic go to sleep routine" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "coming_home_config.json#" + }, + "description": "Automatically turn your lights to choosen light states, when you arrive at home.", + "id": "fd60fcd1-4809-4813-b510-4a18856a595c", + "metadata": { + "name": "Coming home" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "basic_wake_up_config.json#" + }, + "description": "Get your body in the mood to wake up by fading on the lights in the morning.", + "id": "ff8957e3-2eb9-4699-a0c8-ad2cb3ede704", + "metadata": { + "name": "Basic wake up routine" + }, + "state_schema": {}, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "natural_light_config.json#" + }, + "description": "Natural light during the day", + "id": "a4260b49-0c69-4926-a29c-417f4a38a352", + "metadata": { + "name": "Natural Light" + }, + "state_schema": { + "$ref": "natural_light_state.json#" + }, + "trigger_schema": { + "$ref": "smart_scene_trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "configuration_schema": { + "$ref": "timer_config.json#" + }, + "description": "Countdown Timer", + "id": "e73bc72d-96b1-46f8-aa57-729861f80c78", + "metadata": { + "name": "Timers" + }, + "state_schema": { + "$ref": "timer_state.json#" + }, + "trigger_schema": { + "$ref": "trigger.json#" + }, + "type": "behavior_script", + "version": "0.0.1" + }, + { + "id": "c6e03a31-4c30-4cef-834f-26ffbb06a593", + "name": "Test geofence client", + "type": "geofence_client" + }, + { + "id": "52612630-841e-4d39-9763-60346a0da759", + "is_configured": true, + "type": "geolocation" + } +] diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index eba5726229e..c6b114ff612 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -36,6 +36,8 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): assert light_1.attributes["min_mireds"] == 153 assert light_1.attributes["max_mireds"] == 500 assert light_1.attributes["dynamics"] == "dynamic_palette" + assert light_1.attributes["effect_list"] == ["None", "candle", "fire"] + assert light_1.attributes["effect"] == "None" # test light which supports color temperature only light_2 = hass.states.get("light.hue_light_with_color_temperature_only") @@ -49,6 +51,7 @@ async def test_lights(hass, mock_bridge_v2, v2_resources_test_data): assert light_2.attributes["min_mireds"] == 153 assert light_2.attributes["max_mireds"] == 454 assert light_2.attributes["dynamics"] == "none" + assert light_2.attributes["effect_list"] == ["None", "candle", "sunrise"] # test light which supports color only light_3 = hass.states.get("light.hue_light_with_color_only") @@ -164,6 +167,39 @@ async def test_light_turn_on_service(hass, mock_bridge_v2, v2_resources_test_dat assert len(mock_bridge_v2.mock_requests) == 6 assert mock_bridge_v2.mock_requests[5]["json"]["color_temperature"]["mirek"] == 500 + # test enable effect + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "candle"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 7 + assert mock_bridge_v2.mock_requests[6]["json"]["effects"]["effect"] == "candle" + + # test disable effect + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "None"}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 8 + assert mock_bridge_v2.mock_requests[7]["json"]["effects"]["effect"] == "no_effect" + + # test timed effect + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": test_light_id, "effect": "sunrise", "transition": 6}, + blocking=True, + ) + assert len(mock_bridge_v2.mock_requests) == 9 + assert ( + mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["effect"] == "sunrise" + ) + assert mock_bridge_v2.mock_requests[8]["json"]["timed_effects"]["duration"] == 6000 + async def test_light_turn_off_service(hass, mock_bridge_v2, v2_resources_test_data): """Test calling the turn off service on a light.""" From e5f424a28070b669ba34d047af7b86b2965d5701 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Mar 2022 13:20:43 -1000 Subject: [PATCH 0665/1054] Switch filter to use the database executor (#68594) --- homeassistant/components/filter/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index a5b54a621a7..59ba706577c 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN -from homeassistant.components.recorder import history +from homeassistant.components.recorder import get_instance, history from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, @@ -296,7 +296,7 @@ class SensorFilter(SensorEntity): # Retrieve the largest window_size of each type if largest_window_items > 0: - filter_history = await self.hass.async_add_executor_job( + filter_history = await get_instance(self.hass).async_add_executor_job( partial( history.get_last_state_changes, self.hass, @@ -308,7 +308,7 @@ class SensorFilter(SensorEntity): history_list.extend(filter_history[self._entity]) if largest_window_time > timedelta(seconds=0): start = dt_util.utcnow() - largest_window_time - filter_history = await self.hass.async_add_executor_job( + filter_history = await get_instance(self.hass).async_add_executor_job( partial( history.state_changes_during_period, self.hass, From 661f2fd6132a46db366287d0ab87f09406552700 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 24 Mar 2022 02:07:45 +0100 Subject: [PATCH 0666/1054] Bump py-synologydsm-api to 1.0.7 (#68584) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 39eb1190388..1c9df126a89 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.6"], + "requirements": ["py-synologydsm-api==1.0.7"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8beb41ff1cb..9da57ef089a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ py-nightscout==1.2.2 py-schluter==0.1.7 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.6 +py-synologydsm-api==1.0.7 # homeassistant.components.zabbix py-zabbix==1.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b44280da5e..b60aa4516f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.6 +py-synologydsm-api==1.0.7 # homeassistant.components.seventeentrack py17track==2021.12.2 From ad7a2c298b558712dad4fa9ec4c375f9885639b3 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Wed, 23 Mar 2022 22:14:39 -0400 Subject: [PATCH 0667/1054] Add SleepIQ select entity for foundation preset positions (#68489) --- homeassistant/components/sleepiq/__init__.py | 1 + homeassistant/components/sleepiq/select.py | 82 ++++++++++++ tests/components/sleepiq/conftest.py | 124 ++++++++++++++----- tests/components/sleepiq/test_select.py | 119 ++++++++++++++++++ 4 files changed, 292 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/sleepiq/select.py create mode 100644 tests/components/sleepiq/test_select.py diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index c12eace2f03..b98be3dcfe1 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -35,6 +35,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.LIGHT, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py new file mode 100644 index 00000000000..2bf305007ca --- /dev/null +++ b/homeassistant/components/sleepiq/select.py @@ -0,0 +1,82 @@ +"""Support for SleepIQ foundation preset selection.""" +from __future__ import annotations + +from asyncsleepiq import ( + FAVORITE, + FLAT, + READ, + SNORE, + WATCH_TV, + ZERO_G, + SleepIQBed, + SleepIQPreset, +) + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN +from .coordinator import SleepIQData +from .entity import SleepIQBedEntity + +FOUNDATION_PRESET_NAMES = { + "Not at preset": 0, + "Favorite": FAVORITE, + "Read": READ, + "Watch TV": WATCH_TV, + "Flat": FLAT, + "Zero G": ZERO_G, + "Snore": SNORE, +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SleepIQ foundation preset select entities.""" + data: SleepIQData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SleepIQSelectEntity(data.data_coordinator, bed, preset) + for bed in data.client.beds.values() + for preset in bed.foundation.presets + ) + + +class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): + """Representation of a SleepIQ select entity.""" + + _attr_options = list(FOUNDATION_PRESET_NAMES.keys()) + + def __init__( + self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, preset: SleepIQPreset + ) -> None: + """Initialize the select entity.""" + self.preset = preset + + if preset.side: + self._attr_name = ( + f"SleepNumber {bed.name} Foundation Preset {preset.side_full}" + ) + self._attr_unique_id = f"{bed.id}_preset_{preset.side}" + else: + self._attr_name = f"SleepNumber {bed.name} Foundation Preset" + self._attr_unique_id = f"{bed.id}_preset" + + super().__init__(coordinator, bed) + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Update entity attributes.""" + self._attr_current_option = self.preset.preset + + async def async_select_option(self, option: str) -> None: + """Change the current preset.""" + await self.preset.set_preset(FOUNDATION_PRESET_NAMES[option]) + self._attr_current_option = option + self.async_write_ha_state() diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index a6f6ba78aba..4408f41035b 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,13 +1,15 @@ """Common methods for SleepIQ.""" from __future__ import annotations -from unittest.mock import create_autospec, patch +from collections.abc import Generator +from unittest.mock import MagicMock, create_autospec, patch from asyncsleepiq import ( SleepIQActuator, SleepIQBed, SleepIQFoundation, SleepIQLight, + SleepIQPreset, SleepIQSleeper, ) import pytest @@ -28,6 +30,8 @@ SLEEPER_L_NAME = "SleeperL" SLEEPER_R_NAME = "Sleeper R" SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") +PRESET_L_STATE = "Watch TV" +PRESET_R_STATE = "Flat" SLEEPIQ_CONFIG = { CONF_USERNAME: "user@email.com", @@ -36,48 +40,88 @@ SLEEPIQ_CONFIG = { @pytest.fixture -def mock_asyncsleepiq(): - """Mock an AsyncSleepIQ object.""" +def mock_bed() -> MagicMock: + """Mock a SleepIQBed object with sleepers and lights.""" + bed = create_autospec(SleepIQBed) + bed.name = BED_NAME + bed.id = BED_ID + bed.mac_addr = "12:34:56:78:AB:CD" + bed.model = "C10" + bed.paused = False + sleeper_l = create_autospec(SleepIQSleeper) + sleeper_r = create_autospec(SleepIQSleeper) + bed.sleepers = [sleeper_l, sleeper_r] + + sleeper_l.side = "L" + sleeper_l.name = SLEEPER_L_NAME + sleeper_l.in_bed = True + sleeper_l.sleep_number = 40 + sleeper_l.pressure = 1000 + sleeper_l.sleeper_id = SLEEPER_L_ID + + sleeper_r.side = "R" + sleeper_r.name = SLEEPER_R_NAME + sleeper_r.in_bed = False + sleeper_r.sleep_number = 80 + sleeper_r.pressure = 1400 + sleeper_r.sleeper_id = SLEEPER_R_ID + + bed.foundation = create_autospec(SleepIQFoundation) + light_1 = create_autospec(SleepIQLight) + light_1.outlet_id = 1 + light_1.is_on = False + light_2 = create_autospec(SleepIQLight) + light_2.outlet_id = 2 + light_2.is_on = False + bed.foundation.lights = [light_1, light_2] + + return bed + + +@pytest.fixture +def mock_asyncsleepiq_single_foundation( + mock_bed: MagicMock, +) -> Generator[MagicMock, None, None]: + """Mock an AsyncSleepIQ object with a single foundation.""" with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: client = mock.return_value - bed = create_autospec(SleepIQBed) - client.beds = {BED_ID: bed} - bed.name = BED_NAME - bed.id = BED_ID - bed.mac_addr = "12:34:56:78:AB:CD" - bed.model = "C10" - bed.paused = False - sleeper_l = create_autospec(SleepIQSleeper) - sleeper_r = create_autospec(SleepIQSleeper) - bed.sleepers = [sleeper_l, sleeper_r] + client.beds = {BED_ID: mock_bed} - sleeper_l.side = "L" - sleeper_l.name = SLEEPER_L_NAME - sleeper_l.in_bed = True - sleeper_l.sleep_number = 40 - sleeper_l.pressure = 1000 - sleeper_l.sleeper_id = SLEEPER_L_ID + actuator_h = create_autospec(SleepIQActuator) + actuator_f = create_autospec(SleepIQActuator) + mock_bed.foundation.actuators = [actuator_h, actuator_f] - sleeper_r.side = "R" - sleeper_r.name = SLEEPER_R_NAME - sleeper_r.in_bed = False - sleeper_r.sleep_number = 80 - sleeper_r.pressure = 1400 - sleeper_r.sleeper_id = SLEEPER_R_ID + actuator_h.side = "R" + actuator_h.side_full = "Right" + actuator_h.actuator = "H" + actuator_h.actuator_full = "Head" + actuator_h.position = 60 - bed.foundation = create_autospec(SleepIQFoundation) - light_1 = create_autospec(SleepIQLight) - light_1.outlet_id = 1 - light_1.is_on = False - light_2 = create_autospec(SleepIQLight) - light_2.outlet_id = 2 - light_2.is_on = False - bed.foundation.lights = [light_1, light_2] + actuator_f.side = None + actuator_f.actuator = "F" + actuator_f.actuator_full = "Foot" + actuator_f.position = 10 + + preset = create_autospec(SleepIQPreset) + mock_bed.foundation.presets = [preset] + + preset.preset = PRESET_R_STATE + preset.side = None + preset.side_full = None + yield client + + +@pytest.fixture +def mock_asyncsleepiq(mock_bed: MagicMock) -> Generator[MagicMock, None, None]: + """Mock an AsyncSleepIQ object with a split foundation.""" + with patch("homeassistant.components.sleepiq.AsyncSleepIQ", autospec=True) as mock: + client = mock.return_value + client.beds = {BED_ID: mock_bed} actuator_h_r = create_autospec(SleepIQActuator) actuator_h_l = create_autospec(SleepIQActuator) actuator_f = create_autospec(SleepIQActuator) - bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f] + mock_bed.foundation.actuators = [actuator_h_r, actuator_h_l, actuator_f] actuator_h_r.side = "R" actuator_h_r.side_full = "Right" @@ -96,6 +140,18 @@ def mock_asyncsleepiq(): actuator_f.actuator_full = "Foot" actuator_f.position = 10 + preset_l = create_autospec(SleepIQPreset) + preset_r = create_autospec(SleepIQPreset) + mock_bed.foundation.presets = [preset_l, preset_r] + + preset_l.preset = PRESET_L_STATE + preset_l.side = "L" + preset_l.side_full = "Left" + + preset_r.preset = PRESET_R_STATE + preset_r.side = "R" + preset_r.side_full = "Right" + yield client diff --git a/tests/components/sleepiq/test_select.py b/tests/components/sleepiq/test_select.py new file mode 100644 index 00000000000..1b0c34d167c --- /dev/null +++ b/tests/components/sleepiq/test_select.py @@ -0,0 +1,119 @@ +"""Tests for the SleepIQ select platform.""" +from unittest.mock import MagicMock + +from asyncsleepiq import ZERO_G + +from homeassistant.components.select import DOMAIN, SERVICE_SELECT_OPTION +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_OPTION, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.components.sleepiq.conftest import ( + BED_ID, + BED_NAME, + BED_NAME_LOWER, + PRESET_L_STATE, + PRESET_R_STATE, + setup_platform, +) + + +async def test_split_foundation_preset( + hass: HomeAssistant, mock_asyncsleepiq: MagicMock +) -> None: + """Test the SleepIQ select entity for split foundation presets.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_right" + ) + assert state.state == PRESET_R_STATE + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foundation Preset Right" + ) + + entry = entity_registry.async_get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_right" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_preset_R" + + state = hass.states.get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_left" + ) + assert state.state == PRESET_L_STATE + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foundation Preset Left" + ) + + entry = entity_registry.async_get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_left" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_preset_L" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset_left", + ATTR_OPTION: "Zero G", + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq.beds[BED_ID].foundation.presets[0].set_preset.assert_called_once() + mock_asyncsleepiq.beds[BED_ID].foundation.presets[0].set_preset.assert_called_with( + ZERO_G + ) + + +async def test_single_foundation_preset( + hass: HomeAssistant, mock_asyncsleepiq_single_foundation: MagicMock +) -> None: + """Test the SleepIQ select entity for single foundation presets.""" + entry = await setup_platform(hass, DOMAIN) + entity_registry = er.async_get(hass) + + state = hass.states.get(f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset") + assert state.state == PRESET_R_STATE + assert state.attributes.get(ATTR_ICON) == "mdi:bed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == f"SleepNumber {BED_NAME} Foundation Preset" + ) + + entry = entity_registry.async_get( + f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset" + ) + assert entry + assert entry.unique_id == f"{BED_ID}_preset" + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.sleepnumber_{BED_NAME_LOWER}_foundation_preset", + ATTR_OPTION: "Zero G", + }, + blocking=True, + ) + await hass.async_block_till_done() + + mock_asyncsleepiq_single_foundation.beds[BED_ID].foundation.presets[ + 0 + ].set_preset.assert_called_once() + mock_asyncsleepiq_single_foundation.beds[BED_ID].foundation.presets[ + 0 + ].set_preset.assert_called_with(ZERO_G) From adbacdd5c222bcc4803d570072f1db6b2e1f5bbb Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 24 Mar 2022 04:53:31 +0100 Subject: [PATCH 0668/1054] Use DataUpdateCoordinator in here_travel_time (#61398) * Add DataUpdateCoordinator * Use TypedDict for extra_state_attributes * Extend DataUpdateCoordinator * Use platform enum * Use attribution property * Use relative imports * Revert native_value logic * Directly return result in build_hass_attribution * Correctly declare traffic_mode as bool * Use self._attr_* * Fix mypy issues * Update homeassistant/components/here_travel_time/__init__.py Co-authored-by: Allen Porter * Update homeassistant/components/here_travel_time/__init__.py Co-authored-by: Allen Porter * Update homeassistant/components/here_travel_time/sensor.py Co-authored-by: Allen Porter * blacken * from datetime import time * remove none check * Move dataclasses to models.py * Set destination to now if None * Add mypy error code Co-authored-by: Allen Porter --- .../components/here_travel_time/__init__.py | 169 ++++++++++- .../components/here_travel_time/const.py | 77 +++++ .../components/here_travel_time/model.py | 35 +++ .../components/here_travel_time/sensor.py | 275 ++++-------------- .../here_travel_time/test_sensor.py | 74 +++-- 5 files changed, 389 insertions(+), 241 deletions(-) create mode 100644 homeassistant/components/here_travel_time/const.py create mode 100644 homeassistant/components/here_travel_time/model.py diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 9a5c8ec32ac..9bee2fff9a1 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -1 +1,168 @@ -"""The here_travel_time component.""" +"""The HERE Travel Time integration.""" +from __future__ import annotations + +from datetime import datetime, time, timedelta +import logging + +import async_timeout +from herepy import NoRouteFoundError, RouteMode, RoutingApi, RoutingResponse + +from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.location import find_coordinates +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt + +from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + NO_ROUTE_ERROR_MESSAGE, + TRAFFIC_MODE_ENABLED, + TRAVEL_MODES_VEHICLE, +) +from .model import HERERoutingData, HERETravelTimeConfig + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): + """HERETravelTime DataUpdateCoordinator.""" + + def __init__( + self, + hass: HomeAssistant, + api: RoutingApi, + config: HERETravelTimeConfig, + ) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._api = api + self.config = config + + async def _async_update_data(self) -> HERERoutingData | None: + """Get the latest data from the HERE Routing API.""" + try: + async with async_timeout.timeout(10): + return await self.hass.async_add_executor_job(self._update) + except NoRouteFoundError as error: + raise UpdateFailed(NO_ROUTE_ERROR_MESSAGE) from error + + def _update(self) -> HERERoutingData | None: + """Get the latest data from the HERE Routing API.""" + if self.config.origin_entity_id is not None: + origin = find_coordinates(self.hass, self.config.origin_entity_id) + else: + origin = self.config.origin + + if self.config.destination_entity_id is not None: + destination = find_coordinates(self.hass, self.config.destination_entity_id) + else: + destination = self.config.destination + if destination is not None and origin is not None: + here_formatted_destination = destination.split(",") + here_formatted_origin = origin.split(",") + arrival: str | None = None + departure: str | None = None + if self.config.arrival is not None: + arrival = convert_time_to_isodate(self.config.arrival) + if self.config.departure is not None: + departure = convert_time_to_isodate(self.config.departure) + + if arrival is None and departure is None: + departure = "now" + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", + here_formatted_origin, + here_formatted_destination, + RouteMode[self.config.route_mode], + RouteMode[self.config.travel_mode], + RouteMode[TRAFFIC_MODE_ENABLED], + arrival, + departure, + ) + + response: RoutingResponse = self._api.public_transport_timetable( + here_formatted_origin, + here_formatted_destination, + True, + [ + RouteMode[self.config.route_mode], + RouteMode[self.config.travel_mode], + RouteMode[TRAFFIC_MODE_ENABLED], + ], + arrival=arrival, + departure=departure, + ) + + _LOGGER.debug( + "Raw response is: %s", response.response # pylint: disable=no-member + ) + + attribution: str | None = None + if "sourceAttribution" in response.response: # pylint: disable=no-member + attribution = build_hass_attribution( + response.response.get("sourceAttribution") + ) # pylint: disable=no-member + route: list = response.response["route"] # pylint: disable=no-member + summary: dict = route[0]["summary"] + waypoint: list = route[0]["waypoint"] + distance: float = summary["distance"] + traffic_time: float = summary["baseTime"] + if self.config.travel_mode in TRAVEL_MODES_VEHICLE: + traffic_time = summary["trafficTime"] + if self.config.units == CONF_UNIT_SYSTEM_IMPERIAL: + # Convert to miles. + distance = distance / 1609.344 + else: + # Convert to kilometers + distance = distance / 1000 + return HERERoutingData( + { + ATTR_ATTRIBUTION: attribution, + ATTR_DURATION: summary["baseTime"] / 60, # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: traffic_time / 60, + ATTR_DISTANCE: distance, + ATTR_ROUTE: response.route_short, + ATTR_ORIGIN: ",".join(here_formatted_origin), + ATTR_DESTINATION: ",".join(here_formatted_destination), + ATTR_ORIGIN_NAME: waypoint[0]["mappedRoadName"], + ATTR_DESTINATION_NAME: waypoint[1]["mappedRoadName"], + } + ) + return None + + +def build_hass_attribution(source_attribution: dict) -> str | None: + """Build a hass frontend ready string out of the sourceAttribution.""" + if (suppliers := source_attribution.get("supplier")) is not None: + supplier_titles = [] + for supplier in suppliers: + if (title := supplier.get("title")) is not None: + supplier_titles.append(title) + joined_supplier_titles = ",".join(supplier_titles) + return f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." + return None + + +def convert_time_to_isodate(simple_time: time) -> str: + """Take a time like 08:00:00 and combine it with the current date.""" + combined = datetime.combine(dt.start_of_local_day(), simple_time) + if combined < datetime.now(): + combined = combined + timedelta(days=1) + return combined.isoformat() diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py new file mode 100644 index 00000000000..a6b958ebf5e --- /dev/null +++ b/homeassistant/components/here_travel_time/const.py @@ -0,0 +1,77 @@ +"""Constants for the HERE Travel Time integration.""" +from homeassistant.const import ( + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) + +DOMAIN = "here_travel_time" +DEFAULT_SCAN_INTERVAL = 300 + +CONF_DESTINATION = "destination" +CONF_ORIGIN = "origin" +CONF_TRAFFIC_MODE = "traffic_mode" +CONF_ROUTE_MODE = "route_mode" +CONF_ARRIVAL = "arrival" +CONF_DEPARTURE = "departure" +CONF_ARRIVAL_TIME = "arrival_time" +CONF_DEPARTURE_TIME = "departure_time" +CONF_TIME_TYPE = "time_type" +CONF_TIME = "time" + +ARRIVAL_TIME = "Arrival Time" +DEPARTURE_TIME = "Departure Time" +TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME] + +DEFAULT_NAME = "HERE Travel Time" + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +TRAVEL_MODE_BICYCLE = "bicycle" +TRAVEL_MODE_CAR = "car" +TRAVEL_MODE_PEDESTRIAN = "pedestrian" +TRAVEL_MODE_PUBLIC = "publicTransport" +TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" +TRAVEL_MODE_TRUCK = "truck" +TRAVEL_MODES = [ + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, +] + +TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] +TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] + +TRAFFIC_MODE_ENABLED = "traffic_enabled" +TRAFFIC_MODE_DISABLED = "traffic_disabled" +TRAFFIC_MODES = [TRAFFIC_MODE_ENABLED, TRAFFIC_MODE_DISABLED] + +ROUTE_MODE_FASTEST = "fastest" +ROUTE_MODE_SHORTEST = "shortest" +ROUTE_MODES = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] + +ICON_BICYCLE = "mdi:bike" +ICON_CAR = "mdi:car" +ICON_PEDESTRIAN = "mdi:walk" +ICON_PUBLIC = "mdi:bus" +ICON_TRUCK = "mdi:truck" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ROUTE = "route" +ATTR_ORIGIN = "origin" +ATTR_DESTINATION = "destination" + +ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM +ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE + +ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" +ATTR_ORIGIN_NAME = "origin_name" +ATTR_DESTINATION_NAME = "destination_name" + +NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py new file mode 100644 index 00000000000..5cea81d1ece --- /dev/null +++ b/homeassistant/components/here_travel_time/model.py @@ -0,0 +1,35 @@ +"""Model Classes for here_travel_time.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import time +from typing import TypedDict + + +class HERERoutingData(TypedDict): + """Routing information calculated from a herepy.RoutingResponse.""" + + ATTR_ATTRIBUTION: str | None + ATTR_DURATION: float + ATTR_DURATION_IN_TRAFFIC: float + ATTR_DISTANCE: float + ATTR_ROUTE: str + ATTR_ORIGIN: str + ATTR_DESTINATION: str + ATTR_ORIGIN_NAME: str + ATTR_DESTINATION_NAME: str + + +@dataclass +class HERETravelTimeConfig: + """Configuration for HereTravelTimeDataUpdateCoordinator.""" + + origin: str | None + destination: str | None + origin_entity_id: str | None + destination_entity_id: str | None + travel_mode: str + route_mode: str + units: str + arrival: time + departure: time diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 8cba2db8bc8..64fdd704a43 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,7 +1,7 @@ """Support for HERE travel time sensors.""" from __future__ import annotations -from datetime import datetime, time, timedelta +from datetime import timedelta import logging import herepy @@ -18,15 +18,16 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - EVENT_HOMEASSISTANT_START, TIME_MINUTES, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import HereTravelTimeDataUpdateCoordinator +from .model import HERETravelTimeConfig _LOGGER = logging.getLogger(__name__) @@ -175,21 +176,29 @@ async def async_setup_platform( destination = None destination_entity_id = config[CONF_DESTINATION_ENTITY_ID] - travel_mode = config[CONF_MODE] traffic_mode = config[CONF_TRAFFIC_MODE] - route_mode = config[CONF_ROUTE_MODE] name = config[CONF_NAME] - units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) - arrival = config.get(CONF_ARRIVAL) - departure = config.get(CONF_DEPARTURE) - here_data = HERETravelTimeData( - here_client, travel_mode, traffic_mode, route_mode, units, arrival, departure + here_travel_time_config = HERETravelTimeConfig( + origin=origin, + destination=destination, + origin_entity_id=origin_entity_id, + destination_entity_id=destination_entity_id, + travel_mode=config[CONF_MODE], + route_mode=config[CONF_ROUTE_MODE], + units=config.get(CONF_UNIT_SYSTEM, hass.config.units.name), + arrival=config.get(CONF_ARRIVAL), + departure=config.get(CONF_DEPARTURE), ) - sensor = HERETravelTimeSensor( - name, origin, destination, origin_entity_id, destination_entity_id, here_data + coordinator = HereTravelTimeDataUpdateCoordinator( + hass, + here_client, + here_travel_time_config, ) + await coordinator.async_config_entry_first_refresh() + + sensor = HERETravelTimeSensor(name, traffic_mode, coordinator) async_add_entities([sensor]) @@ -216,236 +225,66 @@ def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: return True -class HERETravelTimeSensor(SensorEntity): +class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): """Representation of a HERE travel time sensor.""" def __init__( self, name: str, - origin: str, - destination: str, - origin_entity_id: str, - destination_entity_id: str, - here_data: HERETravelTimeData, + traffic_mode: bool, + coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" - self._name = name - self._origin_entity_id = origin_entity_id - self._destination_entity_id = destination_entity_id - self._here_data = here_data - self._unit_of_measurement = TIME_MINUTES - self._attrs = { - ATTR_UNIT_SYSTEM: self._here_data.units, - ATTR_MODE: self._here_data.travel_mode, - ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, - } - if self._origin_entity_id is None: - self._here_data.origin = origin - - if self._destination_entity_id is None: - self._here_data.destination = destination - - async def async_added_to_hass(self) -> None: - """Delay the sensor update to avoid entity not found warnings.""" - - @callback - def delayed_sensor_update(event): - """Update sensor after Home Assistant started.""" - self.async_schedule_update_ha_state(True) - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, delayed_sensor_update - ) + super().__init__(coordinator) + self._traffic_mode = traffic_mode + self._attr_native_unit_of_measurement = TIME_MINUTES + self._attr_name = name @property def native_value(self) -> str | None: """Return the state of the sensor.""" - if self._here_data.traffic_mode and self._here_data.traffic_time is not None: - return str(round(self._here_data.traffic_time / 60)) - if self._here_data.base_time is not None: - return str(round(self._here_data.base_time / 60)) - + if self.coordinator.data is not None: + return str( + round( + self.coordinator.data.get( + ATTR_DURATION_IN_TRAFFIC + if self._traffic_mode + else ATTR_DURATION + ) + ) + ) return None - @property - def name(self) -> str: - """Get the name of the sensor.""" - return self._name - @property def extra_state_attributes( self, ) -> dict[str, None | float | str | bool] | None: """Return the state attributes.""" - if self._here_data.base_time is None: - return None - - res = self._attrs - if self._here_data.attribution is not None: - res[ATTR_ATTRIBUTION] = self._here_data.attribution - res[ATTR_DURATION] = self._here_data.base_time / 60 - res[ATTR_DISTANCE] = self._here_data.distance - res[ATTR_ROUTE] = self._here_data.route - res[ATTR_DURATION_IN_TRAFFIC] = self._here_data.traffic_time / 60 - res[ATTR_ORIGIN] = self._here_data.origin - res[ATTR_DESTINATION] = self._here_data.destination - res[ATTR_ORIGIN_NAME] = self._here_data.origin_name - res[ATTR_DESTINATION_NAME] = self._here_data.destination_name - return res + if self.coordinator.data is not None: + res = { + ATTR_UNIT_SYSTEM: self.coordinator.config.units, + ATTR_MODE: self.coordinator.config.travel_mode, + ATTR_TRAFFIC_MODE: self._traffic_mode, + **self.coordinator.data, + } + res.pop(ATTR_ATTRIBUTION) + return res + return None @property - def native_unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement + def attribution(self) -> str | None: + """Return the attribution.""" + return self.coordinator.data.get(ATTR_ATTRIBUTION) @property def icon(self) -> str: """Icon to use in the frontend depending on travel_mode.""" - if self._here_data.travel_mode == TRAVEL_MODE_BICYCLE: + if self.coordinator.config.travel_mode == TRAVEL_MODE_BICYCLE: return ICON_BICYCLE - if self._here_data.travel_mode == TRAVEL_MODE_PEDESTRIAN: + if self.coordinator.config.travel_mode == TRAVEL_MODE_PEDESTRIAN: return ICON_PEDESTRIAN - if self._here_data.travel_mode in TRAVEL_MODES_PUBLIC: + if self.coordinator.config.travel_mode in TRAVEL_MODES_PUBLIC: return ICON_PUBLIC - if self._here_data.travel_mode == TRAVEL_MODE_TRUCK: + if self.coordinator.config.travel_mode == TRAVEL_MODE_TRUCK: return ICON_TRUCK return ICON_CAR - - async def async_update(self) -> None: - """Update Sensor Information.""" - # Convert device_trackers to HERE friendly location - if self._origin_entity_id is not None: - self._here_data.origin = find_coordinates(self.hass, self._origin_entity_id) - - if self._destination_entity_id is not None: - self._here_data.destination = find_coordinates( - self.hass, self._destination_entity_id - ) - - await self.hass.async_add_executor_job(self._here_data.update) - - -class HERETravelTimeData: - """HERETravelTime data object.""" - - def __init__( - self, - here_client: herepy.RoutingApi, - travel_mode: str, - traffic_mode: bool, - route_mode: str, - units: str, - arrival: datetime, - departure: datetime, - ) -> None: - """Initialize herepy.""" - self.origin = None - self.destination = None - self.travel_mode = travel_mode - self.traffic_mode = traffic_mode - self.route_mode = route_mode - self.arrival = arrival - self.departure = departure - self.attribution = None - self.traffic_time = None - self.distance = None - self.route = None - self.base_time = None - self.origin_name = None - self.destination_name = None - self.units = units - self._client = here_client - self.combine_change = True - - def update(self) -> None: - """Get the latest data from HERE.""" - if self.traffic_mode: - traffic_mode = TRAFFIC_MODE_ENABLED - else: - traffic_mode = TRAFFIC_MODE_DISABLED - - if self.destination is not None and self.origin is not None: - # Convert location to HERE friendly location - destination = self.destination.split(",") - origin = self.origin.split(",") - if (arrival := self.arrival) is not None: - arrival = convert_time_to_isodate(arrival) - if (departure := self.departure) is not None: - departure = convert_time_to_isodate(departure) - - if departure is None and arrival is None: - departure = "now" - - _LOGGER.debug( - "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", - origin, - destination, - herepy.RouteMode[self.route_mode], - herepy.RouteMode[self.travel_mode], - herepy.RouteMode[traffic_mode], - arrival, - departure, - ) - - try: - response = self._client.public_transport_timetable( - origin, - destination, - self.combine_change, - [ - herepy.RouteMode[self.route_mode], - herepy.RouteMode[self.travel_mode], - herepy.RouteMode[traffic_mode], - ], - arrival=arrival, - departure=departure, - ) - except herepy.NoRouteFoundError: - # Better error message for cryptic no route error codes - _LOGGER.error(NO_ROUTE_ERROR_MESSAGE) - return - - _LOGGER.debug("Raw response is: %s", response.response) - - source_attribution = response.response.get("sourceAttribution") - if source_attribution is not None: - self.attribution = self._build_hass_attribution(source_attribution) - route = response.response["route"] - summary = route[0]["summary"] - waypoint = route[0]["waypoint"] - self.base_time = summary["baseTime"] - if self.travel_mode in TRAVEL_MODES_VEHICLE: - self.traffic_time = summary["trafficTime"] - else: - self.traffic_time = self.base_time - distance = summary["distance"] - if self.units == CONF_UNIT_SYSTEM_IMPERIAL: - # Convert to miles. - self.distance = distance / 1609.344 - else: - # Convert to kilometers - self.distance = distance / 1000 - self.route = response.route_short - self.origin_name = waypoint[0]["mappedRoadName"] - self.destination_name = waypoint[1]["mappedRoadName"] - - @staticmethod - def _build_hass_attribution(source_attribution: dict) -> str | None: - """Build a hass frontend ready string out of the sourceAttribution.""" - suppliers = source_attribution.get("supplier") - if suppliers is not None: - supplier_titles = [] - for supplier in suppliers: - if (title := supplier.get("title")) is not None: - supplier_titles.append(title) - joined_supplier_titles = ",".join(supplier_titles) - attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." - return attribution - - -def convert_time_to_isodate(simple_time: time) -> str: - """Take a time like 08:00:00 and combine it with the current date.""" - combined = datetime.combine(dt.start_of_local_day(), simple_time) - if combined < datetime.now(): - combined = combined + timedelta(days=1) - return combined.isoformat() diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 03d2313da2e..1bb9a380c29 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -49,16 +49,50 @@ PLATFORM = "here_travel_time" @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system", + "mode,icon,traffic_mode,unit_system,expected_state,expected_distance,expected_duration_in_traffic", [ - (TRAVEL_MODE_CAR, ICON_CAR, True, "metric"), - (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric"), - (TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, False, "imperial"), - (TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, False, "imperial"), - (TRAVEL_MODE_TRUCK, ICON_TRUCK, True, "metric"), + (TRAVEL_MODE_CAR, ICON_CAR, True, "metric", "31", 23.903, 31.016666666666666), + (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric", "30", 23.903, 30.05), + ( + TRAVEL_MODE_PEDESTRIAN, + ICON_PEDESTRIAN, + False, + "imperial", + "30", + 14.852635608048994, + 30.05, + ), + ( + TRAVEL_MODE_PUBLIC_TIME_TABLE, + ICON_PUBLIC, + False, + "imperial", + "30", + 14.852635608048994, + 30.05, + ), + ( + TRAVEL_MODE_TRUCK, + ICON_TRUCK, + True, + "metric", + "31", + 23.903, + 31.016666666666666, + ), ], ) -async def test_sensor(hass, mode, icon, traffic_mode, unit_system, valid_response): +async def test_sensor( + hass, + mode, + icon, + traffic_mode, + unit_system, + expected_state, + expected_distance, + expected_duration_in_traffic, + valid_response, +): """Test that sensor works.""" config = { DOMAIN: { @@ -85,25 +119,18 @@ async def test_sensor(hass, mode, icon, traffic_mode, unit_system, valid_respons sensor.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) - if traffic_mode: - assert sensor.state == "31" - else: - assert sensor.state == "30" + assert sensor.state == expected_state assert sensor.attributes.get(ATTR_DURATION) == 30.05 - if unit_system == "metric": - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 - else: - assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 + assert sensor.attributes.get(ATTR_DISTANCE) == expected_distance assert sensor.attributes.get(ATTR_ROUTE) == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system - if mode in TRAVEL_MODES_VEHICLE: - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666 - else: - assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 30.05 + assert ( + sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == expected_duration_in_traffic + ) assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] ) @@ -168,7 +195,7 @@ async def test_entity_ids(hass, valid_response): assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 -async def test_route_not_found(hass, caplog, valid_response): +async def test_route_not_found(hass, caplog): """Test that route not found error is correctly handled.""" config = { DOMAIN: { @@ -181,12 +208,15 @@ async def test_route_not_found(hass, caplog, valid_response): "api_key": API_KEY, } } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() with patch( + "homeassistant.components.here_travel_time.sensor._are_valid_client_credentials", + return_value=True, + ), patch( "herepy.RoutingApi.public_transport_timetable", side_effect=NoRouteFoundError, ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() From 23a630e0bcbd2aec6a598a19ebaf2929eba97e5b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 05:06:43 +0100 Subject: [PATCH 0669/1054] Update Times of the Day tests to use freezegun (#68327) --- tests/components/tod/test_binary_sensor.py | 935 +++++++-------------- 1 file changed, 309 insertions(+), 626 deletions(-) diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index ecdabdb910d..ed675267c65 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,7 +1,7 @@ """Test Times of the Day Binary Sensor.""" from datetime import datetime, timedelta -from unittest.mock import patch +from freezegun import freeze_time import pytest from homeassistant.const import STATE_OFF, STATE_ON @@ -19,11 +19,24 @@ def mock_legacy_time(legacy_patchable_time): yield +@pytest.fixture +def hass_time_zone(): + """Return default hass timezone.""" + return "US/Pacific" + + @pytest.fixture(autouse=True) -def setup_fixture(hass): +def setup_fixture(hass, hass_time_zone): """Set up things to be run when tests are started.""" hass.config.latitude = 50.27583 hass.config.longitude = 18.98583 + hass.config.set_time_zone(hass_time_zone) + + +@pytest.fixture +def hass_tz_info(hass): + """Return timezone info for the hass timezone.""" + return dt_util.get_time_zone(hass.config.time_zone) async def test_setup(hass): @@ -58,11 +71,9 @@ async def test_setup_no_sensors(hass): ) +@freeze_time("2019-01-10 18:43:00-08:00") async def test_in_period_on_start(hass): """Test simple setting.""" - test_time = datetime( - 2019, 1, 10, 18, 43, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) config = { "binary_sensor": [ { @@ -73,164 +84,123 @@ async def test_in_period_on_start(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.evening") assert state.state == STATE_ON +@freeze_time("2019-01-10 22:30:00-08:00") async def test_midnight_turnover_before_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime( - 2019, 1, 10, 22, 30, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_ON -async def test_midnight_turnover_after_midnight_inside_period(hass): +async def test_midnight_turnover_after_midnight_inside_period( + hass, freezer, hass_tz_info +): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime( - 2019, 1, 10, 21, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) + test_time = datetime(2019, 1, 10, 21, 0, 0, tzinfo=hass_tz_info) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + freezer.move_to(test_time) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() - state = hass.states.get("binary_sensor.night") - assert state.state == STATE_OFF + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_OFF - await hass.async_block_till_done() + await hass.async_block_till_done() - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time + timedelta(hours=1), - ): + freezer.move_to(test_time + timedelta(hours=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) - hass.bus.async_fire( - ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: test_time + timedelta(hours=1)} - ) - - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.night") - assert state.state == STATE_ON + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON +@freeze_time("2019-01-10 20:30:00-08:00") async def test_midnight_turnover_before_midnight_outside_period(hass): """Test midnight turnover setting before midnight outside period.""" - test_time = datetime(2019, 1, 10, 20, 30, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF +@freeze_time("2019-01-10 10:00:00-08:00") async def test_after_happens_tomorrow(hass): """Test when both before and after are in the future, and after is later than before.""" - test_time = datetime(2019, 1, 10, 10, 00, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "23:00", "before": "12:00"} ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_ON -async def test_midnight_turnover_after_midnight_outside_period(hass): +async def test_midnight_turnover_after_midnight_outside_period( + hass, freezer, hass_tz_info +): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime( - 2019, 1, 10, 20, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) + test_time = datetime(2019, 1, 10, 20, 0, 0, tzinfo=hass_tz_info) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + freezer.move_to(test_time) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF - switchover_time = datetime( - 2019, 1, 11, 4, 59, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=switchover_time, - ): + switchover_time = datetime(2019, 1, 11, 4, 59, 0, tzinfo=hass_tz_info) + freezer.move_to(switchover_time) - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: switchover_time}) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.night") - assert state.state == STATE_ON + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_ON - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=switchover_time + timedelta(minutes=1, seconds=1), - ): + freezer.move_to(switchover_time + timedelta(minutes=1, seconds=1)) - hass.bus.async_fire( - ha.EVENT_TIME_CHANGED, - {ha.ATTR_NOW: switchover_time + timedelta(minutes=1, seconds=1)}, - ) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) - await hass.async_block_till_done() - state = hass.states.get("binary_sensor.night") - assert state.state == STATE_OFF + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.night") + assert state.state == STATE_OFF -async def test_from_sunrise_to_sunset(hass): +async def test_from_sunrise_to_sunset(hass, freezer, hass_tz_info): """Test period from sunrise to sunset.""" - test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) + test_time = datetime(2019, 1, 12, tzinfo=hass_tz_info) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) ) @@ -248,88 +218,46 @@ async def test_from_sunrise_to_sunset(hass): ] } entity_id = "binary_sensor.day" - testtime = sunrise + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() - - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - testtime = sunrise - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = sunrise + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(sunrise + timedelta(seconds=-1)) + await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(sunrise) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = sunset - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - + freezer.move_to(sunrise + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = sunset + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): + freezer.move_to(sunset + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() + freezer.move_to(sunset) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(sunset + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF -async def test_from_sunset_to_sunrise(hass): +async def test_from_sunset_to_sunrise(hass, freezer, hass_tz_info): """Test period from sunset to sunrise.""" - test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) + test_time = datetime(2019, 1, 12, tzinfo=hass_tz_info) sunset = dt_util.as_local(get_astral_event_date(hass, "sunset", test_time)) sunrise = dt_util.as_local(get_astral_event_next(hass, "sunrise", sunset)) # assert sunset == sunrise @@ -344,90 +272,52 @@ async def test_from_sunset_to_sunrise(hass): ] } entity_id = "binary_sensor.night" - testtime = sunset + timedelta(minutes=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + freezer.move_to(sunset + timedelta(seconds=-1)) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(sunset) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = sunset - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): + freezer.move_to(sunset + timedelta(minutes=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() + freezer.move_to(sunrise + timedelta(minutes=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - state = hass.states.get(entity_id) - assert state.state == STATE_ON + freezer.move_to(sunrise) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset + timedelta(minutes=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = sunrise + timedelta(minutes=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = sunrise - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - await hass.async_block_till_done() - # assert state == "dupa" - assert state.state == STATE_OFF - - testtime = sunrise + timedelta(minutes=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(sunrise + timedelta(minutes=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF -async def test_offset(hass): +async def test_offset(hass, freezer, hass_tz_info): """Test offset.""" - after = datetime( - 2019, 1, 10, 18, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) + timedelta(hours=1, minutes=34) + after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=hass_tz_info) + timedelta( + hours=1, minutes=34 + ) - before = datetime( - 2019, 1, 10, 22, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) + timedelta(hours=1, minutes=45) + before = datetime(2019, 1, 10, 22, 0, 0, tzinfo=hass_tz_info) + timedelta( + hours=1, minutes=45 + ) entity_id = "binary_sensor.evening" config = { @@ -442,67 +332,42 @@ async def test_offset(hass): } ] } - testtime = after + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() - + freezer.move_to(after + timedelta(seconds=-1)) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == STATE_OFF - testtime = after - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() + freezer.move_to(after) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - state = hass.states.get(entity_id) - assert state.state == STATE_ON + freezer.move_to(before + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = before + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() + freezer.move_to(before) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = before - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - testtime = before + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(before + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF -async def test_offset_overnight(hass): +async def test_offset_overnight(hass, freezer, hass_tz_info): """Test offset overnight.""" - after = datetime( - 2019, 1, 10, 18, 0, 0, tzinfo=dt_util.get_time_zone(hass.config.time_zone) - ) + timedelta(hours=1, minutes=34) + after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=hass_tz_info) + timedelta( + hours=1, minutes=34 + ) entity_id = "binary_sensor.evening" config = { "binary_sensor": [ @@ -516,35 +381,25 @@ async def test_offset_overnight(hass): } ] } - testtime = after + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() - + freezer.move_to(after + timedelta(seconds=-1)) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.state == STATE_OFF - testtime = after - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON + freezer.move_to(after) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON -async def test_norwegian_case_winter(hass): +async def test_norwegian_case_winter(hass, freezer, hass_tz_info): """Test location in Norway where the sun doesn't set in summer.""" hass.config.latitude = 69.6 hass.config.longitude = 18.8 - test_time = datetime(2010, 1, 1, tzinfo=dt_util.UTC) + test_time = datetime(2010, 1, 1, tzinfo=hass_tz_info) sunrise = dt_util.as_local( get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) ) @@ -562,104 +417,56 @@ async def test_norwegian_case_winter(hass): ] } entity_id = "binary_sensor.day" - testtime = test_time - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() - - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - testtime = sunrise + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - testtime = sunrise - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = sunrise + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(test_time) + await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(sunrise + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - + freezer.move_to(sunrise) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = sunset + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): + freezer.move_to(sunrise + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() + freezer.move_to(sunset + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(sunset) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + freezer.move_to(sunset + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF -async def test_norwegian_case_summer(hass): +async def test_norwegian_case_summer(hass, freezer, hass_tz_info): """Test location in Norway where the sun doesn't set in summer.""" hass.config.latitude = 69.6 hass.config.longitude = 18.8 hass.config.elevation = 10.0 - test_time = datetime(2010, 6, 1, tzinfo=dt_util.UTC) + test_time = datetime(2010, 6, 1, tzinfo=hass_tz_info) sunrise = dt_util.as_local( get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) @@ -678,100 +485,52 @@ async def test_norwegian_case_summer(hass): ] } entity_id = "binary_sensor.day" - testtime = test_time - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() - - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - testtime = sunrise + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - testtime = sunrise - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = sunrise + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(test_time) + await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(sunrise + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - + freezer.move_to(sunrise) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = sunset + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): + freezer.move_to(sunrise + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() + freezer.move_to(sunset + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(sunset) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + freezer.move_to(sunset + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF -async def test_sun_offset(hass): +async def test_sun_offset(hass, freezer, hass_tz_info): """Test sun event with offset.""" - test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) + test_time = datetime(2019, 1, 12, tzinfo=hass_tz_info) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) + timedelta(hours=-1, minutes=-30) @@ -793,106 +552,61 @@ async def test_sun_offset(hass): ] } entity_id = "binary_sensor.day" - testtime = sunrise + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + freezer.move_to(sunrise + timedelta(seconds=-1)) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + freezer.move_to(sunrise) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - testtime = sunrise - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): + freezer.move_to(sunrise + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - - testtime = sunrise + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON + freezer.move_to(sunset + timedelta(seconds=-1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON await hass.async_block_till_done() - testtime = sunset + timedelta(seconds=-1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - + freezer.move_to(sunset) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_OFF - testtime = sunset - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - + freezer.move_to(sunset + timedelta(seconds=1)) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) await hass.async_block_till_done() - - testtime = sunset + timedelta(seconds=1) - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF + state = hass.states.get(entity_id) + assert state.state == STATE_OFF test_time = test_time + timedelta(days=1) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) + timedelta(hours=-1, minutes=-30) ) - testtime = sunrise - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, - ): - - hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: testtime}) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state.state == STATE_ON + freezer.move_to(sunrise) + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: dt_util.utcnow()}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON -async def test_dst(hass): +async def test_dst(hass, freezer, hass_tz_info): """Test sun event with offset.""" - hass.config.set_time_zone("CET") - test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.UTC) + hass.config.time_zone = "CET" + dt_util.set_default_time_zone(dt_util.get_time_zone("CET")) + test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=hass_tz_info) config = { "binary_sensor": [ {"platform": "tod", "name": "Day", "after": "2:30", "before": "2:40"} @@ -902,25 +616,22 @@ async def test_dst(hass): # after 2019-03-30 03:00 CET the next update should ge scheduled # at 3:30 not 2:30 local time entity_id = "binary_sensor.day" - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + freezer.move_to(test_time) + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.attributes["after"] == "2019-03-31T03:30:00+02:00" - assert state.attributes["before"] == "2019-03-31T03:40:00+02:00" - assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00" - assert state.state == STATE_OFF + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.attributes["after"] == "2019-03-31T03:30:00+02:00" + assert state.attributes["before"] == "2019-03-31T03:40:00+02:00" + assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00" + assert state.state == STATE_OFF +@freeze_time("2019-01-10 18:43:00") +@pytest.mark.parametrize("hass_time_zone", ("UTC",)) async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): """Test simple before after.""" - hass.config.set_time_zone("UTC") - test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -931,12 +642,8 @@ async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF @@ -945,10 +652,10 @@ async def test_simple_before_after_does_not_loop_utc_not_in_range(hass): assert state.attributes["next_update"] == "2019-01-10T22:00:00+00:00" +@freeze_time("2019-01-10 22:43:00") +@pytest.mark.parametrize("hass_time_zone", ("UTC",)) async def test_simple_before_after_does_not_loop_utc_in_range(hass): """Test simple before after.""" - hass.config.set_time_zone("UTC") - test_time = datetime(2019, 1, 10, 22, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -959,12 +666,8 @@ async def test_simple_before_after_does_not_loop_utc_in_range(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_ON @@ -973,10 +676,10 @@ async def test_simple_before_after_does_not_loop_utc_in_range(hass): assert state.attributes["next_update"] == "2019-01-11T06:00:00+00:00" +@freeze_time("2019-01-11 06:00:00") +@pytest.mark.parametrize("hass_time_zone", ("UTC",)) async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): """Test simple before after.""" - hass.config.set_time_zone("UTC") - test_time = datetime(2019, 1, 11, 6, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -987,12 +690,8 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF @@ -1001,10 +700,10 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_before(hass): assert state.attributes["next_update"] == "2019-01-11T22:00:00+00:00" +@freeze_time("2019-01-10 22:00:00") +@pytest.mark.parametrize("hass_time_zone", ("UTC",)) async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): """Test simple before after.""" - hass.config.set_time_zone("UTC") - test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -1015,12 +714,8 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.night") assert state.state == STATE_ON @@ -1029,10 +724,10 @@ async def test_simple_before_after_does_not_loop_utc_fire_at_after(hass): assert state.attributes["next_update"] == "2019-01-11T06:00:00+00:00" +@freeze_time("2019-01-10 22:00:00") +@pytest.mark.parametrize("hass_time_zone", ("UTC",)) async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): """Test simple before after.""" - hass.config.set_time_zone("UTC") - test_time = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -1043,12 +738,8 @@ async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.morning") assert state.state == STATE_OFF @@ -1057,10 +748,10 @@ async def test_simple_before_after_does_not_loop_utc_both_before_now(hass): assert state.attributes["next_update"] == "2019-01-11T00:00:00+00:00" +@freeze_time("2019-01-10 17:43:00+01:00") +@pytest.mark.parametrize("hass_time_zone", ("Europe/Berlin",)) async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): """Test simple before after.""" - hass.config.set_time_zone("Europe/Berlin") - test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -1071,12 +762,8 @@ async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.dark") assert state.state == STATE_OFF @@ -1085,10 +772,10 @@ async def test_simple_before_after_does_not_loop_berlin_not_in_range(hass): assert state.attributes["next_update"] == "2019-01-11T00:00:00+01:00" +@freeze_time("2019-01-11 00:43:00+01:00") +@pytest.mark.parametrize("hass_time_zone", ("Europe/Berlin",)) async def test_simple_before_after_does_not_loop_berlin_in_range(hass): """Test simple before after.""" - hass.config.set_time_zone("Europe/Berlin") - test_time = datetime(2019, 1, 10, 23, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -1099,12 +786,8 @@ async def test_simple_before_after_does_not_loop_berlin_in_range(hass): } ] } - with patch( - "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=test_time, - ): - await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.dark") assert state.state == STATE_ON From 473647091541b4f3786ddc3d1168ecf9ae720cc7 Mon Sep 17 00:00:00 2001 From: James Hodgkinson Date: Thu, 24 Mar 2022 14:42:21 +1000 Subject: [PATCH 0670/1054] Bump pyaussiebb to 0.0.15 (#68600) --- homeassistant/components/aussie_broadband/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aussie_broadband/manifest.json b/homeassistant/components/aussie_broadband/manifest.json index 0823000956a..b3c298f220c 100644 --- a/homeassistant/components/aussie_broadband/manifest.json +++ b/homeassistant/components/aussie_broadband/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aussie_broadband", "requirements": [ - "pyaussiebb==0.0.14" + "pyaussiebb==0.0.15" ], "codeowners": [ "@nickw444", diff --git a/requirements_all.txt b/requirements_all.txt index 9da57ef089a..ad14a435446 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1356,7 +1356,7 @@ pyatome==0.1.1 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.14 +pyaussiebb==0.0.15 # homeassistant.components.balboa pybalboa==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b60aa4516f5..c7cda0ac7de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -899,7 +899,7 @@ pyatmo==6.2.4 pyatv==0.10.0 # homeassistant.components.aussie_broadband -pyaussiebb==0.0.14 +pyaussiebb==0.0.15 # homeassistant.components.balboa pybalboa==0.13 From 61cc8e32f3fb0d30c1f313d378293be9f5624975 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 10:12:01 +0100 Subject: [PATCH 0671/1054] Include has_mean + has_sum in statistics metadata WS response (#68546) * Include has_mean + has_sum in statistics metadata WS response * Don't include has_mean/has_sum in history/list_statistic_ids * Adjust tests * Do include has_mean/has_sum in history/list_statistic_ids --- .../components/recorder/statistics.py | 6 +- homeassistant/components/sensor/recorder.py | 4 ++ tests/components/demo/test_init.py | 4 ++ tests/components/history/test_init.py | 6 ++ tests/components/recorder/test_statistics.py | 2 + .../components/recorder/test_websocket_api.py | 8 +++ tests/components/sensor/test_recorder.py | 68 +++++++++++++++++++ 7 files changed, 97 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 50e987a5533..67b2568fc53 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -753,7 +753,7 @@ def list_statistic_ids( hass: HomeAssistant, statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, -) -> list[dict | None]: +) -> list[dict]: """Return all statistic_ids (or filtered one) and unit of measurement. Queries the database for existing statistic_ids, as well as integrations with @@ -777,6 +777,8 @@ def list_statistic_ids( result = { meta["statistic_id"]: { + "has_mean": meta["has_mean"], + "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], "unit_of_measurement": meta["unit_of_measurement"], @@ -805,6 +807,8 @@ def list_statistic_ids( return [ { "statistic_id": _id, + "has_mean": info["has_mean"], + "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], "unit_of_measurement": info["unit_of_measurement"], diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 75e117d9834..4afc514d457 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -627,6 +627,8 @@ def list_statistic_ids( if device_class not in UNIT_CONVERSIONS: result[state.entity_id] = { + "has_mean": "mean" in provided_statistics, + "has_sum": "sum" in provided_statistics, "source": RECORDER_DOMAIN, "unit_of_measurement": native_unit, } @@ -637,6 +639,8 @@ def list_statistic_ids( statistics_unit = DEVICE_CLASS_UNITS[device_class] result[state.entity_id] = { + "has_mean": "mean" in provided_statistics, + "has_sum": "sum" in provided_statistics, "source": RECORDER_DOMAIN, "unit_of_measurement": statistics_unit, } diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index f25d1a57b34..beffa3ba332 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -57,12 +57,16 @@ async def test_demo_statistics(hass, recorder_mock): list_statistic_ids, hass ) assert { + "has_mean": True, + "has_sum": False, "name": None, "source": "demo", "statistic_id": "demo:temperature_outdoor", "unit_of_measurement": "°C", } in statistic_ids assert { + "has_mean": False, + "has_sum": True, "name": None, "source": "demo", "statistic_id": "demo:energy_consumption", diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 1590dcf1ed0..f9fb7b49fb0 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1038,6 +1038,8 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": unit, @@ -1056,6 +1058,8 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": unit, @@ -1076,6 +1080,8 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": unit, diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 29853c2cc0e..2e8635c6c6e 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -401,6 +401,8 @@ async def test_external_statistics(hass, hass_ws_client, caplog): statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "has_mean": False, + "has_sum": True, "statistic_id": "test:total_energy_import", "name": "Total imported energy", "source": "test", diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 33478b76bcb..3b09350417b 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -230,6 +230,8 @@ async def test_update_statistics_metadata(hass, hass_ws_client, new_unit): assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "W", @@ -254,6 +256,8 @@ async def test_update_statistics_metadata(hass, hass_ws_client, new_unit): assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": new_unit, @@ -525,6 +529,8 @@ async def test_get_statistics_metadata(hass, hass_ws_client, units, attributes, assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": unit, @@ -549,6 +555,8 @@ async def test_get_statistics_metadata(hass, hass_ws_client, units, attributes, assert response["result"] == [ { "statistic_id": "sensor.test", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": unit, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index f26b5e7ead3..33550495589 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -122,6 +122,8 @@ def test_compile_hourly_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -185,6 +187,8 @@ def test_compile_hourly_statistics_purged_state_changes( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -251,18 +255,24 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "°C", }, { "statistic_id": "sensor.test6", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "°C", }, { "statistic_id": "sensor.test7", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "°C", @@ -386,6 +396,8 @@ async def test_compile_hourly_sum_statistics_amount( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": display_unit, @@ -565,6 +577,8 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -654,6 +668,8 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -727,6 +743,8 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -837,6 +855,8 @@ def test_compile_hourly_sum_statistics_negative_state( statistic_ids = list_statistic_ids(hass) assert { "name": None, + "has_mean": False, + "has_sum": True, "source": "recorder", "statistic_id": entity_id, "unit_of_measurement": native_unit, @@ -914,6 +934,8 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1006,6 +1028,8 @@ def test_compile_hourly_sum_statistics_total_increasing( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1109,6 +1133,8 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1200,6 +1226,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": "kWh", @@ -1289,18 +1317,24 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": "kWh", }, { "statistic_id": "sensor.test2", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": "kWh", }, { "statistic_id": "sensor.test3", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": "kWh", @@ -1622,6 +1656,8 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": statistic_type == "mean", + "has_sum": statistic_type == "sum", "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1633,6 +1669,8 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": statistic_type == "mean", + "has_sum": statistic_type == "sum", "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1710,6 +1748,8 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1742,6 +1782,8 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1805,6 +1847,8 @@ def test_compile_hourly_statistics_changing_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "cats", @@ -1858,6 +1902,8 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1888,6 +1934,8 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": native_unit, @@ -1941,6 +1989,8 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": state_unit, @@ -1987,6 +2037,8 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": state_unit, @@ -2041,6 +2093,8 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": statistic_unit, @@ -2087,6 +2141,8 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": statistic_unit, @@ -2144,6 +2200,8 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": None, @@ -2176,6 +2234,8 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": None, @@ -2372,24 +2432,32 @@ def test_compile_statistics_hourly_daily_monthly_summary( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "%", }, { "statistic_id": "sensor.test2", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "%", }, { "statistic_id": "sensor.test3", + "has_mean": True, + "has_sum": False, "name": None, "source": "recorder", "unit_of_measurement": "%", }, { "statistic_id": "sensor.test4", + "has_mean": False, + "has_sum": True, "name": None, "source": "recorder", "unit_of_measurement": "EUR", From eca5fb5d54f50e7a103338ba74a94ff7ee0882db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Mar 2022 12:17:52 +0100 Subject: [PATCH 0672/1054] Move remove all light option from group config flow (#68609) --- homeassistant/components/group/config_flow.py | 9 ++--- homeassistant/components/group/light.py | 2 +- homeassistant/components/group/strings.json | 29 +++++++++------- .../components/group/translations/en.json | 33 +++++++++++-------- tests/components/group/test_config_flow.py | 3 +- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 8b4bf20b0e5..83485bd16bf 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -57,11 +57,6 @@ BINARY_SENSOR_CONFIG_SCHEMA = vol.Schema( {vol.Required("name"): selector.selector({"text": {}})} ).extend(BINARY_SENSOR_OPTIONS_SCHEMA.schema) -LIGHT_CONFIG_SCHEMA = vol.Schema( - {vol.Required("name"): selector.selector({"text": {}})} -).extend(LIGHT_OPTIONS_SCHEMA.schema) - - GROUP_TYPES = ["binary_sensor", "cover", "fan", "light", "media_player"] @@ -91,7 +86,9 @@ CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { basic_group_config_schema("cover"), set_group_type("cover") ), "fan": HelperFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")), - "light": HelperFlowFormStep(LIGHT_CONFIG_SCHEMA, set_group_type("light")), + "light": HelperFlowFormStep( + basic_group_config_schema("light"), set_group_type("light") + ), "media_player": HelperFlowFormStep( basic_group_config_schema("media_player"), set_group_type("media_player") ), diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e5c87e91889..aa9e6ad0b53 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -109,7 +109,7 @@ async def async_setup_entry( entities = er.async_validate_entity_ids( registry, config_entry.options[CONF_ENTITIES] ) - mode = config_entry.options[CONF_ALL] + mode = config_entry.options.get(CONF_ALL, False) async_add_entities( [LightGroup(config_entry.entry_id, config_entry.title, entities, mode)] diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 440ec7740ab..cc42e150447 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -40,10 +40,8 @@ } }, "light": { - "description": "[%key:component::group::config::step::binary_sensor::description%]", "title": "[%key:component::group::config::step::user::title%]", "data": { - "all": "[%key:component::group::config::step::binary_sensor::data::all%]", "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" @@ -61,31 +59,38 @@ }, "options": { "step": { - "binary_sensor_options": { + "binary_sensor": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", - "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, - "cover_options": { + "cover": { "data": { - "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, - "fan_options": { + "fan": { "data": { - "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, - "light_options": { + "light": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { "all": "[%key:component::group::config::step::binary_sensor::data::all%]", - "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, - "media_player_options": { + "media_player": { "data": { - "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]" + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } } } diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index 57a24b0bdba..bb2b7ec8825 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -29,12 +29,10 @@ }, "light": { "data": { - "all": "All entities", "entities": "Members", "hide_members": "Hide members", "name": "Name" }, - "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", "title": "New Group" }, "media_player": { @@ -60,31 +58,38 @@ }, "options": { "step": { - "binary_sensor_options": { + "binary_sensor": { "data": { "all": "All entities", - "entities": "Members" - } + "entities": "Members", + "hide_members": "Hide members" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, - "cover_options": { + "cover": { "data": { - "entities": "Members" + "entities": "Members", + "hide_members": "Hide members" } }, - "fan_options": { + "fan": { "data": { - "entities": "Members" + "entities": "Members", + "hide_members": "Hide members" } }, - "light_options": { + "light": { "data": { "all": "All entities", - "entities": "Members" - } + "entities": "Members", + "hide_members": "Hide members" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, - "media_player_options": { + "media_player": { "data": { - "entities": "Members" + "entities": "Members", + "hide_members": "Hide members" } } } diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 9c2657f0001..c9f5768fc8f 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -23,8 +23,7 @@ from tests.common import MockConfigEntry ("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}), ("cover", "open", "open", {}, {}, {}, {}), ("fan", "on", "on", {}, {}, {}, {}), - ("light", "on", "on", {}, {}, {"all": False}, {}), - ("light", "on", "on", {}, {"all": True}, {"all": True}, {}), + ("light", "on", "on", {}, {}, {}, {}), ("media_player", "on", "on", {}, {}, {}, {}), ), ) From de407709262c8476745e26b95c0c3e94a7f4a545 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 24 Mar 2022 13:18:19 +0100 Subject: [PATCH 0673/1054] Add diagnostics support to Forecast.Solar (#65063) Co-authored-by: Franck Nijhof --- .../components/forecast_solar/__init__.py | 4 +- .../components/forecast_solar/diagnostics.py | 58 +++++++++++++++++++ tests/components/forecast_solar/conftest.py | 13 +++++ .../forecast_solar/test_diagnostics.py | 58 +++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/forecast_solar/diagnostics.py create mode 100644 tests/components/forecast_solar/test_diagnostics.py diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index d4e8e3af969..18d542c1d3b 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from forecast_solar import ForecastSolar +from forecast_solar import Estimate, ForecastSolar from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if api_key is not None: update_interval = timedelta(minutes=30) - coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + coordinator: DataUpdateCoordinator[Estimate] = DataUpdateCoordinator( hass, logging.getLogger(__name__), name=DOMAIN, diff --git a/homeassistant/components/forecast_solar/diagnostics.py b/homeassistant/components/forecast_solar/diagnostics.py new file mode 100644 index 00000000000..7fdcd22d0fc --- /dev/null +++ b/homeassistant/components/forecast_solar/diagnostics.py @@ -0,0 +1,58 @@ +"""Diagnostics support for Forecast.Solar integration.""" +from __future__ import annotations + +from typing import Any + +from forecast_solar import Estimate + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +TO_REDACT = { + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: DataUpdateCoordinator[Estimate] = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + "options": async_redact_data(entry.options, TO_REDACT), + }, + "data": { + "energy_production_today": coordinator.data.energy_production_today, + "energy_production_tomorrow": coordinator.data.energy_production_tomorrow, + "energy_current_hour": coordinator.data.energy_current_hour, + "power_production_now": coordinator.data.power_production_now, + "watts": { + watt_datetime.isoformat(): watt_value + for watt_datetime, watt_value in coordinator.data.watts.items() + }, + "wh_days": { + wh_datetime.isoformat(): wh_value + for wh_datetime, wh_value in coordinator.data.wh_days.items() + }, + "wh_hours": { + wh_datetime.isoformat(): wh_value + for wh_datetime, wh_value in coordinator.data.wh_hours.items() + }, + }, + "account": { + "type": coordinator.data.account_type.value, + "rate_limit": coordinator.data.api_rate_limit, + "timezone": coordinator.data.timezone, + }, + } diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 0d9f76b367e..31256c95866 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -59,6 +59,7 @@ def mock_forecast_solar(hass) -> Generator[None, MagicMock, None]: estimate = MagicMock(spec=models.Estimate) estimate.now.return_value = now estimate.timezone = "Europe/Amsterdam" + estimate.api_rate_limit = 60 estimate.account_type.value = "public" estimate.energy_production_today = 100000 estimate.energy_production_tomorrow = 200000 @@ -80,6 +81,18 @@ def mock_forecast_solar(hass) -> Generator[None, MagicMock, None]: estimate.sum_energy_production.side_effect = { 1: 900000, }.get + estimate.watts = { + datetime(2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 10, + datetime(2022, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 100, + } + estimate.wh_days = { + datetime(2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 20, + datetime(2022, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 200, + } + estimate.wh_hours = { + datetime(2021, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 30, + datetime(2022, 6, 27, 13, 0, tzinfo=dt_util.DEFAULT_TIME_ZONE): 300, + } forecast_solar.estimate.return_value = estimate yield forecast_solar diff --git a/tests/components/forecast_solar/test_diagnostics.py b/tests/components/forecast_solar/test_diagnostics.py new file mode 100644 index 00000000000..3c388951009 --- /dev/null +++ b/tests/components/forecast_solar/test_diagnostics.py @@ -0,0 +1,58 @@ +"""Tests for the diagnostics data provided by the Forecast.Solar integration.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "Green House", + "data": { + "latitude": REDACTED, + "longitude": REDACTED, + }, + "options": { + "api_key": REDACTED, + "declination": 30, + "azimuth": 190, + "modules power": 5100, + "damping": 0.5, + "inverter_size": 2000, + }, + }, + "data": { + "energy_production_today": 100000, + "energy_production_tomorrow": 200000, + "energy_current_hour": 800000, + "power_production_now": 300000, + "watts": { + "2021-06-27T13:00:00-07:00": 10, + "2022-06-27T13:00:00-07:00": 100, + }, + "wh_days": { + "2021-06-27T13:00:00-07:00": 20, + "2022-06-27T13:00:00-07:00": 200, + }, + "wh_hours": { + "2021-06-27T13:00:00-07:00": 30, + "2022-06-27T13:00:00-07:00": 300, + }, + }, + "account": { + "type": "public", + "rate_limit": 60, + "timezone": "Europe/Amsterdam", + }, + } From 15cffbe4966436f2923e4408083fb47d570e25a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Mar 2022 13:19:11 +0100 Subject: [PATCH 0674/1054] Clean up async_dispatcher_connect helper usage (#68613) --- homeassistant/components/aftership/sensor.py | 9 ++++---- .../alarmdecoder/alarm_control_panel.py | 5 ++-- .../components/alarmdecoder/binary_sensor.py | 23 +++++++++---------- .../components/alarmdecoder/sensor.py | 5 ++-- homeassistant/components/aqualogic/sensor.py | 5 ++-- homeassistant/components/aqualogic/switch.py | 5 ++-- homeassistant/components/econet/__init__.py | 6 ++--- homeassistant/components/enocean/device.py | 5 ++-- .../components/homekit_controller/__init__.py | 7 ++++-- .../components/mobile_app/device_tracker.py | 7 ++++-- .../components/owntracks/__init__.py | 5 ++-- homeassistant/components/plaato/entity.py | 4 +++- .../components/qwikswitch/__init__.py | 5 ++-- homeassistant/components/rfxtrx/__init__.py | 7 +++--- .../components/tellstick/__init__.py | 5 ++-- .../components/waterfurnace/sensor.py | 5 ++-- .../components/websocket_api/sensor.py | 9 ++++---- tests/common.py | 3 ++- tests/components/heos/test_media_player.py | 11 +++++---- tests/components/websocket_api/test_auth.py | 9 +++----- 20 files changed, 75 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 62d138d2123..d816afa3b17 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -15,7 +15,10 @@ from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle @@ -128,9 +131,7 @@ class AfterShipSensor(SensorEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self._force_update - ) + async_dispatcher_connect(self.hass, UPDATE_TOPIC, self._force_update) ) async def _force_update(self) -> None: diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 4d7b55db991..b16490357d1 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -22,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -95,8 +96,8 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback ) ) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 87b6c86fc33..24eeffde691 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -90,26 +91,24 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_FAULT, self._fault_callback + async_dispatcher_connect(self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) + ) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback ) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_ZONE_RESTORE, self._restore_callback + async_dispatcher_connect( + self.hass, SIGNAL_RFX_MESSAGE, self._rfx_message_callback ) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RFX_MESSAGE, self._rfx_message_callback - ) - ) - - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_REL_MESSAGE, self._rel_message_callback + async_dispatcher_connect( + self.hass, SIGNAL_REL_MESSAGE, self._rel_message_callback ) ) diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 897348f1386..90d0606d681 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import SIGNAL_PANEL_MESSAGE @@ -26,8 +27,8 @@ class AlarmDecoderSensor(SensorEntity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_PANEL_MESSAGE, self._message_callback + async_dispatcher_connect( + self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback ) ) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 3337e9c40a2..5e6e35cce76 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -146,8 +147,8 @@ class AquaLogicSensor(SensorEntity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback + async_dispatcher_connect( + self.hass, UPDATE_TOPIC, self.async_update_callback ) ) diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 54a37056187..953b0c9b527 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -8,6 +8,7 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -96,7 +97,5 @@ class AquaLogicSwitch(SwitchEntity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_write_ha_state - ) + async_dispatcher_connect(self.hass, UPDATE_TOPIC, self.async_write_ha_state) ) diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 100b58b107d..a706ceb8e7e 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -119,9 +119,7 @@ class EcoNetEntity(Entity): """Subscribe to device events.""" await super().async_added_to_hass() self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - PUSH_UPDATE, self.on_update_received - ) + async_dispatcher_connect(self.hass, PUSH_UPDATE, self.on_update_received) ) @callback diff --git a/homeassistant/components/enocean/device.py b/homeassistant/components/enocean/device.py index 36477d21cff..b57b053f4a7 100644 --- a/homeassistant/components/enocean/device.py +++ b/homeassistant/components/enocean/device.py @@ -2,6 +2,7 @@ from enocean.protocol.packet import Packet from enocean.utils import combine_hex +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE @@ -18,8 +19,8 @@ class EnOceanEntity(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RECEIVE_MESSAGE, self._message_received_callback + async_dispatcher_connect( + self.hass, SIGNAL_RECEIVE_MESSAGE, self._message_received_callback ) ) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index e4f8f715bc8..6b538658b23 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -65,8 +66,10 @@ class HomeKitEntity(Entity): async def async_added_to_hass(self) -> None: """Entity added to hass.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - self._accessory.signal_state_updated, self.async_write_ha_state + async_dispatcher_connect( + self.hass, + self._accessory.signal_state_updated, + self.async_write_ha_state, ) ) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 5e2ae23af16..0f6f0835c3b 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_LONGITUDE, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -114,8 +115,10 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() - self._dispatch_unsub = self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id), self.update_data + self._dispatch_unsub = async_dispatcher_connect( + self.hass, + SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id), + self.update_data, ) # Don't restore if we got set up with data. diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 1ac454d0c8c..a9f89d26238 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_when_setup @@ -101,8 +102,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN]["unsub"] = hass.helpers.dispatcher.async_dispatcher_connect( - DOMAIN, async_handle_message + hass.data[DOMAIN]["unsub"] = async_dispatcher_connect( + hass, DOMAIN, async_handle_message ) return True diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index b0297ec3024..33ac5d910aa 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -2,6 +2,7 @@ from pyplaato.models.device import PlaatoDevice from homeassistant.helpers import entity +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( DEVICE, @@ -95,7 +96,8 @@ class PlaatoEntity(entity.Entity): ) else: self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( + async_dispatcher_connect( + self.hass, SENSOR_SIGNAL % (self._device_id, self._sensor_type), self.async_write_ha_state, ) diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index ecf7720b283..e529483d0cc 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -96,9 +97,7 @@ class QSEntity(Entity): async def async_added_to_hass(self): """Listen for updates from QSUSb via dispatcher.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - self.qsid, self.update_packet - ) + async_dispatcher_connect(self.hass, self.qsid, self.update_packet) ) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index ba4cb9eb226..c10075bbb79 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceRegistry, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -334,7 +335,7 @@ async def async_setup_platform_entry( async_add_entities(constructor(event, event, device_id, {})) config_entry.async_on_unload( - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, _update) + async_dispatcher_connect(hass, SIGNAL_EVENT, _update) ) @@ -484,9 +485,7 @@ class RfxtrxEntity(RestoreEntity): self._apply_event(self._event) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_EVENT, self._handle_event - ) + async_dispatcher_connect(self.hass, SIGNAL_EVENT, self._handle_event) ) @property diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index b95928c01b4..8611c99b654 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -188,8 +189,8 @@ class TellstickDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_TELLCORE_CALLBACK, self.update_from_callback + async_dispatcher_connect( + self.hass, SIGNAL_TELLCORE_CALLBACK, self.update_from_callback ) ) diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 15f3f64c9ba..8418992ac5d 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify @@ -138,8 +139,8 @@ class WaterFurnaceSensor(SensorEntity): async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.async_update_callback + async_dispatcher_connect( + self.hass, UPDATE_TOPIC, self.async_update_callback ) ) diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 29cc2f4a44d..9377fcefd92 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -35,13 +36,13 @@ class APICount(SensorEntity): async def async_added_to_hass(self) -> None: """Added to hass.""" self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_CONNECTED, self._update_count + async_dispatcher_connect( + self.hass, SIGNAL_WEBSOCKET_CONNECTED, self._update_count ) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count + async_dispatcher_connect( + self.hass, SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count ) ) diff --git a/tests/common.py b/tests/common.py index 55878f76da7..5968a21cddb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -55,6 +55,7 @@ from homeassistant.helpers import ( restore_state, storage, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.async_ import run_callback_threadsafe @@ -1208,7 +1209,7 @@ def async_mock_signal(hass, signal): """Mock service call.""" calls.append(args) - hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) + async_dispatcher_connect(hass, signal, mock_signal_handler) return calls diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index d134840d652..ee75793df8e 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -58,6 +58,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -155,7 +156,7 @@ async def test_updates_from_connection_event( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) # Connected player.available = True @@ -201,7 +202,7 @@ async def test_updates_from_sources_updated( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) input_sources.clear() player.heos.dispatcher.send( @@ -225,7 +226,7 @@ async def test_updates_from_players_changed( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) assert hass.states.get("media_player.test_player").state == STATE_IDLE player.state = const.PLAY_STATE_PLAY @@ -259,7 +260,7 @@ async def test_updates_from_players_changed_new_ids( async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) player.heos.dispatcher.send( const.SIGNAL_CONTROLLER_EVENT, const.EVENT_PLAYERS_CHANGED, @@ -287,7 +288,7 @@ async def test_updates_from_user_changed(hass, config_entry, config, controller) async def set_signal(): event.set() - hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_HEOS_UPDATED, set_signal) + async_dispatcher_connect(hass, SIGNAL_HEOS_UPDATED, set_signal) controller.is_signed_in = False controller.signed_in_username = None diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 7834474470c..6591bd58dfd 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -16,6 +16,7 @@ from homeassistant.components.websocket_api.const import ( URL, ) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -30,18 +31,14 @@ def track_connected(hass): def track_connected(): connected_evt.append(1) - hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_CONNECTED, track_connected - ) + async_dispatcher_connect(hass, SIGNAL_WEBSOCKET_CONNECTED, track_connected) disconnected_evt = [] @callback def track_disconnected(): disconnected_evt.append(1) - hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_DISCONNECTED, track_disconnected - ) + async_dispatcher_connect(hass, SIGNAL_WEBSOCKET_DISCONNECTED, track_disconnected) return {"connected": connected_evt, "disconnected": disconnected_evt} From 3068c9c9d318231e9eb487e50071f35d70f94068 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 13:20:16 +0100 Subject: [PATCH 0675/1054] Sort selectors alphabetically (#68612) --- homeassistant/helpers/selector.py | 458 +++++++++++++++--------------- 1 file changed, 230 insertions(+), 228 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 6668229c0cf..1652e2110f4 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -84,6 +84,136 @@ SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( } ) +DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema( + { + # Integration linked to it with a config entry + vol.Optional("integration"): str, + # Manufacturer of device + vol.Optional("manufacturer"): str, + # Model of device + vol.Optional("model"): str, + # Device has to contain entities matching this selector + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, + } +) + + +@SELECTORS.register("action") +class ActionSelector(Selector): + """Selector of an action sequence (script syntax).""" + + selector_type = "action" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + return data + + +@SELECTORS.register("addon") +class AddonSelector(Selector): + """Selector of a add-on.""" + + selector_type = "addon" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("name"): str, + vol.Optional("slug"): str, + } + ) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + addon: str = vol.Schema(str)(data) + return addon + + +@SELECTORS.register("area") +class AreaSelector(Selector): + """Selector of a single or list of areas.""" + + selector_type = "area" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): DEVICE_SELECTOR_CONFIG_SCHEMA, + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + area_id: str = vol.Schema(str)(data) + return area_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + +@SELECTORS.register("attribute") +class AttributeSelector(Selector): + """Selector for an entity attribute.""" + + selector_type = "attribute" + + CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + attribute: str = vol.Schema(str)(data) + return attribute + + +@SELECTORS.register("boolean") +class BooleanSelector(Selector): + """Selector of a boolean value.""" + + selector_type = "boolean" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> bool: + """Validate the passed selection.""" + value: bool = vol.Coerce(bool)(data) + return value + + +@SELECTORS.register("device") +class DeviceSelector(Selector): + """Selector of a single or list of devices.""" + + selector_type = "device" + + CONFIG_SCHEMA = DEVICE_SELECTOR_CONFIG_SCHEMA + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + device_id: str = vol.Schema(str)(data) + return device_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + +@SELECTORS.register("duration") +class DurationSelector(Selector): + """Selector for a duration.""" + + selector_type = "duration" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> dict[str, float]: + """Validate the passed selection.""" + cv.time_period_dict(data) + return cast(dict[str, float], data) + @SELECTORS.register("entity") class EntitySelector(Selector): @@ -118,58 +248,69 @@ class EntitySelector(Selector): return cast(list, vol.Schema([validate])(data)) # Output is a list -@SELECTORS.register("device") -class DeviceSelector(Selector): - """Selector of a single or list of devices.""" +@SELECTORS.register("icon") +class IconSelector(Selector): + """Selector for an icon.""" - selector_type = "device" + selector_type = "icon" CONFIG_SCHEMA = vol.Schema( + {vol.Optional("placeholder"): str} + # Frontend also has a fallbackPath option, this is not used by core + ) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + icon: str = vol.Schema(str)(data) + return icon + + +@SELECTORS.register("location") +class LocationSelector(Selector): + """Selector for a location.""" + + selector_type = "location" + + CONFIG_SCHEMA = vol.Schema( + {vol.Optional("radius"): bool, vol.Optional("icon"): str} + ) + DATA_SCHEMA = vol.Schema( { - # Integration linked to it with a config entry - vol.Optional("integration"): str, - # Manufacturer of device - vol.Optional("manufacturer"): str, - # Model of device - vol.Optional("model"): str, - # Device has to contain entities matching this selector - vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("multiple", default=False): cv.boolean, + vol.Required("latitude"): float, + vol.Required("longitude"): float, + vol.Optional("radius"): float, } ) - def __call__(self, data: Any) -> str | list[str]: + def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" - if not self.config["multiple"]: - device_id: str = vol.Schema(str)(data) - return device_id - if not isinstance(data, list): - raise vol.Invalid("Value should be a list") - return [vol.Schema(str)(val) for val in data] + location: dict[str, float] = self.DATA_SCHEMA(data) + return location -@SELECTORS.register("area") -class AreaSelector(Selector): - """Selector of a single or list of areas.""" +@SELECTORS.register("media") +class MediaSelector(Selector): + """Selector for media.""" - selector_type = "area" + selector_type = "media" - CONFIG_SCHEMA = vol.Schema( + CONFIG_SCHEMA = vol.Schema({}) + DATA_SCHEMA = vol.Schema( { - vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, - vol.Optional("multiple", default=False): cv.boolean, + # Although marked as optional in frontend, this field is required + vol.Required("entity_id"): cv.entity_id_or_uuid, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_id"): str, + # Although marked as optional in frontend, this field is required + vol.Required("media_content_type"): str, + vol.Remove("metadata"): dict, } ) - def __call__(self, data: Any) -> str | list[str]: + def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" - if not self.config["multiple"]: - area_id: str = vol.Schema(str)(data) - return area_id - if not isinstance(data, list): - raise vol.Invalid("Value should be a list") - return [vol.Schema(str)(val) for val in data] + media: dict[str, float] = self.DATA_SCHEMA(data) + return media def has_min_max_if_slider(data: Any) -> Any: @@ -219,90 +360,6 @@ class NumberSelector(Selector): return value -@SELECTORS.register("addon") -class AddonSelector(Selector): - """Selector of a add-on.""" - - selector_type = "addon" - - CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("name"): str, - vol.Optional("slug"): str, - } - ) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - addon: str = vol.Schema(str)(data) - return addon - - -@SELECTORS.register("boolean") -class BooleanSelector(Selector): - """Selector of a boolean value.""" - - selector_type = "boolean" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> bool: - """Validate the passed selection.""" - value: bool = vol.Coerce(bool)(data) - return value - - -@SELECTORS.register("time") -class TimeSelector(Selector): - """Selector of a time value.""" - - selector_type = "time" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - cv.time(data) - return cast(str, data) - - -@SELECTORS.register("target") -class TargetSelector(Selector): - """Selector of a target value (area ID, device ID, entity ID etc). - - Value should follow cv.TARGET_SERVICE_FIELDS format. - """ - - selector_type = "target" - - CONFIG_SCHEMA = vol.Schema( - { - vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("device"): DeviceSelector.CONFIG_SCHEMA, - } - ) - - TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) - - def __call__(self, data: Any) -> dict[str, list[str]]: - """Validate the passed selection.""" - target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) - return target - - -@SELECTORS.register("action") -class ActionSelector(Selector): - """Selector of an action sequence (script syntax).""" - - selector_type = "action" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> Any: - """Validate the passed selection.""" - return data - - @SELECTORS.register("object") class ObjectSelector(Selector): """Selector for an arbitrary object.""" @@ -316,6 +373,40 @@ class ObjectSelector(Selector): return data +select_option = vol.All( + dict, + vol.Schema( + { + vol.Required("value"): str, + vol.Required("label"): str, + } + ), +) + + +@SELECTORS.register("select") +class SelectSelector(Selector): + """Selector for an single-choice input select.""" + + selector_type = "select" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("options"): vol.All( + vol.Any([str], [select_option]), vol.Length(min=1) + ) + } + ) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + if isinstance(self.config["options"][0], str): + options = self.config["options"] + else: + options = [option["value"] for option in self.config["options"]] + return vol.In(options)(vol.Schema(str)(data)) + + @SELECTORS.register("text") class StringSelector(Selector): """Selector for a multi-line text string.""" @@ -353,83 +444,28 @@ class StringSelector(Selector): return text -select_option = vol.All( - dict, - vol.Schema( - { - vol.Required("value"): str, - vol.Required("label"): str, - } - ), -) +@SELECTORS.register("target") +class TargetSelector(Selector): + """Selector of a target value (area ID, device ID, entity ID etc). + Value should follow cv.TARGET_SERVICE_FIELDS format. + """ -@SELECTORS.register("select") -class SelectSelector(Selector): - """Selector for an single-choice input select.""" - - selector_type = "select" + selector_type = "target" CONFIG_SCHEMA = vol.Schema( { - vol.Required("options"): vol.All( - vol.Any([str], [select_option]), vol.Length(min=1) - ) + vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): DEVICE_SELECTOR_CONFIG_SCHEMA, } ) - def __call__(self, data: Any) -> Any: + TARGET_SELECTION_SCHEMA = vol.Schema(cv.TARGET_SERVICE_FIELDS) + + def __call__(self, data: Any) -> dict[str, list[str]]: """Validate the passed selection.""" - if isinstance(self.config["options"][0], str): - options = self.config["options"] - else: - options = [option["value"] for option in self.config["options"]] - return vol.In(options)(vol.Schema(str)(data)) - - -@SELECTORS.register("attribute") -class AttributeSelector(Selector): - """Selector for an entity attribute.""" - - selector_type = "attribute" - - CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - attribute: str = vol.Schema(str)(data) - return attribute - - -@SELECTORS.register("duration") -class DurationSelector(Selector): - """Selector for a duration.""" - - selector_type = "duration" - - CONFIG_SCHEMA = vol.Schema({}) - - def __call__(self, data: Any) -> dict[str, float]: - """Validate the passed selection.""" - cv.time_period_dict(data) - return cast(dict[str, float], data) - - -@SELECTORS.register("icon") -class IconSelector(Selector): - """Selector for an icon.""" - - selector_type = "icon" - - CONFIG_SCHEMA = vol.Schema( - {vol.Optional("placeholder"): str} - # Frontend also has a fallbackPath option, this is not used by core - ) - - def __call__(self, data: Any) -> str: - """Validate the passed selection.""" - icon: str = vol.Schema(str)(data) - return icon + target: dict[str, list[str]] = self.TARGET_SELECTION_SCHEMA(data) + return target @SELECTORS.register("theme") @@ -446,49 +482,15 @@ class ThemeSelector(Selector): return theme -@SELECTORS.register("media") -class MediaSelector(Selector): - """Selector for media.""" +@SELECTORS.register("time") +class TimeSelector(Selector): + """Selector of a time value.""" - selector_type = "media" + selector_type = "time" CONFIG_SCHEMA = vol.Schema({}) - DATA_SCHEMA = vol.Schema( - { - # Although marked as optional in frontend, this field is required - vol.Required("entity_id"): cv.entity_id_or_uuid, - # Although marked as optional in frontend, this field is required - vol.Required("media_content_id"): str, - # Although marked as optional in frontend, this field is required - vol.Required("media_content_type"): str, - vol.Remove("metadata"): dict, - } - ) - def __call__(self, data: Any) -> dict[str, float]: + def __call__(self, data: Any) -> str: """Validate the passed selection.""" - media: dict[str, float] = self.DATA_SCHEMA(data) - return media - - -@SELECTORS.register("location") -class LocationSelector(Selector): - """Selector for a location.""" - - selector_type = "location" - - CONFIG_SCHEMA = vol.Schema( - {vol.Optional("radius"): bool, vol.Optional("icon"): str} - ) - DATA_SCHEMA = vol.Schema( - { - vol.Required("latitude"): float, - vol.Required("longitude"): float, - vol.Optional("radius"): float, - } - ) - - def __call__(self, data: Any) -> dict[str, float]: - """Validate the passed selection.""" - location: dict[str, float] = self.DATA_SCHEMA(data) - return location + cv.time(data) + return cast(str, data) From cbf5b5ead5a20ef7c4f33b60cf81d8bd8d2efa75 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 14:15:09 +0100 Subject: [PATCH 0676/1054] Use recorder threadpool in WS recorder/get_statistics_metadata (#68615) --- homeassistant/components/recorder/websocket_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 241dca9026c..585641665af 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -82,7 +82,8 @@ async def ws_get_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Get metadata for a list of statistic_ids.""" - statistic_ids = await hass.async_add_executor_job( + instance: Recorder = hass.data[DATA_INSTANCE] + statistic_ids = await instance.async_add_executor_job( list_statistic_ids, hass, msg.get("statistic_ids") ) connection.send_result(msg["id"], statistic_ids) From 8aff8d89d25034ddaaaeb2c2b1933282ad749e1e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Mar 2022 14:40:54 +0100 Subject: [PATCH 0677/1054] Clean up async_reproduce_state helper usage (#68617) --- .../tests/test_reproduce_state.py | 11 +++++--- .../test_reproduce_state.py | 15 ++++++----- .../components/alert/test_reproduce_state.py | 14 +++++------ .../automation/test_reproduce_state.py | 15 +++++------ .../counter/test_reproduce_state.py | 15 +++++------ .../components/cover/test_reproduce_state.py | 13 +++++----- tests/components/fan/test_reproduce_state.py | 11 ++++---- .../humidifier/test_reproduce_state.py | 11 +++++--- .../input_boolean/test_reproduce_state.py | 7 ++++-- .../input_datetime/test_reproduce_state.py | 10 +++++--- .../input_number/test_reproduce_state.py | 14 ++++++----- .../input_select/test_reproduce_state.py | 13 ++++++---- .../input_text/test_reproduce_state.py | 15 ++++++----- .../components/light/test_reproduce_state.py | 25 ++++++++++--------- tests/components/lock/test_reproduce_state.py | 11 ++++---- .../components/number/test_reproduce_state.py | 11 ++++---- .../components/remote/test_reproduce_state.py | 11 ++++---- .../components/select/test_reproduce_state.py | 15 ++++++----- .../components/switch/test_reproduce_state.py | 13 +++++----- .../components/timer/test_reproduce_state.py | 11 ++++---- .../components/vacuum/test_reproduce_state.py | 11 ++++---- .../water_heater/test_reproduce_state.py | 13 ++++++---- 22 files changed, 159 insertions(+), 126 deletions(-) diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py index 83d95570b45..cb4e37c0933 100644 --- a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py +++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -17,7 +18,8 @@ async def test_reproducing_states( turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("NEW_DOMAIN.entity_off", "off"), State("NEW_DOMAIN.entity_on", "on", {"color": "red"}), @@ -29,8 +31,8 @@ async def test_reproducing_states( assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("NEW_DOMAIN.entity_off", "not_supported")], blocking=True + await async_reproduce_state( + hass, [State("NEW_DOMAIN.entity_off", "not_supported")], blocking=True ) assert "not_supported" in caplog.text @@ -38,7 +40,8 @@ async def test_reproducing_states( assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("NEW_DOMAIN.entity_on", "off"), State("NEW_DOMAIN.entity_off", "on", {"color": "red"}), diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py index 0f87e2206ac..b78815c24f4 100644 --- a/tests/components/alarm_control_panel/test_reproduce_state.py +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -67,7 +68,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY), State( @@ -81,7 +83,7 @@ async def test_reproducing_states(hass, caplog): ), State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED), State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED), - ] + ], ) assert len(arm_away_calls) == 0 @@ -93,8 +95,8 @@ async def test_reproducing_states(hass, caplog): assert len(trigger_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("alarm_control_panel.entity_triggered", "not_supported")] + await async_reproduce_state( + hass, [State("alarm_control_panel.entity_triggered", "not_supported")] ) assert "not_supported" in caplog.text @@ -107,7 +109,8 @@ async def test_reproducing_states(hass, caplog): assert len(trigger_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("alarm_control_panel.entity_armed_away", STATE_ALARM_TRIGGERED), State( @@ -122,7 +125,7 @@ async def test_reproducing_states(hass, caplog): State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED), # Should not raise State("alarm_control_panel.non_existing", "on"), - ] + ], ) assert len(arm_away_calls) == 1 diff --git a/tests/components/alert/test_reproduce_state.py b/tests/components/alert/test_reproduce_state.py index 2470106558c..83b5cf45701 100644 --- a/tests/components/alert/test_reproduce_state.py +++ b/tests/components/alert/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Alert.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,30 +14,29 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "alert", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( - [State("alert.entity_off", "off"), State("alert.entity_on", "on")] + await async_reproduce_state( + hass, [State("alert.entity_off", "off"), State("alert.entity_on", "on")] ) assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("alert.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("alert.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("alert.entity_on", "off"), State("alert.entity_off", "on"), # Should not raise State("alert.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py index c00378aa369..4c596afee2d 100644 --- a/tests/components/automation/test_reproduce_state.py +++ b/tests/components/automation/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Automation.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,30 +14,30 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "automation", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( - [State("automation.entity_off", "off"), State("automation.entity_on", "on")] + await async_reproduce_state( + hass, + [State("automation.entity_off", "off"), State("automation.entity_on", "on")], ) assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("automation.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("automation.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("automation.entity_on", "off"), State("automation.entity_off", "on"), # Should not raise State("automation.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py index 7b7816f8272..876c441d60c 100644 --- a/tests/components/counter/test_reproduce_state.py +++ b/tests/components/counter/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Counter.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -16,7 +17,8 @@ async def test_reproducing_states(hass, caplog): configure_calls = async_mock_service(hass, "counter", "configure") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("counter.entity", "5"), State( @@ -24,21 +26,20 @@ async def test_reproducing_states(hass, caplog): "8", {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, ), - ] + ], ) assert len(configure_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("counter.entity", "not_supported")] - ) + await async_reproduce_state(hass, [State("counter.entity", "not_supported")]) assert "not_supported" in caplog.text assert len(configure_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("counter.entity", "2"), State( @@ -48,7 +49,7 @@ async def test_reproducing_states(hass, caplog): ), # Should not raise State("counter.non_existing", "6"), - ] + ], ) valid_calls = [ diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py index aec4c8c2324..d51f7ed06e2 100644 --- a/tests/components/cover/test_reproduce_state.py +++ b/tests/components/cover/test_reproduce_state.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_OPEN, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -61,7 +62,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("cover.entity_close", STATE_CLOSED), State( @@ -93,7 +95,7 @@ async def test_reproducing_states(hass, caplog): STATE_OPEN, {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, ), - ] + ], ) assert len(close_calls) == 0 @@ -104,9 +106,7 @@ async def test_reproducing_states(hass, caplog): assert len(position_tilt_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("cover.entity_close", "not_supported")] - ) + await async_reproduce_state(hass, [State("cover.entity_close", "not_supported")]) assert "not_supported" in caplog.text assert len(close_calls) == 0 @@ -117,7 +117,8 @@ async def test_reproducing_states(hass, caplog): assert len(position_tilt_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("cover.entity_close", STATE_OPEN), State( diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py index eb429669429..fe1f27ba625 100644 --- a/tests/components/fan/test_reproduce_state.py +++ b/tests/components/fan/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Fan.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -19,7 +20,8 @@ async def test_reproducing_states(hass, caplog): set_percentage_calls = async_mock_service(hass, "fan", "set_percentage") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("fan.entity_off", "off"), State("fan.entity_on", "on"), @@ -35,9 +37,7 @@ async def test_reproducing_states(hass, caplog): assert len(oscillate_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("fan.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("fan.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 @@ -47,7 +47,8 @@ async def test_reproducing_states(hass, caplog): assert len(set_percentage_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("fan.entity_on", "off"), State("fan.entity_off", "on"), diff --git a/tests/components/humidifier/test_reproduce_state.py b/tests/components/humidifier/test_reproduce_state.py index 15b797c66a8..491b2f803d9 100644 --- a/tests/components/humidifier/test_reproduce_state.py +++ b/tests/components/humidifier/test_reproduce_state.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import Context, State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -38,7 +39,8 @@ async def test_reproducing_on_off_states(hass, caplog): humidity_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY_1, "off", {ATTR_MODE: MODE_NORMAL, ATTR_HUMIDITY: 45}), State(ENTITY_2, "on", {ATTR_MODE: MODE_NORMAL, ATTR_HUMIDITY: 45}), @@ -51,7 +53,7 @@ async def test_reproducing_on_off_states(hass, caplog): assert len(humidity_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state([State(ENTITY_1, "not_supported")]) + await async_reproduce_state(hass, [State(ENTITY_1, "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 @@ -60,13 +62,14 @@ async def test_reproducing_on_off_states(hass, caplog): assert len(humidity_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY_2, "off"), State(ENTITY_1, "on", {}), # Should not raise State("humidifier.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/input_boolean/test_reproduce_state.py b/tests/components/input_boolean/test_reproduce_state.py index 3ee5bfb5da9..96f73284969 100644 --- a/tests/components/input_boolean/test_reproduce_state.py +++ b/tests/components/input_boolean/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for input boolean.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component @@ -15,7 +16,8 @@ async def test_reproducing_states(hass): } }, ) - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_boolean.initial_on", "off"), State("input_boolean.initial_off", "on"), @@ -26,7 +28,8 @@ async def test_reproducing_states(hass): assert hass.states.get("input_boolean.initial_off").state == "on" assert hass.states.get("input_boolean.initial_on").state == "off" - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ # Test invalid state State("input_boolean.initial_on", "invalid_state"), diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index f2d9dd4d445..bac620b8983 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input datetime.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -28,7 +29,8 @@ async def test_reproducing_states(hass, caplog): datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_datetime.entity_datetime", "2010-10-10 01:20:00"), State("input_datetime.entity_time", "01:20:00"), @@ -39,7 +41,8 @@ async def test_reproducing_states(hass, caplog): assert len(datetime_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_datetime.entity_datetime", "not_supported"), State("input_datetime.entity_datetime", "not-valid-date"), @@ -55,7 +58,8 @@ async def test_reproducing_states(hass, caplog): assert len(datetime_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_datetime.entity_datetime", "2011-10-10 02:20:00"), State("input_datetime.entity_time", "02:20:00"), diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py index 38a777732ad..9c6f5bda708 100644 --- a/tests/components/input_number/test_reproduce_state.py +++ b/tests/components/input_number/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input number.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component VALID_NUMBER1 = "19.0" @@ -20,7 +21,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_number.test_number", VALID_NUMBER1), # Should not raise @@ -31,7 +33,8 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_number.test_number").state == VALID_NUMBER1 # Test reproducing with different state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_number.test_number", VALID_NUMBER2), # Should not raise @@ -42,14 +45,13 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 # Test setting state to number out of range - await hass.helpers.state.async_reproduce_state( - [State("input_number.test_number", "150")] - ) + await async_reproduce_state(hass, [State("input_number.test_number", "150")]) # The entity states should be unchanged after trying to set them to out-of-range number assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ # Test invalid state State("input_number.test_number", "invalid_state"), diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py index c4cfbea268d..e095bd97dfc 100644 --- a/tests/components/input_select/test_reproduce_state.py +++ b/tests/components/input_select/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input select.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component VALID_OPTION1 = "Option A" @@ -29,7 +30,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY, VALID_OPTION1), # Should not raise @@ -41,7 +43,8 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get(ENTITY).state == VALID_OPTION1 # Try reproducing with different state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State(ENTITY, VALID_OPTION3), # Should not raise @@ -53,14 +56,14 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get(ENTITY).state == VALID_OPTION3 # Test setting state to invalid state - await hass.helpers.state.async_reproduce_state([State(ENTITY, INVALID_OPTION)]) + await async_reproduce_state(hass, [State(ENTITY, INVALID_OPTION)]) # The entity state should be unchanged assert hass.states.get(ENTITY).state == VALID_OPTION3 # Test setting a different option set - await hass.helpers.state.async_reproduce_state( - [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})] + await async_reproduce_state( + hass, [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})] ) # These should fail if options weren't changed to VALID_OPTION_SET2 diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py index 01117a28f53..b3235beab15 100644 --- a/tests/components/input_text/test_reproduce_state.py +++ b/tests/components/input_text/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Input text.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.setup import async_setup_component VALID_TEXT1 = "Test text" @@ -23,7 +24,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_text.test_text", VALID_TEXT1), # Should not raise @@ -35,7 +37,8 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_text.test_text").state == VALID_TEXT1 # Try reproducing with different state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("input_text.test_text", VALID_TEXT2), # Should not raise @@ -47,17 +50,13 @@ async def test_reproducing_states(hass, caplog): assert hass.states.get("input_text.test_text").state == VALID_TEXT2 # Test setting state to invalid state (length too long) - await hass.helpers.state.async_reproduce_state( - [State("input_text.test_text", INVALID_TEXT1)] - ) + await async_reproduce_state(hass, [State("input_text.test_text", INVALID_TEXT1)]) # The entity state should be unchanged assert hass.states.get("input_text.test_text").state == VALID_TEXT2 # Test setting state to invalid state (length too short) - await hass.helpers.state.async_reproduce_state( - [State("input_text.test_text", INVALID_TEXT2)] - ) + await async_reproduce_state(hass, [State("input_text.test_text", INVALID_TEXT2)]) # The entity state should be unchanged assert hass.states.get("input_text.test_text").state == VALID_TEXT2 diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 97d969acdd9..75e917828c2 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components import light from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -43,7 +44,8 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "light", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), @@ -58,23 +60,22 @@ async def test_reproducing_states(hass, caplog): State("light.entity_profile", "on", VALID_PROFILE), State("light.entity_rgb", "on", VALID_RGB_COLOR), State("light.entity_xy", "on", VALID_XY_COLOR), - ] + ], ) assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("light.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("light.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), @@ -194,8 +195,8 @@ async def test_filter_color_modes(hass, caplog, color_mode): turn_on_calls = async_mock_service(hass, "light", "turn_on") - await hass.helpers.state.async_reproduce_state( - [State("light.entity", "on", {**all_colors, "color_mode": color_mode})] + await async_reproduce_state( + hass, [State("light.entity", "on", {**all_colors, "color_mode": color_mode})] ) expected_map = { @@ -225,8 +226,8 @@ async def test_filter_color_modes(hass, caplog, color_mode): # This should do nothing, the light is already in the desired state hass.states.async_set("light.entity", "on", {"color_mode": color_mode, **expected}) - await hass.helpers.state.async_reproduce_state( - [State("light.entity", "on", {**expected, "color_mode": color_mode})] + await async_reproduce_state( + hass, [State("light.entity", "on", {**expected, "color_mode": color_mode})] ) assert len(turn_on_calls) == 1 @@ -235,8 +236,8 @@ async def test_deprecation_warning(hass, caplog): """Test deprecation warning.""" hass.states.async_set("light.entity_off", "off", {}) turn_on_calls = async_mock_service(hass, "light", "turn_on") - await hass.helpers.state.async_reproduce_state( - [State("light.entity_off", "on", {"brightness_pct": 80})] + await async_reproduce_state( + hass, [State("light.entity_off", "on", {"brightness_pct": 80})] ) assert len(turn_on_calls) == 1 assert DEPRECATION_WARNING % ["brightness_pct"] in caplog.text diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py index 8c08e9f6b10..bcbdc8db33b 100644 --- a/tests/components/lock/test_reproduce_state.py +++ b/tests/components/lock/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Lock.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,7 +14,8 @@ async def test_reproducing_states(hass, caplog): unlock_calls = async_mock_service(hass, "lock", "unlock") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("lock.entity_locked", "locked"), State("lock.entity_unlocked", "unlocked", {}), @@ -24,16 +26,15 @@ async def test_reproducing_states(hass, caplog): assert len(unlock_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("lock.entity_locked", "not_supported")] - ) + await async_reproduce_state(hass, [State("lock.entity_locked", "not_supported")]) assert "not_supported" in caplog.text assert len(lock_calls) == 0 assert len(unlock_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("lock.entity_locked", "unlocked"), State("lock.entity_unlocked", "locked"), diff --git a/tests/components/number/test_reproduce_state.py b/tests/components/number/test_reproduce_state.py index 654f87cbceb..44ff89de93c 100644 --- a/tests/components/number/test_reproduce_state.py +++ b/tests/components/number/test_reproduce_state.py @@ -6,6 +6,7 @@ from homeassistant.components.number.const import ( SERVICE_SET_VALUE, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -21,7 +22,8 @@ async def test_reproducing_states(hass, caplog): ) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("number.test_number", VALID_NUMBER1), # Should not raise @@ -33,7 +35,8 @@ async def test_reproducing_states(hass, caplog): # Test reproducing with different state calls = async_mock_service(hass, DOMAIN, SERVICE_SET_VALUE) - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("number.test_number", VALID_NUMBER2), # Should not raise @@ -46,8 +49,6 @@ async def test_reproducing_states(hass, caplog): assert calls[0].data == {"entity_id": "number.test_number", "value": VALID_NUMBER2} # Test invalid state - await hass.helpers.state.async_reproduce_state( - [State("number.test_number", "invalid_state")] - ) + await async_reproduce_state(hass, [State("number.test_number", "invalid_state")]) assert len(calls) == 1 diff --git a/tests/components/remote/test_reproduce_state.py b/tests/components/remote/test_reproduce_state.py index 8795cadb4de..0e97ee48793 100644 --- a/tests/components/remote/test_reproduce_state.py +++ b/tests/components/remote/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Remote.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,7 +14,8 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "remote", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [State("remote.entity_off", "off"), State("remote.entity_on", "on")], ) @@ -21,16 +23,15 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("remote.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("remote.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("remote.entity_on", "off"), State("remote.entity_off", "on", {}), diff --git a/tests/components/select/test_reproduce_state.py b/tests/components/select/test_reproduce_state.py index b1ab3a0a5aa..bbd1ae17a7b 100644 --- a/tests/components/select/test_reproduce_state.py +++ b/tests/components/select/test_reproduce_state.py @@ -9,6 +9,7 @@ from homeassistant.components.select.const import ( ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -24,7 +25,8 @@ async def test_reproducing_states( {ATTR_OPTIONS: ["option_one", "option_two", "option_three"]}, ) - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("select.test", "option_two"), ], @@ -35,7 +37,8 @@ async def test_reproducing_states( assert calls[0].data == {ATTR_ENTITY_ID: "select.test", ATTR_OPTION: "option_two"} # Calling it again should not do anything - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("select.test", "option_one"), ], @@ -43,15 +46,11 @@ async def test_reproducing_states( assert len(calls) == 1 # Restoring an invalid state should not work either - await hass.helpers.state.async_reproduce_state( - [State("select.test", "option_four")] - ) + await async_reproduce_state(hass, [State("select.test", "option_four")]) assert len(calls) == 1 assert "Invalid state specified" in caplog.text # Restoring an state for an invalid entity ID logs a warning - await hass.helpers.state.async_reproduce_state( - [State("select.non_existing", "option_three")] - ) + await async_reproduce_state(hass, [State("select.non_existing", "option_three")]) assert len(calls) == 1 assert "Unable to find entity" in caplog.text diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py index 65de4b36c59..e64d43b9ba6 100644 --- a/tests/components/switch/test_reproduce_state.py +++ b/tests/components/switch/test_reproduce_state.py @@ -1,5 +1,6 @@ """Test reproduce state for Switch.""" from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -13,7 +14,8 @@ async def test_reproducing_states(hass, caplog): turn_off_calls = async_mock_service(hass, "switch", "turn_off") # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [State("switch.entity_off", "off"), State("switch.entity_on", "on", {})], ) @@ -21,22 +23,21 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("switch.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("switch.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 assert len(turn_off_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("switch.entity_on", "off"), State("switch.entity_off", "on", {}), # Should not raise State("switch.non_existing", "on"), - ] + ], ) assert len(turn_on_calls) == 1 diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py index 8ee8a86c5cc..3235802efbc 100644 --- a/tests/components/timer/test_reproduce_state.py +++ b/tests/components/timer/test_reproduce_state.py @@ -9,6 +9,7 @@ from homeassistant.components.timer import ( STATUS_PAUSED, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -27,7 +28,8 @@ async def test_reproducing_states(hass, caplog): cancel_calls = async_mock_service(hass, "timer", SERVICE_CANCEL) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("timer.entity_idle", STATUS_IDLE), State("timer.entity_paused", STATUS_PAUSED), @@ -43,9 +45,7 @@ async def test_reproducing_states(hass, caplog): assert len(cancel_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("timer.entity_idle", "not_supported")] - ) + await async_reproduce_state(hass, [State("timer.entity_idle", "not_supported")]) assert "not_supported" in caplog.text assert len(start_calls) == 0 @@ -53,7 +53,8 @@ async def test_reproducing_states(hass, caplog): assert len(cancel_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("timer.entity_idle", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}), State("timer.entity_paused", STATUS_ACTIVE), diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py index 5edc3a924e9..fcdada7f8b6 100644 --- a/tests/components/vacuum/test_reproduce_state.py +++ b/tests/components/vacuum/test_reproduce_state.py @@ -19,6 +19,7 @@ from homeassistant.const import ( STATE_PAUSED, ) from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -48,7 +49,8 @@ async def test_reproducing_states(hass, caplog): fan_speed_calls = async_mock_service(hass, "vacuum", SERVICE_SET_FAN_SPEED) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("vacuum.entity_off", STATE_OFF), State("vacuum.entity_on", STATE_ON), @@ -70,9 +72,7 @@ async def test_reproducing_states(hass, caplog): assert len(fan_speed_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("vacuum.entity_off", "not_supported")] - ) + await async_reproduce_state(hass, [State("vacuum.entity_off", "not_supported")]) assert "not_supported" in caplog.text assert len(turn_on_calls) == 0 @@ -84,7 +84,8 @@ async def test_reproducing_states(hass, caplog): assert len(fan_speed_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("vacuum.entity_off", STATE_ON), State("vacuum.entity_on", STATE_OFF), diff --git a/tests/components/water_heater/test_reproduce_state.py b/tests/components/water_heater/test_reproduce_state.py index d1719986fc9..66b90d95818 100644 --- a/tests/components/water_heater/test_reproduce_state.py +++ b/tests/components/water_heater/test_reproduce_state.py @@ -11,6 +11,7 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON from homeassistant.core import State +from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service @@ -34,7 +35,8 @@ async def test_reproducing_states(hass, caplog): set_away_calls = async_mock_service(hass, "water_heater", SERVICE_SET_AWAY_MODE) # These calls should do nothing as entities already in desired state - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("water_heater.entity_off", STATE_OFF), State("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}), @@ -45,7 +47,7 @@ async def test_reproducing_states(hass, caplog): STATE_ECO, {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, ), - ] + ], ) assert len(turn_on_calls) == 0 @@ -55,8 +57,8 @@ async def test_reproducing_states(hass, caplog): assert len(set_away_calls) == 0 # Test invalid state is handled - await hass.helpers.state.async_reproduce_state( - [State("water_heater.entity_off", "not_supported")] + await async_reproduce_state( + hass, [State("water_heater.entity_off", "not_supported")] ) assert "not_supported" in caplog.text @@ -67,7 +69,8 @@ async def test_reproducing_states(hass, caplog): assert len(set_away_calls) == 0 # Make sure correct services are called - await hass.helpers.state.async_reproduce_state( + await async_reproduce_state( + hass, [ State("water_heater.entity_on", STATE_OFF), State("water_heater.entity_off", STATE_ON, {ATTR_TEMPERATURE: 45}), From 76103752b8cb8479ee507fedddf615644535078b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 16:51:31 +0100 Subject: [PATCH 0678/1054] Only show light group all option in advanced mode (#68610) --- homeassistant/components/group/config_flow.py | 4 +- .../helpers/helper_config_entry_flow.py | 27 ++ tests/components/group/test_config_flow.py | 67 +++++ .../helpers/test_helper_config_entry_flow.py | 248 ++++++++++++++++++ 4 files changed, 345 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 83485bd16bf..23394c0ee59 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -49,7 +49,9 @@ BINARY_SENSOR_OPTIONS_SCHEMA = basic_group_options_schema("binary_sensor").exten LIGHT_OPTIONS_SCHEMA = basic_group_options_schema("light").extend( { - vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}), + vol.Required( + CONF_ALL, default=False, description={"advanced": True} + ): selector.selector({"boolean": {}}), } ) diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 87716dcbccf..ba62d5d59e5 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -79,6 +79,23 @@ class HelperCommonFlowHandler: """Handle a form step.""" form_step: HelperFlowFormStep = cast(HelperFlowFormStep, self._flow[step_id]) + if ( + user_input is not None + and (data_schema := form_step.schema) + and data_schema.schema + and not self._handler.show_advanced_options + ): + # Add advanced field default if not set + for key in data_schema.schema.keys(): + if isinstance(key, (vol.Optional, vol.Required)): + if ( + key.description + and key.description.get("advanced") + and key.default is not vol.UNDEFINED + and key not in self._options + ): + user_input[str(key.schema)] = key.default() + if user_input is not None and form_step.schema is not None: # Do extra validation of user input try: @@ -120,6 +137,16 @@ class HelperCommonFlowHandler: # Make a copy of the schema with suggested values set to saved options schema = {} for key, val in data_schema.schema.items(): + + if isinstance(key, vol.Marker): + # Exclude advanced field + if ( + key.description + and key.description.get("advanced") + and not self._handler.show_advanced_options + ): + continue + new_key = key if key in options and isinstance(key, vol.Marker): # Copy the marker to not modify the flow schema diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index c9f5768fc8f..67fdec1820f 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -266,6 +266,73 @@ async def test_options( assert get_suggested(result["data_schema"].schema, "name") is None +@pytest.mark.parametrize( + "group_type,extra_options,extra_options_after,advanced", + ( + ("light", {"all": False}, {"all": False}, False), + ("light", {"all": True}, {"all": True}, False), + ("light", {"all": False}, {"all": False}, True), + ("light", {"all": True}, {"all": False}, True), + ), +) +async def test_light_all_options( + hass: HomeAssistant, group_type, extra_options, extra_options_after, advanced +) -> None: + """Test reconfiguring.""" + members1 = [f"{group_type}.one", f"{group_type}.two"] + members2 = [f"{group_type}.four", f"{group_type}.five"] + + group_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entities": members1, + "group_type": group_type, + "name": "Bed Room", + **extra_options, + }, + title="Bed Room", + ) + group_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(group_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{group_type}.bed_room") + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": advanced} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == group_type + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entities": members2, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options_after, + } + assert config_entry.data == {} + assert config_entry.options == { + "entities": members2, + "group_type": group_type, + "hide_members": False, + "name": "Bed Room", + **extra_options_after, + } + assert config_entry.title == "Bed Room" + + @pytest.mark.parametrize( "hide_members,hidden_by_initial,hidden_by", ((False, "integration", None), (True, None, "integration")), diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py index a92a0cd3b36..dece7ace37c 100644 --- a/tests/helpers/test_helper_config_entry_flow.py +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -1,9 +1,52 @@ """Test helper_config_entry_flow.""" +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowFormStep, + HelperFlowMenuStep, wrapped_entity_config_entry_title, ) +from homeassistant.util.decorator import Registry + +from tests.common import MockConfigEntry + + +@pytest.fixture +def manager(): + """Return a flow manager.""" + handlers = Registry() + entries = [] + + class FlowManager(data_entry_flow.FlowManager): + """Test flow manager.""" + + async def async_create_flow(self, handler_key, *, context, data): + """Test create flow.""" + handler = handlers.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + flow = handler() + flow.init_step = context.get("init_step", "init") + return flow + + async def async_finish_flow(self, flow, result): + """Test finish flow.""" + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + result["source"] = flow.context.get("source") + entries.append(result) + return result + + mgr = FlowManager(None) + mgr.mock_created_entries = entries + mgr.mock_reg_handler = handlers.register + return mgr async def test_name(hass: HomeAssistant) -> None: @@ -36,3 +79,208 @@ async def test_name(hass: HomeAssistant) -> None: registry.async_update_entity("switch.ceiling", name="Custom Name") assert wrapped_entity_config_entry_title(hass, entity_id) == "Custom Name" assert wrapped_entity_config_entry_title(hass, entry.id) == "Custom Name" + + +@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +async def test_config_flow_advanced_option( + hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker +): + """Test handling of advanced options in config flow.""" + manager.hass = hass + + CONFIG_SCHEMA = vol.Schema( + { + marker("option1"): str, + marker("advanced_no_default", description={"advanced": True}): str, + marker( + "advanced_default", + default="a very reasonable default", + description={"advanced": True}, + ): str, + } + ) + + CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(CONFIG_SCHEMA) + } + + @manager.mock_reg_handler("test") + class TestFlow(HelperConfigFlowHandler): + config_flow = CONFIG_FLOW + + # Start flow in basic mode + result = await manager.async_init("test") + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == ["option1"] + + result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "a very reasonable default", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await manager.async_init("test", context={"show_advanced_options": True}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await manager.async_configure( + result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "a very reasonable default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await manager.async_init("test", context={"show_advanced_options": True}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await manager.async_configure( + result["flow_id"], + { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blabla", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + assert result["options"] == { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["options"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + +@pytest.mark.parametrize("marker", (vol.Required, vol.Optional)) +async def test_options_flow_advanced_option( + hass: HomeAssistant, manager: data_entry_flow.FlowManager, marker +): + """Test handling of advanced options in options flow.""" + manager.hass = hass + + OPTIONS_SCHEMA = vol.Schema( + { + marker("option1"): str, + marker("advanced_no_default", description={"advanced": True}): str, + marker( + "advanced_default", + default="a very reasonable default", + description={"advanced": True}, + ): str, + } + ) + + OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) + } + + class TestFlow(HelperConfigFlowHandler, domain="test"): + config_flow = {} + options_flow = OPTIONS_FLOW + + config_entry = MockConfigEntry( + data={}, + domain="test", + options={ + "option1": "blabla", + "advanced_no_default": "abc123", + "advanced_default": "not default", + }, + ) + config_entry.add_to_hass(hass) + + # Start flow in basic mode + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == ["option1"] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"option1": "blublu"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "advanced_default": "not default", + "advanced_no_default": "abc123", + "option1": "blublu", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "advanced_default": "a very reasonable default", + "advanced_no_default": "def456", + "option1": "blabla", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) + + # Start flow in advanced mode + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert list(result["data_schema"].schema.keys()) == [ + "option1", + "advanced_no_default", + "advanced_default", + ] + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + "advanced_default": "also not default", + "advanced_no_default": "abc123", + "option1": "blabla", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "advanced_default": "also not default", + "advanced_no_default": "abc123", + "option1": "blabla", + } + for option in result["data"]: + # Make sure we didn't get the Optional or Required instance as key + assert isinstance(option, str) From 46072d299787648c014d94bde71919d46c851a87 Mon Sep 17 00:00:00 2001 From: Numa Perez <41305393+nprez83@users.noreply.github.com> Date: Thu, 24 Mar 2022 13:11:06 -0400 Subject: [PATCH 0679/1054] Fix Lyric temperature setting when off (#68573) --- homeassistant/components/lyric/climate.py | 33 ++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index dfe88048ba4..56b5b6fd022 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -211,29 +211,35 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): """Return the temperature we try to reach.""" device = self.device if ( - not device.changeableValues.autoChangeoverActive - and HVAC_MODES[device.changeableValues.mode] != HVAC_MODE_OFF + device.changeableValues.autoChangeoverActive + or HVAC_MODES[device.changeableValues.mode] == HVAC_MODE_OFF ): - if self.hvac_mode == HVAC_MODE_COOL: - return device.changeableValues.coolSetpoint - return device.changeableValues.heatSetpoint - return None + return None + if self.hvac_mode == HVAC_MODE_COOL: + return device.changeableValues.coolSetpoint + return device.changeableValues.heatSetpoint @property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" device = self.device - if device.changeableValues.autoChangeoverActive: - return device.changeableValues.coolSetpoint - return None + if ( + not device.changeableValues.autoChangeoverActive + or HVAC_MODES[device.changeableValues.mode] == HVAC_MODE_OFF + ): + return None + return device.changeableValues.coolSetpoint @property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" device = self.device - if device.changeableValues.autoChangeoverActive: - return device.changeableValues.heatSetpoint - return None + if ( + not device.changeableValues.autoChangeoverActive + or HVAC_MODES[device.changeableValues.mode] == HVAC_MODE_OFF + ): + return None + return device.changeableValues.heatSetpoint @property def preset_mode(self) -> str | None: @@ -269,6 +275,9 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" + if self.hvac_mode == HVAC_MODE_OFF: + return + device = self.device target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) From b13e14b80cd0667b62228f3121cfe28ab5dfcd92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 24 Mar 2022 18:58:58 +0100 Subject: [PATCH 0680/1054] Add command support to SamsungTV H/J models (#68301) Co-authored-by: epenet Co-authored-by: J. Nick Koston --- homeassistant/components/samsungtv/bridge.py | 66 +++++++++++-- .../components/samsungtv/media_player.py | 15 +-- .../components/samsungtv/test_media_player.py | 95 +++++++++++++------ 3 files changed, 129 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 73764d51dc0..b89c76f028e 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -13,7 +13,11 @@ from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledRespo from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.async_rest import SamsungTVAsyncRest from samsungtvws.command import SamsungTVCommand -from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote +from samsungtvws.encrypted.command import SamsungTVEncryptedCommand +from samsungtvws.encrypted.remote import ( + SamsungTVEncryptedWSAsyncRemote, + SendRemoteKey as SendEncryptedRemoteKey, +) from samsungtvws.event import ( ED_INSTALLED_APP_EVENT, MS_ERROR_EVENT, @@ -33,12 +37,12 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_DESCRIPTION, + CONF_MODEL, CONF_SESSION_ID, ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, @@ -59,6 +63,9 @@ from .const import ( KEY_PRESS_TIMEOUT = 1.2 +ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400"} +ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"} + def mac_from_device_info(info: dict[str, Any]) -> str | None: """Extract the mac address from the device info.""" @@ -617,9 +624,16 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): ) -> None: """Initialize Bridge.""" super().__init__(hass, method, host, port) + self._power_off_warning_logged: bool = False + self._model: str | None = None + self._short_model: str | None = None if entry_data: self.token = entry_data.get(CONF_TOKEN) self.session_id = entry_data.get(CONF_SESSION_ID) + self._model = entry_data.get(CONF_MODEL) + if self._model and len(self._model) > 4: + self._short_model = self._model[4:] + self._rest_api_port: int | None = None self._device_info: dict[str, Any] | None = None self._remote: SamsungTVEncryptedWSAsyncRemote | None = None @@ -693,10 +707,33 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): async def async_send_keys(self, keys: list[str]) -> None: """Send a list of keys using websocket protocol.""" - raise HomeAssistantError( - "Sending commands to encrypted TVs is not yet supported" + await self._async_send_commands( + [SendEncryptedRemoteKey.click(key) for key in keys] ) + async def _async_send_commands( + self, commands: list[SamsungTVEncryptedCommand] + ) -> None: + """Send the commands using websocket protocol.""" + try: + # recreate connection if connection was dead + retry_count = 1 + for _ in range(retry_count + 1): + try: + if remote := await self._async_get_remote(): + await remote.send_commands(commands) + break + except ( + BrokenPipeError, + WebSocketException, + ): + # BrokenPipe can occur when the commands is sent to fast + # WebSocketException can occur when timed out + self._remote = None + except OSError: + # Different reasons, e.g. hostname not resolveable + pass + async def _async_get_remote(self) -> SamsungTVEncryptedWSAsyncRemote | None: """Create or return a remote control instance.""" if (remote := self._remote) and remote.is_alive(): @@ -737,9 +774,24 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): async def async_power_off(self) -> None: """Send power off command to remote.""" - raise HomeAssistantError( - "Sending commands to encrypted TVs is not yet supported" - ) + power_off_commands: list[SamsungTVEncryptedCommand] = [] + if self._short_model in ENCRYPTED_MODEL_USES_POWER_OFF: + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWEROFF")) + elif self._short_model in ENCRYPTED_MODEL_USES_POWER: + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWER")) + else: + if self._model and not self._power_off_warning_logged: + LOGGER.warning( + "Unknown power_off command for %s (%s): sending KEY_POWEROFF and KEY_POWER", + self._model, + self.host, + ) + self._power_off_warning_logged = True + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWEROFF")) + power_off_commands.append(SendEncryptedRemoteKey.click("KEY_POWER")) + await self._async_send_commands(power_off_commands) + # Force closing of remote session to provide instant UI feedback + await self.async_close_remote() async def async_close_remote(self) -> None: """Close remote object.""" diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 677dbfab66a..edd929273b1 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -27,14 +27,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_METHOD, - CONF_NAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component import homeassistant.helpers.config_validation as cv @@ -52,7 +45,6 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, - METHOD_ENCRYPTED_WEBSOCKET, ) SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} @@ -125,10 +117,7 @@ class SamsungTVDevice(MediaPlayerEntity): self._app_list: dict[str, str] | None = None self._app_list_event: asyncio.Event = asyncio.Event() - if config_entry.data.get(CONF_METHOD) != METHOD_ENCRYPTED_WEBSOCKET: - # Encrypted websockets currently only support ON/OFF status - self._attr_supported_features = SUPPORT_SAMSUNGTV - + self._attr_supported_features = SUPPORT_SAMSUNGTV if self._on_script or self._mac: # Add turn-on if on_script or mac is available self._attr_supported_features |= SUPPORT_TURN_ON diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 96b939d08b9..a0b712e575b 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -8,7 +8,10 @@ import pytest from samsungctl import exceptions from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.command import SamsungTVSleepCommand -from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote +from samsungtvws.encrypted.remote import ( + SamsungTVEncryptedCommand, + SamsungTVEncryptedWSAsyncRemote, +) from samsungtvws.exceptions import ConnectionFailure, HttpApiError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException @@ -28,6 +31,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, ) from homeassistant.components.samsungtv.const import ( + CONF_MODEL, CONF_ON_ACTION, DOMAIN as SAMSUNGTV_DOMAIN, ENCRYPTED_WEBSOCKET_PORT, @@ -605,11 +609,9 @@ async def test_send_key_websocketexception_encrypted( """Testing unhandled response exception.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.send_commands = Mock(side_effect=WebSocketException("Boom")) - with pytest.raises(HomeAssistantError) as exc_info: - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - assert exc_info.match("media_player.fake does not support this service.") + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -631,11 +633,9 @@ async def test_send_key_os_error_ws_encrypted( """Testing unhandled response exception.""" await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.send_commands = Mock(side_effect=OSError("Boom")) - with pytest.raises(HomeAssistantError) as exc_info: - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - assert exc_info.match("media_player.fake does not support this service.") + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -805,22 +805,64 @@ async def test_turn_off_encrypted_websocket( hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for turn_off.""" - await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) + entry_data = deepcopy(MOCK_ENTRYDATA_ENCRYPTED_WS) + entry_data[CONF_MODEL] = "UE48UNKNOWN" + await setup_samsungtv_entry(hass, entry_data) remoteencws.send_commands.reset_mock() - with pytest.raises(HomeAssistantError) as exc_info: - assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - assert exc_info.match("media_player.fake does not support this service.") + caplog.clear() + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key called + assert remoteencws.send_commands.call_count == 1 + commands = remoteencws.send_commands.call_args_list[0].args[0] + assert len(commands) == 2 + assert isinstance(command := commands[0], SamsungTVEncryptedCommand) + assert command.body["param3"] == "KEY_POWEROFF" + assert isinstance(command := commands[1], SamsungTVEncryptedCommand) + assert command.body["param3"] == "KEY_POWER" + assert "Unknown power_off command for UE48UNKNOWN (fake_host)" in caplog.text # commands not sent : power off in progress - with pytest.raises(HomeAssistantError) as exc_info: - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - assert exc_info.match("media_player.fake does not support this service.") + remoteencws.send_commands.reset_mock() + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text + remoteencws.send_commands.assert_not_called() + + +@pytest.mark.parametrize( + ("model", "expected_key_type"), + [("UE50H6400", "KEY_POWEROFF"), ("UN75JU641D", "KEY_POWER")], +) +async def test_turn_off_encrypted_websocket_key_type( + hass: HomeAssistant, + remoteencws: Mock, + caplog: pytest.LogCaptureFixture, + model: str, + expected_key_type: str, +) -> None: + """Test for turn_off.""" + entry_data = deepcopy(MOCK_ENTRYDATA_ENCRYPTED_WS) + entry_data[CONF_MODEL] = model + await setup_samsungtv_entry(hass, entry_data) + + remoteencws.send_commands.reset_mock() + + caplog.clear() + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key called + assert remoteencws.send_commands.call_count == 1 + commands = remoteencws.send_commands.call_args_list[0].args[0] + assert len(commands) == 1 + assert isinstance(command := commands[0], SamsungTVEncryptedCommand) + assert command.body["param3"] == expected_key_type + assert "Unknown power_off command for" not in caplog.text async def test_turn_off_legacy(hass: HomeAssistant, remote: Mock) -> None: @@ -867,11 +909,10 @@ async def test_turn_off_encryptedws_os_error( caplog.set_level(logging.DEBUG) await setup_samsungtv_entry(hass, MOCK_ENTRYDATA_ENCRYPTED_WS) remoteencws.close = Mock(side_effect=OSError("BOOM")) - with pytest.raises(HomeAssistantError) as exc_info: - assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True - ) - assert exc_info.match("media_player.fake does not support this service.") + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert "Error closing connection" in caplog.text async def test_volume_up(hass: HomeAssistant, remote: Mock) -> None: From f562f4264faa776d723a01534e06b2cae08f553b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Mar 2022 20:45:18 +0100 Subject: [PATCH 0681/1054] Update grpcio to 1.45.0 (#68632) --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c1acde9fcbd..cf68cda8531 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,7 +50,7 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.44.0 +grpcio==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 896c87a2d80..c48a59f90d3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -67,7 +67,7 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.44.0 +grpcio==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, From 9f079a22d5f0544a34054c4061ae76c2a825d6ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 24 Mar 2022 20:46:34 +0100 Subject: [PATCH 0682/1054] Add config flow for times of the day binary sensor (#68246) Co-authored-by: Franck Nijhof --- homeassistant/components/tod/__init__.py | 28 +++- homeassistant/components/tod/binary_sensor.py | 54 ++++++-- homeassistant/components/tod/config_flow.py | 49 +++++++ homeassistant/components/tod/const.py | 8 ++ homeassistant/components/tod/manifest.json | 4 +- homeassistant/components/tod/strings.json | 30 +++++ .../components/tod/translations/en.json | 30 +++++ homeassistant/generated/config_flows.py | 3 +- tests/components/tod/test_config_flow.py | 125 ++++++++++++++++++ tests/components/tod/test_init.py | 49 +++++++ 10 files changed, 364 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/tod/config_flow.py create mode 100644 homeassistant/components/tod/const.py create mode 100644 homeassistant/components/tod/strings.json create mode 100644 homeassistant/components/tod/translations/en.json create mode 100644 tests/components/tod/test_config_flow.py create mode 100644 tests/components/tod/test_init.py diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index fa15326becb..09038836e2e 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -1 +1,27 @@ -"""The tod component.""" +"""The Times of the Day integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Times of the Day from a config entry.""" + hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (Platform.BINARY_SENSOR,) + ) diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 6f14d5735fb..1100892b876 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_AFTER, CONF_BEFORE, @@ -22,15 +23,19 @@ from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_ne from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util +from .const import ( + CONF_AFTER_OFFSET, + CONF_AFTER_TIME, + CONF_BEFORE_OFFSET, + CONF_BEFORE_TIME, +) + _LOGGER = logging.getLogger(__name__) ATTR_AFTER = "after" ATTR_BEFORE = "before" ATTR_NEXT_UPDATE = "next_update" -CONF_AFTER_OFFSET = "after_offset" -CONF_BEFORE_OFFSET = "before_offset" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), @@ -42,6 +47,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Times of the Day config entry.""" + if hass.config.time_zone is None: + _LOGGER.error("Timezone is not set in Home Assistant configuration") + return + + after = cv.time(config_entry.options[CONF_AFTER_TIME]) + after_offset = timedelta(0) + before = cv.time(config_entry.options[CONF_BEFORE_TIME]) + before_offset = timedelta(0) + name = config_entry.title + unique_id = config_entry.entry_id + + async_add_entities( + [TodSensor(name, after, after_offset, before, before_offset, unique_id)] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -58,12 +85,12 @@ async def async_setup_platform( before = config[CONF_BEFORE] before_offset = config[CONF_BEFORE_OFFSET] name = config[CONF_NAME] - sensor = TodSensor(name, after, after_offset, before, before_offset) + sensor = TodSensor(name, after, after_offset, before, before_offset, None) async_add_entities([sensor]) -def is_sun_event(sun_event): +def _is_sun_event(sun_event): """Return true if event is sun event not time.""" return sun_event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) @@ -71,8 +98,9 @@ def is_sun_event(sun_event): class TodSensor(BinarySensorEntity): """Time of the Day Sensor.""" - def __init__(self, name, after, after_offset, before, before_offset): + def __init__(self, name, after, after_offset, before, before_offset, unique_id): """Init the ToD Sensor...""" + self._attr_unique_id = unique_id self._name = name self._time_before = self._time_after = self._next_update = None self._after_offset = after_offset @@ -119,11 +147,11 @@ class TodSensor(BinarySensorEntity): # calculate utc datetime corresponding to local time return dt_util.as_utc(datetime.combine(current_local_date, naive_time)) - def _calculate_boudary_time(self): + def _calculate_boundary_time(self): """Calculate internal absolute time boundaries.""" nowutc = dt_util.utcnow() # If after value is a sun event instead of absolute time - if is_sun_event(self._after): + if _is_sun_event(self._after): # Calculate the today's event utc time or # if not available take next after_event_date = get_astral_event_date( @@ -139,7 +167,7 @@ class TodSensor(BinarySensorEntity): self._time_after = after_event_date # If before value is a sun event instead of absolute time - if is_sun_event(self._before): + if _is_sun_event(self._before): # Calculate the today's event utc time or if not available take # next before_event_date = get_astral_event_date( @@ -168,7 +196,7 @@ class TodSensor(BinarySensorEntity): # _time_after is set to 23:00 today # nowutc is set to 10:00 today if ( - not is_sun_event(self._after) + not _is_sun_event(self._after) and self._time_after > nowutc and self._time_before > nowutc + timedelta(days=1) ): @@ -182,7 +210,7 @@ class TodSensor(BinarySensorEntity): def _turn_to_next_day(self): """Turn to to the next day.""" - if is_sun_event(self._after): + if _is_sun_event(self._after): self._time_after = get_astral_event_next( self.hass, self._after, self._time_after - self._after_offset ) @@ -191,7 +219,7 @@ class TodSensor(BinarySensorEntity): # Offset is already there self._time_after += timedelta(days=1) - if is_sun_event(self._before): + if _is_sun_event(self._before): self._time_before = get_astral_event_next( self.hass, self._before, self._time_before - self._before_offset ) @@ -202,7 +230,7 @@ class TodSensor(BinarySensorEntity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - self._calculate_boudary_time() + self._calculate_boundary_time() self._calculate_next_update() @callback diff --git a/homeassistant/components/tod/config_flow.py b/homeassistant/components/tod/config_flow.py new file mode 100644 index 00000000000..ad0d70b2a0c --- /dev/null +++ b/homeassistant/components/tod/config_flow.py @@ -0,0 +1,49 @@ +"""Config flow for Times of the Day integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_NAME +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowFormStep, + HelperFlowMenuStep, +) + +from .const import CONF_AFTER_TIME, CONF_BEFORE_TIME, DOMAIN + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_AFTER_TIME): selector.selector({"time": {}}), + vol.Optional(CONF_BEFORE_TIME): selector.selector({"time": {}}), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): selector.selector({"text": {}}), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Times of the Day.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) diff --git a/homeassistant/components/tod/const.py b/homeassistant/components/tod/const.py new file mode 100644 index 00000000000..3b6f8c23e17 --- /dev/null +++ b/homeassistant/components/tod/const.py @@ -0,0 +1,8 @@ +"""Constants for the Times of the Day integration.""" + +DOMAIN = "tod" + +CONF_AFTER_TIME = "after_time" +CONF_AFTER_OFFSET = "after_offset" +CONF_BEFORE_TIME = "before_time" +CONF_BEFORE_OFFSET = "before_offset" diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index b74465e05c3..8d3c0d4eab4 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -1,8 +1,10 @@ { "domain": "tod", + "integration_type": "helper", "name": "Times of the Day", "documentation": "https://www.home-assistant.io/integrations/tod", "codeowners": [], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/tod/strings.json b/homeassistant/components/tod/strings.json new file mode 100644 index 00000000000..713ff6e58e3 --- /dev/null +++ b/homeassistant/components/tod/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "New Times of the Day Sensor", + "description": "Configure when the sensor should turn on and off.", + "data": { + "after": "On after", + "after_time": "On time", + "before": "Off after", + "before_time": "Off time", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "init": { + "description": "[%key:component::tod::config::step::user::description%]", + "data": { + "after": "[%key:component::tod::config::step::user::data::after%]", + "after_time": "[%key:component::tod::config::step::user::data::after_time%]", + "before": "[%key:component::tod::config::step::user::data::before%]", + "before_time": "[%key:component::tod::config::step::user::data::before_time%]" + } + } + } + } +} diff --git a/homeassistant/components/tod/translations/en.json b/homeassistant/components/tod/translations/en.json new file mode 100644 index 00000000000..288b51c4b5e --- /dev/null +++ b/homeassistant/components/tod/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "On after", + "after_time": "On time", + "before": "Off after", + "before_time": "Off time", + "name": "Name" + }, + "description": "Configure when the sensor should turn on and off.", + "title": "New Times of the Day Sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "On after", + "after_time": "On time", + "before": "Off after", + "before_time": "Off time" + }, + "description": "Configure when the sensor should turn on and off." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e1b6e95800d..f16f69cbede 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -400,6 +400,7 @@ FLOWS = { "zwave_me" ], "helper": [ - "derivative" + "derivative", + "tod" ] } diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py new file mode 100644 index 00000000000..35f9ef0d5bd --- /dev/null +++ b/tests/components/tod/test_config_flow.py @@ -0,0 +1,125 @@ +"""Test the Times of the Day config flow.""" +from unittest.mock import patch + +from freezegun import freeze_time +import pytest + +from homeassistant import config_entries +from homeassistant.components.tod.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.tod.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "after_time": "10:00", + "before_time": "18:00", + "name": "My tod", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My tod" + assert result["data"] == {} + assert result["options"] == { + "after_time": "10:00", + "before_time": "18:00", + "name": "My tod", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "after_time": "10:00", + "before_time": "18:00", + "name": "My tod", + } + assert config_entry.title == "My tod" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@freeze_time("2022-03-16 17:37:00", tz_offset=-7) +async def test_options(hass: HomeAssistant) -> None: + """Test reconfiguring.""" + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "after_time": "10:00", + "before_time": "18:05", + "name": "My tod", + }, + title="My tod", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "after_time") == "10:00" + assert get_suggested(schema, "before_time") == "18:05" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "after_time": "10:00", + "before_time": "17:05", + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "after_time": "10:00", + "before_time": "17:05", + "name": "My tod", + } + assert config_entry.data == {} + assert config_entry.options == { + "after_time": "10:00", + "before_time": "17:05", + "name": "My tod", + } + assert config_entry.title == "My tod" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 1 + + # Check the state of the entity has changed as expected + state = hass.states.get("binary_sensor.my_tod") + assert state.state == "off" + assert state.attributes["after"] == "2022-03-16T10:00:00-07:00" + assert state.attributes["before"] == "2022-03-16T17:05:00-07:00" diff --git a/tests/components/tod/test_init.py b/tests/components/tod/test_init.py new file mode 100644 index 00000000000..510bf848ad4 --- /dev/null +++ b/tests/components/tod/test_init.py @@ -0,0 +1,49 @@ +"""Test the Times of the Day integration.""" +from freezegun import freeze_time + +from homeassistant.components.tod.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@freeze_time("2022-03-16 17:37:00", tz_offset=-7) +async def test_setup_and_remove_config_entry(hass: HomeAssistant) -> None: + """Test setting up and removing a config entry.""" + registry = er.async_get(hass) + tod_entity_id = "binary_sensor.my_tod" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "after_time": "10:00:00", + "before_time": "18:05:00", + "name": "My tod", + }, + title="My tod", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(tod_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(tod_entity_id) + # Check the state of the entity is as expected + state = hass.states.get("binary_sensor.my_tod") + assert state.state == "off" + assert state.attributes["after"] == "2022-03-16T10:00:00-07:00" + assert state.attributes["before"] == "2022-03-16T18:05:00-07:00" + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(tod_entity_id) is None + assert registry.async_get(tod_entity_id) is None From 3777fa52f0b64677c8582567dc4f146fa4fee946 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Mar 2022 09:48:09 -1000 Subject: [PATCH 0683/1054] Ensure recorder statistics process registry updates in the db executor (#68633) --- homeassistant/components/recorder/statistics.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 67b2568fc53..f01190097df 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -190,7 +190,7 @@ def async_setup(hass: HomeAssistant) -> None: hass.data[STATISTICS_META_BAKERY] = baked.bakery() hass.data[STATISTICS_SHORT_TERM_BAKERY] = baked.bakery() - def entity_id_changed(event: Event) -> None: + def _entity_id_changed(event: Event) -> None: """Handle entity_id changed.""" old_entity_id = event.data["old_entity_id"] entity_id = event.data["entity_id"] @@ -200,6 +200,9 @@ def async_setup(hass: HomeAssistant) -> None: & (StatisticsMeta.source == DOMAIN) ).update({StatisticsMeta.statistic_id: entity_id}) + async def _async_entity_id_changed(event: Event) -> None: + await hass.data[DATA_INSTANCE].async_add_executor_job(_entity_id_changed, event) + @callback def entity_registry_changed_filter(event: Event) -> bool: """Handle entity_id changed filter.""" @@ -211,7 +214,7 @@ def async_setup(hass: HomeAssistant) -> None: if hass.is_running: hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, - entity_id_changed, + _async_entity_id_changed, event_filter=entity_registry_changed_filter, ) From e911936a0d050d1d405486656419b71c7204b78d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Mar 2022 09:48:49 -1000 Subject: [PATCH 0684/1054] Remove direct usage of concurrent.futures from recorder (#68593) --- homeassistant/components/recorder/__init__.py | 51 +++++++++++-------- tests/components/recorder/test_migrate.py | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index d68b993444b..205b544f6ab 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations import abc import asyncio from collections.abc import Callable, Iterable -import concurrent.futures from dataclasses import dataclass from datetime import datetime, timedelta import logging @@ -149,6 +148,8 @@ EXPIRE_AFTER_COMMITS = 120 # - How much memory our low end hardware has STATE_ATTRIBUTES_ID_CACHE_SIZE = 2048 +SHUTDOWN_TASK = object() + DB_LOCK_TIMEOUT = 30 DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 @@ -298,6 +299,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: exclude_t=exclude_t, ) instance.async_initialize() + instance.async_register() instance.start() _async_register_services(hass, instance) history.async_setup(hass) @@ -566,6 +568,7 @@ class Recorder(threading.Thread): self.hass = hass self.auto_purge = auto_purge self.keep_days = keep_days + self._hass_started: asyncio.Future[object] = asyncio.Future() self.commit_interval = commit_interval self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() self.recording_start = dt_util.utcnow() @@ -710,12 +713,10 @@ class Recorder(threading.Thread): self.queue.put(StatisticsTask(start)) @callback - def async_register( - self, shutdown_task: object, hass_started: concurrent.futures.Future - ) -> None: + def async_register(self) -> None: """Post connection initialize.""" - def _empty_queue(event): + def _empty_queue(event: Event) -> None: """Empty the queue if its still present at final write.""" # If the queue is full of events to be processed because @@ -734,26 +735,28 @@ class Recorder(threading.Thread): self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) - def shutdown(event): + async def _async_shutdown(event: Event) -> None: """Shut down the Recorder.""" - if not hass_started.done(): - hass_started.set_result(shutdown_task) + if not self._hass_started.done(): + self._hass_started.set_result(SHUTDOWN_TASK) self.queue.put(StopTask()) - self.hass.add_job(self._async_stop_queue_watcher_and_event_listener) - self.join() + self._async_stop_queue_watcher_and_event_listener() + await self.hass.async_add_executor_job(self.join) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) if self.hass.state == CoreState.running: - hass_started.set_result(None) + self._hass_started.set_result(None) return @callback - def async_hass_started(event): + def _async_hass_started(event: Event) -> None: """Notify that hass has started.""" - hass_started.set_result(None) + self._hass_started.set_result(None) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, async_hass_started) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _async_hass_started + ) @callback def async_connection_failed(self) -> None: @@ -833,13 +836,18 @@ class Recorder(threading.Thread): self.hass, self.async_periodic_statistics, minute=range(0, 60, 5), second=10 ) + async def _async_wait_for_started(self) -> object | None: + """Wait for the hass started future.""" + return await self._hass_started + + def _wait_startup_or_shutdown(self) -> object | None: + """Wait for startup or shutdown before starting.""" + return asyncio.run_coroutine_threadsafe( + self._async_wait_for_started(), self.hass.loop + ).result() + def run(self) -> None: """Start processing events to save.""" - shutdown_task = object() - hass_started: concurrent.futures.Future = concurrent.futures.Future() - - self.hass.add_job(self.async_register, shutdown_task, hass_started) - current_version = self._setup_recorder() if current_version is None: @@ -853,8 +861,9 @@ class Recorder(threading.Thread): self.migration_in_progress = True self.hass.add_job(self.async_connection_success) + # If shutdown happened before Home Assistant finished starting - if hass_started.result() is shutdown_task: + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: self.migration_in_progress = False # Make sure we cleanly close the run if # we restart before startup finishes diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 5e837eb36ac..337c0a65c2b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -299,7 +299,7 @@ async def test_schema_migrate(hass, start_version): assert recorder.util.async_migration_in_progress(hass) is True migration_stall.set() await hass.async_block_till_done() - migration_done.wait() + await hass.async_add_executor_job(migration_done.wait) await async_wait_recording_done_without_instance(hass) assert migration_version == models.SCHEMA_VERSION assert setup_run.called From a566d3943ca94fc85cdb302ec6cd3b2cde15cf19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Mar 2022 09:49:13 -1000 Subject: [PATCH 0685/1054] Fix history queries while the database migration is in progress (#68598) --- homeassistant/components/recorder/history.py | 92 +++++++--- tests/components/recorder/test_history.py | 171 ++++++++++++++++++- 2 files changed, 241 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 6f5597ac268..9348e1cbb4b 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -6,8 +6,9 @@ from datetime import datetime from itertools import groupby import logging import time +from typing import Any -from sqlalchemy import Text, and_, bindparam, func, or_ +from sqlalchemy import Column, Text, and_, bindparam, func, or_ from sqlalchemy.ext import baked from sqlalchemy.sql.expression import literal @@ -59,8 +60,17 @@ QUERY_STATE_NO_ATTR = [ literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] +# Remove QUERY_STATES_PRE_SCHEMA_25 +# and the migration_in_progress check +# once schema 26 is created +QUERY_STATES_PRE_SCHEMA_25 = [ + *BASE_STATES, + States.attributes, + literal(value=None, type_=Text).label("shared_attrs"), +] QUERY_STATES = [ *BASE_STATES, + # Remove States.attributes once all attributes are in StateAttributes.shared_attrs States.attributes, StateAttributes.shared_attrs, ] @@ -68,6 +78,51 @@ QUERY_STATES = [ HISTORY_BAKERY = "recorder_history_bakery" +def query_and_join_attributes( + hass: HomeAssistant, no_attributes: bool +) -> tuple[list[Column], bool]: + """Return the query keys and if StateAttributes should be joined.""" + # If no_attributes was requested we do the query + # without the attributes fields and do not join the + # state_attributes table + if no_attributes: + return QUERY_STATE_NO_ATTR, False + # If we in the process of migrating schema we do + # not want to join the state_attributes table as we + # do not know if it will be there yet + if recorder.get_instance(hass).migration_in_progress: + return QUERY_STATES_PRE_SCHEMA_25, False + # Finally if no migration is in progress and no_attributes + # was not requested, we query both attributes columns and + # join state_attributes + return QUERY_STATES, True + + +def bake_query_and_join_attributes( + hass: HomeAssistant, no_attributes: bool +) -> tuple[Any, bool]: + """Return the initial backed query and if StateAttributes should be joined. + + Because these are baked queries the values inside the lambdas need + to be explicitly written out to avoid caching the wrong values. + """ + bakery: baked.bakery = hass.data[HISTORY_BAKERY] + # If no_attributes was requested we do the query + # without the attributes fields and do not join the + # state_attributes table + if no_attributes: + return bakery(lambda session: session.query(*QUERY_STATE_NO_ATTR)), False + # If we in the process of migrating schema we do + # not want to join the state_attributes table as we + # do not know if it will be there yet + if recorder.get_instance(hass).migration_in_progress: + return bakery(lambda session: session.query(*QUERY_STATES_PRE_SCHEMA_25)), False + # Finally if no migration is in progress and no_attributes + # was not requested, we query both attributes columns and + # join state_attributes + return bakery(lambda session: session.query(*QUERY_STATES)), True + + def async_setup(hass): """Set up the history hooks.""" hass.data[HISTORY_BAKERY] = baked.bakery() @@ -104,8 +159,7 @@ def get_significant_states_with_session( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES - baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) + baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) if entity_ids is not None and len(entity_ids) == 1: if ( @@ -146,7 +200,7 @@ def get_significant_states_with_session( if end_time is not None: baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) - if not no_attributes: + if join_attributes: baked_query += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) @@ -187,9 +241,8 @@ def state_changes_during_period( ) -> dict[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: - query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*query_keys) + baked_query, join_attributes = bake_query_and_join_attributes( + hass, no_attributes ) baked_query += lambda q: q.filter( @@ -206,7 +259,7 @@ def state_changes_during_period( baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() - if not no_attributes: + if join_attributes: baked_query += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) @@ -240,18 +293,18 @@ def get_last_state_changes(hass, number_of_states, entity_id): start_time = dt_util.utcnow() with session_scope(hass=hass) as session: - baked_query = hass.data[HISTORY_BAKERY]( - lambda session: session.query(*QUERY_STATES) - ) + baked_query, join_attributes = bake_query_and_join_attributes(hass, False) + baked_query += lambda q: q.filter(States.last_changed == States.last_updated) if entity_id is not None: baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) entity_id = entity_id.lower() - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) baked_query += lambda q: q.order_by( States.entity_id, States.last_updated.desc() ) @@ -322,7 +375,7 @@ def _get_states_with_session( # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. - query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES + query_keys, join_attributes = query_and_join_attributes(hass, no_attributes) query = session.query(*query_keys) if entity_ids: @@ -344,7 +397,7 @@ def _get_states_with_session( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, ) - if not no_attributes: + if join_attributes: query = query.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) @@ -386,7 +439,7 @@ def _get_states_with_session( query = query.filter(~States.entity_id.like(entity_domain)) if filters: query = filters.apply(query) - if not no_attributes: + if join_attributes: query = query.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) @@ -400,13 +453,12 @@ def _get_single_entity_states_with_session( ): # Use an entirely different (and extremely fast) query if we only # have a single entity id - query_keys = QUERY_STATE_NO_ATTR if no_attributes else QUERY_STATES - baked_query = hass.data[HISTORY_BAKERY](lambda session: session.query(*query_keys)) + baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) baked_query += lambda q: q.filter( States.last_updated < bindparam("utc_point_in_time"), States.entity_id == bindparam("entity_id"), ) - if not no_attributes: + if join_attributes: baked_query += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 5d1d72ca650..15f306ae549 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -1,22 +1,64 @@ """The tests the History component.""" # pylint: disable=protected-access,invalid-name from copy import copy -from datetime import timedelta +from datetime import datetime, timedelta import json from unittest.mock import patch, sentinel import pytest +from homeassistant.components import recorder from homeassistant.components.recorder import history -from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.models import ( + Events, + StateAttributes, + States, + process_timestamp, +) import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util +from .conftest import SetupRecorderInstanceT + from tests.common import mock_state_change_event from tests.components.recorder.common import wait_recording_done +def _add_db_entries( + hass: ha.HomeAssistant, point: datetime, entity_ids: list[str] +) -> None: + with recorder.session_scope(hass=hass) as session: + for idx, entity_id in enumerate(entity_ids): + session.add( + Events( + event_id=1001 + idx, + event_type="state_changed", + event_data="{}", + origin="LOCAL", + time_fired=point, + ) + ) + session.add( + States( + entity_id=entity_id, + state="on", + attributes='{"name":"the light"}', + last_changed=point, + last_updated=point, + event_id=1001 + idx, + attributes_id=1002 + idx, + ) + ) + session.add( + StateAttributes( + shared_attrs='{"name":"the shared light"}', + hash=1234 + idx, + attributes_id=1002 + idx, + ) + ) + + def _setup_get_states(hass): """Set up for testing get_states.""" states = [] @@ -501,3 +543,128 @@ def record_states(hass): ) return zero, four, states + + +async def test_state_changes_during_period_query_during_migration_to_schema_25( + hass: ha.HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test we can query data prior to schema 25 and during migration to schema 25.""" + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await hass.async_add_executor_job(_add_db_entries, hass, point, [entity_id]) + + no_attributes = True + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the shared light"} + + instance.engine.execute("update states set attributes_id=NULL;") + instance.engine.execute("drop table state_attributes;") + + with patch.object(instance, "migration_in_progress", True): + no_attributes = True + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25( + hass: ha.HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test we can query data prior to schema 25 and during migration to schema 25.""" + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await hass.async_add_executor_job(_add_db_entries, hass, point, [entity_id]) + + no_attributes = True + hist = history.get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = history.get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {"name": "the shared light"} + + instance.engine.execute("update states set attributes_id=NULL;") + instance.engine.execute("drop table state_attributes;") + + with patch.object(instance, "migration_in_progress", True): + no_attributes = True + hist = history.get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = history.get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25_multiple_entities( + hass: ha.HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test we can query data prior to schema 25 and during migration to schema 25.""" + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id_1 = "light.test" + entity_id_2 = "switch.test" + entity_ids = [entity_id_1, entity_id_2] + + await hass.async_add_executor_job(_add_db_entries, hass, point, entity_ids) + + no_attributes = True + hist = history.get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = history.get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {"name": "the shared light"} + assert hist[1].attributes == {"name": "the shared light"} + + instance.engine.execute("update states set attributes_id=NULL;") + instance.engine.execute("drop table state_attributes;") + + with patch.object(instance, "migration_in_progress", True): + no_attributes = True + hist = history.get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = history.get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {"name": "the light"} + assert hist[1].attributes == {"name": "the light"} From 5fffe9b22fee2f8a1ebae08286972b485d216adb Mon Sep 17 00:00:00 2001 From: hesselonline Date: Thu, 24 Mar 2022 22:09:59 +0100 Subject: [PATCH 0686/1054] Wallbox remove unnecessary try..except (#68636) --- homeassistant/components/wallbox/sensor.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index 57f2176febb..9a7fd1a21a3 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -169,14 +169,8 @@ class WallboxSensor(WallboxEntity, SensorEntity): def native_value(self) -> StateType: """Return the state of the sensor.""" if (sensor_round := self.entity_description.precision) is not None: - try: - return cast( - StateType, - round( - self.coordinator.data[self.entity_description.key], sensor_round - ), - ) - except TypeError: - _LOGGER.debug("Cannot format %s", self._attr_name) - return None + return cast( + StateType, + round(self.coordinator.data[self.entity_description.key], sensor_round), + ) return cast(StateType, self.coordinator.data[self.entity_description.key]) From d23d19f9e688dabc3ef211b7ba97a60a0c8fb985 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 24 Mar 2022 22:15:08 +0100 Subject: [PATCH 0687/1054] Improve data handling for Sensibo (#68419) --- .../components/sensibo/binary_sensor.py | 26 ++- homeassistant/components/sensibo/climate.py | 83 +++----- .../components/sensibo/coordinator.py | 179 +----------------- homeassistant/components/sensibo/entity.py | 23 +-- .../components/sensibo/manifest.json | 2 +- homeassistant/components/sensibo/number.py | 12 +- homeassistant/components/sensibo/select.py | 18 +- homeassistant/components/sensibo/sensor.py | 21 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 81 insertions(+), 287 deletions(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index fef81c6d7c1..02f86dbe009 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -3,7 +3,8 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any + +from pysensibo.model import MotionSensor, SensiboDevice from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -15,8 +16,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER -from .coordinator import MotionSensor, SensiboDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity @@ -31,7 +32,7 @@ class MotionBaseEntityDescriptionMixin: class DeviceBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" - value_fn: Callable[[dict[str, Any]], bool | None] + value_fn: Callable[[SensiboDevice], bool | None] @dataclass @@ -79,7 +80,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.MOTION, name="Room Occupied", icon="mdi:motion-sensor", - value_fn=lambda data: data["room_occupied"], + value_fn=lambda data: data.room_occupied, ), SensiboDeviceBinarySensorEntityDescription( key="update_available", @@ -87,7 +88,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, name="Update Available", icon="mdi:rocket-launch", - value_fn=lambda data: data["update_available"], + value_fn=lambda data: data.update_available, ), ) @@ -100,22 +101,19 @@ async def async_setup_entry( coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] - LOGGER.debug("parsed data: %s", coordinator.data.parsed) entities.extend( SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) for device_id, device_data in coordinator.data.parsed.items() - for sensor_id, sensor_data in device_data["motion_sensors"].items() + for sensor_id, sensor_data in device_data.motion_sensors.items() for description in MOTION_SENSOR_TYPES - if device_data["motion_sensors"] + if device_data.motion_sensors ) - LOGGER.debug("start device %s", entities) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for description in DEVICE_SENSOR_TYPES for device_id, device_data in coordinator.data.parsed.items() - if device_data[description.key] is not None + if getattr(device_data, description.key) is not None ) - LOGGER.debug("list: %s", entities) async_add_entities(entities) @@ -144,7 +142,7 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" self._attr_name = ( - f"{self.device_data['name']} Motion Sensor {entity_description.name}" + f"{self.device_data.name} Motion Sensor {entity_description.name}" ) @property @@ -171,7 +169,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data['name']} {entity_description.name}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index cec25d2a918..6e9d2709b8b 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -126,11 +126,9 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Initiate SensiboClimate.""" super().__init__(coordinator, device_id) self._attr_unique_id = device_id - self._attr_name = coordinator.data.parsed[device_id]["name"] + self._attr_name = self.device_data.name self._attr_temperature_unit = ( - TEMP_CELSIUS - if coordinator.data.parsed[device_id]["temp_unit"] == "C" - else TEMP_FAHRENHEIT + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT ) self._attr_supported_features = self.get_features() self._attr_precision = PRECISION_TENTHS @@ -138,7 +136,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): def get_features(self) -> int: """Get supported features.""" features = 0 - for key in self.coordinator.data.parsed[self.unique_id]["full_features"]: + for key in self.device_data.full_features: if key in FIELD_TO_FLAG: features |= FIELD_TO_FLAG[key] return features @@ -146,30 +144,27 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @property def current_humidity(self) -> int | None: """Return the current humidity.""" - return self.coordinator.data.parsed[self.unique_id]["humidity"] + return self.device_data.humidity @property def hvac_mode(self) -> str: """Return hvac operation.""" return ( - SENSIBO_TO_HA[self.coordinator.data.parsed[self.unique_id]["hvac_mode"]] - if self.coordinator.data.parsed[self.unique_id]["on"] + SENSIBO_TO_HA[self.device_data.hvac_mode] + if self.device_data.device_on else HVAC_MODE_OFF ) @property def hvac_modes(self) -> list[str]: """Return the list of available hvac operation modes.""" - return [ - SENSIBO_TO_HA[mode] - for mode in self.coordinator.data.parsed[self.unique_id]["hvac_modes"] - ] + return [SENSIBO_TO_HA[mode] for mode in self.device_data.hvac_modes] @property def current_temperature(self) -> float | None: """Return the current temperature.""" return convert_temperature( - self.coordinator.data.parsed[self.unique_id]["temp"], + self.device_data.temp, TEMP_CELSIUS, self.temperature_unit, ) @@ -177,57 +172,51 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return self.coordinator.data.parsed[self.unique_id]["target_temp"] + return self.device_data.target_temp @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" - return self.coordinator.data.parsed[self.unique_id]["temp_step"] + return self.device_data.temp_step @property def fan_mode(self) -> str | None: """Return the fan setting.""" - return self.coordinator.data.parsed[self.unique_id]["fan_mode"] + return self.device_data.fan_mode @property def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" - return self.coordinator.data.parsed[self.unique_id]["fan_modes"] + return self.device_data.fan_modes @property def swing_mode(self) -> str | None: """Return the swing setting.""" - return self.coordinator.data.parsed[self.unique_id]["swing_mode"] + return self.device_data.swing_mode @property def swing_modes(self) -> list[str] | None: """Return the list of available swing modes.""" - return self.coordinator.data.parsed[self.unique_id]["swing_modes"] + return self.device_data.swing_modes @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.coordinator.data.parsed[self.unique_id]["temp_list"][0] + return self.device_data.temp_list[0] @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.coordinator.data.parsed[self.unique_id]["temp_list"][-1] + return self.device_data.temp_list[-1] @property def available(self) -> bool: """Return True if entity is available.""" - return ( - self.coordinator.data.parsed[self.unique_id]["available"] - and super().available - ) + return self.device_data.available and super().available async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - if ( - "targetTemperature" - not in self.coordinator.data.parsed[self.unique_id]["active_features"] - ): + if "targetTemperature" not in self.device_data.active_features: raise HomeAssistantError( "Current mode doesn't support setting Target Temperature" ) @@ -238,23 +227,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if temperature == self.target_temperature: return - if temperature not in self.coordinator.data.parsed[self.unique_id]["temp_list"]: + if temperature not in self.device_data.temp_list: # Requested temperature is not supported. - if ( - temperature - > self.coordinator.data.parsed[self.unique_id]["temp_list"][-1] - ): - temperature = self.coordinator.data.parsed[self.unique_id]["temp_list"][ - -1 - ] + if temperature > self.device_data.temp_list[-1]: + temperature = self.device_data.temp_list[-1] - elif ( - temperature - < self.coordinator.data.parsed[self.unique_id]["temp_list"][0] - ): - temperature = self.coordinator.data.parsed[self.unique_id]["temp_list"][ - 0 - ] + elif temperature < self.device_data.temp_list[0]: + temperature = self.device_data.temp_list[0] else: return @@ -263,10 +242,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if ( - "fanLevel" - not in self.coordinator.data.parsed[self.unique_id]["active_features"] - ): + if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Fanlevel") await self._async_set_ac_state_property("fanLevel", fan_mode) @@ -278,7 +254,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): return # Turn on if not currently on. - if not self.coordinator.data.parsed[self.unique_id]["on"]: + if not self.device_data.device_on: await self._async_set_ac_state_property("on", True) await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode]) @@ -286,10 +262,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" - if ( - "swing" - not in self.coordinator.data.parsed[self.unique_id]["active_features"] - ): + if "swing" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Swing") await self._async_set_ac_state_property("swing", swing_mode) @@ -309,13 +282,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): params = { "name": name, "value": value, - "ac_states": self.coordinator.data.parsed[self.unique_id]["ac_states"], + "ac_states": self.device_data.ac_states, "assumed_state": assumed_state, } result = await self.async_send_command("set_ac_state", params) if result["result"]["status"] == "Success": - self.coordinator.data.parsed[self.unique_id][AC_STATE_TO_DATA[name]] = value + setattr(self.device_data, AC_STATE_TO_DATA[name], value) self.async_write_ha_state() return diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index 6aaf53a0e73..a0321bf611e 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -1,12 +1,11 @@ """DataUpdateCoordinator for the Sensibo integration.""" from __future__ import annotations -from dataclasses import dataclass from datetime import timedelta -from typing import Any from pysensibo import SensiboClient from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo.model import SensiboData from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY @@ -17,33 +16,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT -MAX_POSSIBLE_STEP = 1000 - - -@dataclass -class MotionSensor: - """Dataclass for motionsensors.""" - - id: str - alive: bool | None = None - motion: bool | None = None - fw_ver: str | None = None - fw_type: str | None = None - is_main_sensor: bool | None = None - battery_voltage: int | None = None - humidity: int | None = None - temperature: float | None = None - model: str | None = None - rssi: int | None = None - - -@dataclass -class SensiboData: - """Dataclass for Sensibo data.""" - - raw: dict - parsed: dict - class SensiboDataUpdateCoordinator(DataUpdateCoordinator): """A Sensibo Data Update Coordinator.""" @@ -67,156 +39,13 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> SensiboData: """Fetch data from Sensibo.""" - devices = [] try: - data = await self.client.async_get_devices() - for dev in data["result"]: - devices.append(dev) + data = await self.client.async_get_devices_data() except AuthenticationError as error: raise ConfigEntryAuthFailed from error except SensiboError as error: raise UpdateFailed from error - if not devices: + if not data.raw: raise UpdateFailed("No devices found") - - device_data: dict[str, Any] = {} - for dev in devices: - unique_id = dev["id"] - mac = dev["macAddress"] - name = dev["room"]["name"] - temperature = dev["measurements"].get("temperature") - humidity = dev["measurements"].get("humidity") - ac_states = dev["acState"] - target_temperature = ac_states.get("targetTemperature") - hvac_mode = ac_states.get("mode") - running = ac_states.get("on") - fan_mode = ac_states.get("fanLevel") - swing_mode = ac_states.get("swing") - horizontal_swing_mode = ac_states.get("horizontalSwing") - light_mode = ac_states.get("light") - available = dev["connectionStatus"].get("isAlive", True) - capabilities = dev["remoteCapabilities"] - hvac_modes = list(capabilities["modes"]) - if hvac_modes: - hvac_modes.append("off") - current_capabilities = capabilities["modes"][ac_states.get("mode")] - fan_modes = current_capabilities.get("fanLevels") - swing_modes = current_capabilities.get("swing") - horizontal_swing_modes = current_capabilities.get("horizontalSwing") - light_modes = current_capabilities.get("light") - temperature_unit_key = dev.get("temperatureUnit") or ac_states.get( - "temperatureUnit" - ) - temperatures_list = ( - current_capabilities["temperatures"] - .get(temperature_unit_key, {}) - .get("values", [0, 1]) - ) - if temperatures_list: - diff = MAX_POSSIBLE_STEP - for i in range(len(temperatures_list) - 1): - if temperatures_list[i + 1] - temperatures_list[i] < diff: - diff = temperatures_list[i + 1] - temperatures_list[i] - temperature_step = diff - - active_features = list(ac_states) - full_features = set() - for mode in capabilities["modes"]: - if "temperatures" in capabilities["modes"][mode]: - full_features.add("targetTemperature") - if "swing" in capabilities["modes"][mode]: - full_features.add("swing") - if "fanLevels" in capabilities["modes"][mode]: - full_features.add("fanLevel") - if "horizontalSwing" in capabilities["modes"][mode]: - full_features.add("horizontalSwing") - if "light" in capabilities["modes"][mode]: - full_features.add("light") - - state = hvac_mode if hvac_mode else "off" - - fw_ver = dev["firmwareVersion"] - fw_type = dev["firmwareType"] - model = dev["productModel"] - - calibration_temp = dev["sensorsCalibration"].get("temperature") - calibration_hum = dev["sensorsCalibration"].get("humidity") - - # Sky plus supports functionality to use motion sensor as sensor for temp and humidity - if main_sensor := dev["mainMeasurementsSensor"]: - measurements = main_sensor["measurements"] - temperature = measurements.get("temperature") - humidity = measurements.get("humidity") - - motion_sensors: dict[str, Any] = {} - if dev["motionSensors"]: - for sensor in dev["motionSensors"]: - measurement = sensor["measurements"] - motion_sensors[sensor["id"]] = MotionSensor( - id=sensor["id"], - alive=sensor["connectionStatus"].get("isAlive"), - motion=measurement.get("motion"), - fw_ver=sensor.get("firmwareVersion"), - fw_type=sensor.get("firmwareType"), - is_main_sensor=sensor.get("isMainSensor"), - battery_voltage=measurement.get("batteryVoltage"), - humidity=measurement.get("humidity"), - temperature=measurement.get("temperature"), - model=sensor.get("productModel"), - rssi=measurement.get("rssi"), - ) - - # Add information for pure devices - pure_conf = dev["pureBoostConfig"] - pure_sensitivity = pure_conf.get("sensitivity") if pure_conf else None - pure_boost_enabled = pure_conf.get("enabled") if pure_conf else None - pm25 = dev["measurements"].get("pm25") - - # Binary sensors for main device - room_occupied = dev["roomIsOccupied"] - update_available = bool( - dev["firmwareVersion"] != dev["currentlyAvailableFirmwareVersion"] - ) - - device_data[unique_id] = { - "id": unique_id, - "mac": mac, - "name": name, - "ac_states": ac_states, - "temp": temperature, - "humidity": humidity, - "target_temp": target_temperature, - "hvac_mode": hvac_mode, - "on": running, - "fan_mode": fan_mode, - "swing_mode": swing_mode, - "horizontal_swing_mode": horizontal_swing_mode, - "light_mode": light_mode, - "available": available, - "hvac_modes": hvac_modes, - "fan_modes": fan_modes, - "swing_modes": swing_modes, - "horizontal_swing_modes": horizontal_swing_modes, - "light_modes": light_modes, - "temp_unit": temperature_unit_key, - "temp_list": temperatures_list, - "temp_step": temperature_step, - "active_features": active_features, - "full_features": full_features, - "state": state, - "fw_ver": fw_ver, - "fw_type": fw_type, - "model": model, - "calibration_temp": calibration_temp, - "calibration_hum": calibration_hum, - "full_capabilities": capabilities, - "motion_sensors": motion_sensors, - "pure_sensitivity": pure_sensitivity, - "pure_boost_enabled": pure_boost_enabled, - "pm25": pm25, - "room_occupied": room_occupied, - "update_available": update_available, - } - - return SensiboData(raw=data, parsed=device_data) + return data diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index bc0e2f49a2a..ce85ecf2a38 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any import async_timeout +from pysensibo.model import MotionSensor, SensiboDevice from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -11,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT -from .coordinator import MotionSensor, SensiboDataUpdateCoordinator +from .coordinator import SensiboDataUpdateCoordinator class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): @@ -28,7 +29,7 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): self._client = coordinator.client @property - def device_data(self) -> dict[str, Any]: + def device_data(self) -> SensiboDevice: """Return data for device.""" return self.coordinator.data.parsed[self._device_id] @@ -44,15 +45,15 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): """Initiate Sensibo Number.""" super().__init__(coordinator, device_id) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.device_data["id"])}, - name=self.device_data["name"], - connections={(CONNECTION_NETWORK_MAC, self.device_data["mac"])}, + identifiers={(DOMAIN, self.device_data.id)}, + name=self.device_data.name, + connections={(CONNECTION_NETWORK_MAC, self.device_data.mac)}, manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", - model=self.device_data["model"], - sw_version=self.device_data["fw_ver"], - hw_version=self.device_data["fw_type"], - suggested_area=self.device_data["name"], + model=self.device_data.model, + sw_version=self.device_data.fw_ver, + hw_version=self.device_data.fw_type, + suggested_area=self.device_data.name, ) async def async_send_command( @@ -108,7 +109,7 @@ class SensiboMotionBaseEntity(SensiboBaseEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor_id)}, - name=f"{self.device_data['name']} Motion Sensor {name}", + name=f"{self.device_data.name} Motion Sensor {name}", via_device=(DOMAIN, device_id), manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", @@ -120,4 +121,4 @@ class SensiboMotionBaseEntity(SensiboBaseEntity): @property def sensor_data(self) -> MotionSensor: """Return data for device.""" - return self.device_data["motion_sensors"][self._sensor_id] + return self.device_data.motion_sensors[self._sensor_id] diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index f0132833d70..8313ddd4077 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.8"], + "requirements": ["pysensibo==1.0.9"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 001269ae168..69d9237da7a 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -84,25 +84,19 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = ( - f"{coordinator.data.parsed[device_id]['name']} {entity_description.name}" - ) + self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def value(self) -> float | None: """Return the value from coordinator data.""" - return self.coordinator.data.parsed[self._device_id][ - self.entity_description.key - ] + return getattr(self.device_data, self.entity_description.key) async def async_set_value(self, value: float) -> None: """Set value for calibration.""" data = {self.entity_description.remote_key: value} result = await self.async_send_command("set_calibration", {"data": data}) if result["status"] == "success": - self.coordinator.data.parsed[self._device_id][ - self.entity_description.key - ] = value + setattr(self.device_data, self.entity_description.key, value) self.async_write_ha_state() return raise HomeAssistantError(f"Could not set calibration for device {self.name}") diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index d211a9dd223..c442fbc1374 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -58,7 +58,7 @@ async def async_setup_entry( SensiboSelect(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() for description in SELECT_TYPES - if description.key in device_data["full_features"] + if description.key in device_data.full_features ) @@ -77,37 +77,35 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = ( - f"{coordinator.data.parsed[device_id]['name']} {entity_description.name}" - ) + self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def current_option(self) -> str | None: """Return the current selected option.""" - return self.device_data[self.entity_description.remote_key] + return getattr(self.device_data, self.entity_description.remote_key) @property def options(self) -> list[str]: """Return possible options.""" - return self.device_data[self.entity_description.remote_options] or [] + return getattr(self.device_data, self.entity_description.remote_options) or [] async def async_select_option(self, option: str) -> None: """Set state to the selected option.""" - if self.entity_description.key not in self.device_data["active_features"]: + if self.entity_description.key not in self.device_data.active_features: raise HomeAssistantError( - f"Current mode {self.device_data['hvac_mode']} doesn't support setting {self.entity_description.name}" + f"Current mode {self.device_data.hvac_mode} doesn't support setting {self.entity_description.name}" ) params = { "name": self.entity_description.key, "value": option, - "ac_states": self.device_data["ac_states"], + "ac_states": self.device_data.ac_states, "assumed_state": False, } result = await self.async_send_command("set_ac_state", params) if result["result"]["status"] == "Success": - self.device_data[self.entity_description.remote_key] = option + setattr(self.device_data, self.entity_description.remote_key, option) self.async_write_ha_state() return diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 21f2bf2b5da..307cfac6003 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -3,7 +3,8 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any + +from pysensibo.model import MotionSensor, SensiboDevice from homeassistant.components.sensor import ( SensorDeviceClass, @@ -25,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import DOMAIN -from .coordinator import MotionSensor, SensiboDataUpdateCoordinator +from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity @@ -40,7 +41,7 @@ class MotionBaseEntityDescriptionMixin: class DeviceBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" - value_fn: Callable[[dict[str, Any]], StateType] + value_fn: Callable[[SensiboDevice], StateType] @dataclass @@ -106,13 +107,13 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, name="PM2.5", icon="mdi:air-filter", - value_fn=lambda data: data["pm25"], + value_fn=lambda data: data.pm25, ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", name="Pure Sensitivity", icon="mdi:air-filter", - value_fn=lambda data: data["pure_sensitivity"], + value_fn=lambda data: data.pure_sensitivity, ), ) @@ -129,15 +130,15 @@ async def async_setup_entry( entities.extend( SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) for device_id, device_data in coordinator.data.parsed.items() - for sensor_id, sensor_data in device_data["motion_sensors"].items() + for sensor_id, sensor_data in device_data.motion_sensors.items() for description in MOTION_SENSOR_TYPES - if device_data["motion_sensors"] + if device_data.motion_sensors ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() for description in DEVICE_SENSOR_TYPES - if device_data[description.key] is not None + if getattr(device_data, description.key) is not None ) async_add_entities(entities) @@ -166,7 +167,7 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" self._attr_name = ( - f"{self.device_data['name']} Motion Sensor {entity_description.name}" + f"{self.device_data.name} Motion Sensor {entity_description.name}" ) @property @@ -193,7 +194,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data['name']} {entity_description.name}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def native_value(self) -> StateType: diff --git a/requirements_all.txt b/requirements_all.txt index ad14a435446..6bbd9c38ce1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1767,7 +1767,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.8 +pysensibo==1.0.9 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7cda0ac7de..014b1acdf81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,7 +1169,7 @@ pyrituals==0.0.6 pyruckus==0.12 # homeassistant.components.sensibo -pysensibo==1.0.8 +pysensibo==1.0.9 # homeassistant.components.serial # homeassistant.components.zha From cfa8f99b1c12f7b05e2ee811a6d016b76ace5f61 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 24 Mar 2022 22:23:03 +0100 Subject: [PATCH 0688/1054] Update jinja2 to 3.1.0 (#68625) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cf68cda8531..7673e102929 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ hass-nabucasa==0.54.0 home-assistant-frontend==20220322.0 httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.0.3 +jinja2==3.1.0 lru-dict==1.1.7 paho-mqtt==1.6.1 pillow==9.0.1 diff --git a/requirements.txt b/requirements.txt index 923075bb5e1..7853ab1d4dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.0.3 +jinja2==3.1.0 PyJWT==2.1.0 cryptography==35.0.0 pip>=21.0,<22.1 diff --git a/setup.cfg b/setup.cfg index 1fbd8265e46..fbb1016a4d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = # httpcore, anyio, and h11 in gen_requirements_all httpx==0.22.0 ifaddr==0.1.7 - jinja2==3.0.3 + jinja2==3.1.0 PyJWT==2.1.0 # PyJWT has loose dependency. We want the latest one. cryptography==35.0.0 From 9a396c1d1696b74dea048d4e88d7933cc2e399dc Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 24 Mar 2022 23:52:25 +0100 Subject: [PATCH 0689/1054] remove unused constant (#68646) --- homeassistant/components/fronius/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 0f2a819818c..f13caf83a5d 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -51,7 +51,6 @@ if TYPE_CHECKING: _LOGGER: Final = logging.getLogger(__name__) -ELECTRIC_CHARGE_AMPERE_HOURS: Final = "Ah" ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" PLATFORM_SCHEMA = vol.All( From 63ca0e70bec57a75563ebb237a257a056df74b8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Mar 2022 14:23:53 -1000 Subject: [PATCH 0690/1054] Migrate Unifi Protect last tripped time attributes to their own entities (#68347) --- .../components/unifiprotect/binary_sensor.py | 21 +---- .../components/unifiprotect/sensor.py | 60 +++++++++++++ .../unifiprotect/test_binary_sensor.py | 14 +-- tests/components/unifiprotect/test_sensor.py | 86 ++++++++++++++++--- 4 files changed, 139 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index a5b41a446fc..7613ffb8ebf 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL +from homeassistant.const import ATTR_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,7 +27,6 @@ from .entity import ( async_all_device_entities, ) from .models import ProtectRequiredKeysMixin -from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @@ -39,8 +38,6 @@ class ProtectBinaryEntityDescription( ): """Describes UniFi Protect Binary Sensor entity.""" - ufp_last_trip_value: str | None = None - MOUNT_DEVICE_CLASS_MAP = { MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR, @@ -57,7 +54,6 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( icon="mdi:doorbell-video", ufp_required_field="feature_flags.has_chime", ufp_value="is_ringing", - ufp_last_trip_value="last_ring", ), ProtectBinaryEntityDescription( key="dark", @@ -79,7 +75,6 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Motion Detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_pir_motion_detected", - ufp_last_trip_value="last_motion", ), ) @@ -89,7 +84,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Contact", device_class=BinarySensorDeviceClass.DOOR, ufp_value="is_opened", - ufp_last_trip_value="open_status_changed_at", ufp_enabled="is_contact_sensor_enabled", ), ProtectBinaryEntityDescription( @@ -104,7 +98,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Motion Detected", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", - ufp_last_trip_value="motion_detected_at", ufp_enabled="is_motion_sensor_enabled", ), ProtectBinaryEntityDescription( @@ -112,7 +105,6 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Tampering Detected", device_class=BinarySensorDeviceClass.TAMPER, ufp_value="is_tampering_detected", - ufp_last_trip_value="tampering_detected_at", ), ) @@ -122,7 +114,6 @@ MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( name="Motion", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", - ufp_last_trip_value="last_motion", ), ) @@ -215,16 +206,6 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): super()._async_update_device_from_protect() self._attr_is_on = self.entity_description.get_ufp_value(self.device) - if self.entity_description.ufp_last_trip_value is not None: - last_trip = get_nested_attr( - self.device, self.entity_description.ufp_last_trip_value - ) - attrs = self.extra_state_attributes or {} - self._attr_extra_state_attributes = { - **attrs, - ATTR_LAST_TRIP_TIME: last_trip, - } - # UP Sense can be any of the 3 contact sensor device classes if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor): self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get( diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 330c90880ab..b90688efc70 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -186,6 +186,15 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_required_field="voltage", precision=2, ), + ProtectSensorEntityDescription( + key="doorbell_last_trip_time", + name="Last Doorbell Ring", + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:doorbell-video", + ufp_required_field="feature_flags.has_chime", + ufp_value="last_ring", + entity_registry_enabled_default=False, + ), ) CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -252,6 +261,27 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_value_fn=_get_alarm_sound, ufp_enabled="is_alarm_sensor_enabled", ), + ProtectSensorEntityDescription( + key="door_last_trip_time", + name="Last Open", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="open_status_changed_at", + entity_registry_enabled_default=False, + ), + ProtectSensorEntityDescription( + key="motion_last_trip_time", + name="Last Motion Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="motion_detected_at", + entity_registry_enabled_default=False, + ), + ProtectSensorEntityDescription( + key="tampering_last_trip_time", + name="Last Tampering Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="tampering_detected_at", + entity_registry_enabled_default=False, + ), ) DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -399,6 +429,27 @@ MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ) +LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( + ProtectSensorEntityDescription( + key="motion_last_trip_time", + name="Last Motion Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="last_motion", + entity_registry_enabled_default=False, + ), +) + +MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( + ProtectSensorEntityDescription( + key="motion_last_trip_time", + name="Last Motion Detected", + device_class=SensorDeviceClass.TIMESTAMP, + ufp_value="last_motion", + entity_registry_enabled_default=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -412,6 +463,7 @@ async def async_setup_entry( all_descs=ALL_DEVICES_SENSORS, camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, sense_descs=SENSE_SENSORS, + light_descs=LIGHT_SENSORS, lock_descs=DOORLOCK_SENSORS, ) entities += _async_motion_entities(data) @@ -426,6 +478,14 @@ def _async_motion_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] for device in data.api.bootstrap.cameras.values(): + for description in MOTION_TRIP_SENSORS: + entities.append(ProtectDeviceSensor(data, device, description)) + _LOGGER.debug( + "Adding trip sensor entity %s for %s", + description.name, + device.name, + ) + if not device.feature_flags.has_smart_detect: continue diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 88f19e59d7d..60a3d5a8126 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -24,7 +24,6 @@ from homeassistant.components.unifiprotect.const import ( from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, - ATTR_LAST_TRIP_TIME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -230,7 +229,7 @@ async def test_binary_sensor_setup_light( entity_registry = er.async_get(hass) - for index, description in enumerate(LIGHT_SENSORS): + for description in LIGHT_SENSORS: unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, light, description ) @@ -244,9 +243,6 @@ async def test_binary_sensor_setup_light( assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - if index == 1: - assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1) - async def test_binary_sensor_setup_camera_all( hass: HomeAssistant, camera: Camera, now: datetime @@ -269,8 +265,6 @@ async def test_binary_sensor_setup_camera_all( assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - assert state.attributes[ATTR_LAST_TRIP_TIME] == now - timedelta(hours=1) - # Is Dark description = CAMERA_SENSORS[1] unique_id, entity_id = ids_from_device_description( @@ -333,8 +327,7 @@ async def test_binary_sensor_setup_sensor( entity_registry = er.async_get(hass) - expected_trip_time = now - timedelta(hours=1) - for index, description in enumerate(SENSE_SENSORS): + for description in SENSE_SENSORS: unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, sensor, description ) @@ -348,9 +341,6 @@ async def test_binary_sensor_setup_sensor( assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - if index != 1: - assert state.attributes[ATTR_LAST_TRIP_TIME] == expected_trip_time - async def test_binary_sensor_setup_sensor_none( hass: HomeAssistant, sensor_none: Sensor diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 1f5624c30a9..5b14ef3f2fb 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from copy import copy from datetime import datetime, timedelta -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock import pytest from pyunifiprotect.data import NVR, Camera, Event, Sensor @@ -21,6 +21,7 @@ from homeassistant.components.unifiprotect.sensor import ( CAMERA_DISABLED_SENSORS, CAMERA_SENSORS, MOTION_SENSORS, + MOTION_TRIP_SENSORS, NVR_DISABLED_SENSORS, NVR_SENSORS, OBJECT_TYPE_NONE, @@ -78,9 +79,6 @@ async def sensor_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - # 2 from all, 4 from sense, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 19, 14) - yield sensor_obj Sensor.__config__.validate_assignment = True @@ -117,8 +115,8 @@ async def sensor_none_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - # 2 from all, 4 from sense, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 19, 14) + # 4 from all, 5 from sense, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 22, 14) yield sensor_obj @@ -144,6 +142,7 @@ async def camera_fixture( camera_obj.channels[2]._api = mock_entry.api camera_obj.name = "Test Camera" camera_obj.feature_flags.has_smart_detect = True + camera_obj.feature_flags.has_chime = True camera_obj.is_smart_detected = False camera_obj.wired_connection_state = WiredConnectionState(phy_rate=1000) camera_obj.wifi_connection_state = WifiConnectionState( @@ -166,9 +165,6 @@ async def camera_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - # 3 from all, 6 from camera, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 22, 14) - yield camera_obj Camera.__config__.validate_assignment = True @@ -178,11 +174,21 @@ async def test_sensor_setup_sensor( hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor ): """Test sensor entity setup for sensor devices.""" + # 5 from all, 5 from sense, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 22, 14) entity_registry = er.async_get(hass) - expected_values = ("10", "10.0", "10.0", "10.0", "none") + expected_values = ( + "10", + "10.0", + "10.0", + "10.0", + "none", + ) for index, description in enumerate(SENSE_SENSORS): + if not description.entity_registry_enabled_default: + continue unique_id, entity_id = ids_from_device_description( Platform.SENSOR, sensor, description ) @@ -229,6 +235,8 @@ async def test_sensor_setup_sensor_none( STATE_UNAVAILABLE, ) for index, description in enumerate(SENSE_SENSORS): + if not description.entity_registry_enabled_default: + continue unique_id, entity_id = ids_from_device_description( Platform.SENSOR, sensor_none, description ) @@ -395,6 +403,8 @@ async def test_sensor_setup_camera( hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime ): """Test sensor entity setup for camera devices.""" + # 3 from all, 8 from camera, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 24, 14) entity_registry = er.async_get(hass) @@ -405,6 +415,8 @@ async def test_sensor_setup_camera( "20.0", ) for index, description in enumerate(CAMERA_SENSORS): + if not description.entity_registry_enabled_default: + continue unique_id, entity_id = ids_from_device_description( Platform.SENSOR, camera, description ) @@ -487,10 +499,37 @@ async def test_sensor_setup_camera( assert state.attributes[ATTR_EVENT_SCORE] == 0 +async def test_sensor_setup_camera_with_last_trip_time( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + mock_entry: MockEntityFixture, + camera: Camera, + now: datetime, +): + """Test sensor entity setup for camera devices with last trip time.""" + entity_registry = er.async_get(hass) + + # Last Trip Time + unique_id, entity_id = ids_from_device_description( + Platform.SENSOR, camera, MOTION_TRIP_SENSORS[0] + ) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.state == "2021-12-20T17:26:53+00:00" + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + async def test_sensor_update_motion( hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime ): """Test sensor motion entity.""" + # 3 from all, 8 from camera, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 24, 14) _, entity_id = ids_from_device_description( Platform.SENSOR, camera, MOTION_SENSORS[0] @@ -534,6 +573,8 @@ async def test_sensor_update_alarm( hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor, now: datetime ): """Test sensor motion entity.""" + # 5 from all, 5 from sense, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 22, 14) _, entity_id = ids_from_device_description( Platform.SENSOR, sensor, SENSE_SENSORS[4] @@ -571,3 +612,28 @@ async def test_sensor_update_alarm( assert state assert state.state == "smoke" await time_changed(hass, 10) + + +async def test_sensor_update_alarm_with_last_trip_time( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + mock_entry: MockEntityFixture, + sensor: Sensor, + now: datetime, +): + """Test sensor motion entity with last trip time.""" + + # Last Trip Time + unique_id, entity_id = ids_from_device_description( + Platform.SENSOR, sensor, SENSE_SENSORS[-3] + ) + entity_registry = er.async_get(hass) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.state == "2022-01-04T04:03:56+00:00" + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION From 20c0a5a838f90f8a3cd6b1649c45081b5fabdf10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Mar 2022 17:25:50 -0700 Subject: [PATCH 0691/1054] Add support for field descriptions in config flows (#68604) --- homeassistant/components/derivative/strings.json | 14 +++++++++++--- .../components/derivative/translations/en.json | 12 ++++++++++-- script/hassfest/translations.py | 15 ++++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index c21a486d039..b225653cafa 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -3,7 +3,6 @@ "step": { "user": { "title": "New Derivative sensor", - "description": "Precision controls the number of decimal digits in the output.\nIf the time window is not 0, the sensor's value is a time weighted moving average of derivatives within the window.\nThe derivative will be scaled according to the selected metric prefix and time unit of the derivative.", "data": { "name": "Name", "round": "Precision", @@ -11,6 +10,11 @@ "time_window": "Time window", "unit_prefix": "Metric prefix", "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative." } } } @@ -18,7 +22,6 @@ "options": { "step": { "options": { - "description": "[%key:component::derivative::config::step::user::description%]", "data": { "name": "[%key:component::derivative::config::step::user::data::name%]", "round": "[%key:component::derivative::config::step::user::data::round%]", @@ -26,8 +29,13 @@ "time_window": "[%key:component::derivative::config::step::user::data::time_window%]", "unit_prefix": "[%key:component::derivative::config::step::user::data::unit_prefix%]", "unit_time": "[%key:component::derivative::config::step::user::data::unit_time%]" + }, + "data_description": { + "round": "[%key:component::derivative::config::step::user::data_description::round%]", + "time_window": "[%key:component::derivative::config::step::user::data_description::time_window%]", + "unit_prefix": "[%key:component::derivative::config::step::user::data_description::unit_prefix%]." } } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json index b1fa702fe3c..cfa3bf2ab96 100644 --- a/homeassistant/components/derivative/translations/en.json +++ b/homeassistant/components/derivative/translations/en.json @@ -10,7 +10,11 @@ "unit_prefix": "Metric prefix", "unit_time": "Time unit" }, - "description": "Precision controls the number of decimal digits in the output.\nIf the time window is not 0, the sensor's value is a time weighted moving average of derivatives within the window.\nThe derivative will be scaled according to the selected metric prefix and time unit of the derivative.", + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative." + }, "title": "New Derivative sensor" } } @@ -26,7 +30,11 @@ "unit_prefix": "Metric prefix", "unit_time": "Time unit" }, - "description": "Precision controls the number of decimal digits in the output.\nIf the time window is not 0, the sensor's value is a time weighted moving average of derivatives within the window.\nThe derivative will be scaled according to the selected metric prefix and time unit of the derivative." + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative.." + } } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index b721b4d708d..5e64c70210c 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -110,6 +110,7 @@ def gen_data_entry_schema( step_title_class("title"): cv.string_with_no_html, vol.Optional("description"): cv.string_with_no_html, vol.Optional("data"): {str: cv.string_with_no_html}, + vol.Optional("data_description"): {str: cv.string_with_no_html}, vol.Optional("menu_options"): {str: cv.string_with_no_html}, } }, @@ -125,7 +126,19 @@ def gen_data_entry_schema( removed_title_validator, config, integration ) - return schema + def data_description_validator(value): + """Validate data description.""" + for step_info in value["step"].values(): + if "data_description" not in step_info: + continue + + for key in step_info["data_description"]: + if key not in step_info["data"]: + raise vol.Invalid(f"data_description key {key} is not in data") + + return value + + return vol.All(vol.Schema(schema), data_description_validator) def gen_strings_schema(config: Config, integration: Integration): From c5c34bc0d7c2e93949f0bf66543fd133fb905324 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 24 Mar 2022 14:58:38 -1000 Subject: [PATCH 0692/1054] Typing and code quality for recorder history (#68647) --- .strict-typing | 1 + homeassistant/components/recorder/__init__.py | 11 +- homeassistant/components/recorder/history.py | 157 +++++++++++------- homeassistant/components/recorder/models.py | 5 +- homeassistant/components/recorder/util.py | 4 +- homeassistant/components/sensor/recorder.py | 20 ++- mypy.ini | 11 ++ 7 files changed, 138 insertions(+), 71 deletions(-) diff --git a/.strict-typing b/.strict-typing index a7a157c54bf..6b5d1e5d751 100644 --- a/.strict-typing +++ b/.strict-typing @@ -163,6 +163,7 @@ homeassistant.components.pure_energie.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* +homeassistant.components.recorder.history homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 205b544f6ab..bbe1b676741 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -227,7 +227,7 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: return hass.data[DATA_INSTANCE].entity_filter(entity_id) -def run_information(hass, point_in_time: datetime | None = None): +def run_information(hass, point_in_time: datetime | None = None) -> RecorderRuns | None: """Return information about current run. There is also the run that covers point_in_time. @@ -240,7 +240,9 @@ def run_information(hass, point_in_time: datetime | None = None): return run_information_with_session(session, point_in_time) -def run_information_from_instance(hass, point_in_time: datetime | None = None): +def run_information_from_instance( + hass, point_in_time: datetime | None = None +) -> RecorderRuns | None: """Return information about current run from the existing instance. Does not query the database for older runs. @@ -249,9 +251,12 @@ def run_information_from_instance(hass, point_in_time: datetime | None = None): if point_in_time is None or point_in_time > ins.recording_start: return ins.run_info + return None -def run_information_with_session(session, point_in_time: datetime | None = None): +def run_information_with_session( + session, point_in_time: datetime | None = None +) -> RecorderRuns | None: """Return information about current run from the database.""" recorder_runs = RecorderRuns diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 9348e1cbb4b..82a74c36a83 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import defaultdict +from collections.abc import Iterable, MutableMapping from datetime import datetime from itertools import groupby import logging @@ -10,6 +11,7 @@ from typing import Any from sqlalchemy import Column, Text, and_, bindparam, func, or_ from sqlalchemy.ext import baked +from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal from homeassistant.components import recorder @@ -18,6 +20,7 @@ import homeassistant.util.dt as dt_util from .models import ( LazyState, + RecorderRuns, StateAttributes, States, process_timestamp_to_utc_isoformat, @@ -123,29 +126,50 @@ def bake_query_and_join_attributes( return bakery(lambda session: session.query(*QUERY_STATES)), True -def async_setup(hass): +def async_setup(hass: HomeAssistant) -> None: """Set up the history hooks.""" hass.data[HISTORY_BAKERY] = baked.bakery() -def get_significant_states(hass, *args, **kwargs): +def get_significant_states( + hass: HomeAssistant, + start_time: datetime, + end_time: datetime | None = None, + entity_ids: list[str] | None = None, + filters: Any | None = None, + include_start_time_state: bool = True, + significant_changes_only: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass) as session: - return get_significant_states_with_session(hass, session, *args, **kwargs) + return get_significant_states_with_session( + hass, + session, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) def get_significant_states_with_session( - hass, - session, - start_time, - end_time=None, - entity_ids=None, - filters=None, - include_start_time_state=True, - significant_changes_only=True, - minimal_response=False, - no_attributes=False, -): + hass: HomeAssistant, + session: Session, + start_time: datetime, + end_time: datetime | None = None, + entity_ids: list[str] | None = None, + filters: Any = None, + include_start_time_state: bool = True, + significant_changes_only: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """ Return states changes during UTC period start_time - end_time. @@ -238,7 +262,7 @@ def state_changes_during_period( descending: bool = False, limit: int | None = None, include_start_time_state: bool = True, -) -> dict[str, list[State]]: +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: baked_query, join_attributes = bake_query_and_join_attributes( @@ -288,7 +312,9 @@ def state_changes_during_period( ) -def get_last_state_changes(hass, number_of_states, entity_id): +def get_last_state_changes( + hass: HomeAssistant, number_of_states: int, entity_id: str +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() @@ -330,20 +356,21 @@ def get_last_state_changes(hass, number_of_states, entity_id): def get_states( - hass, - utc_point_in_time, - entity_ids=None, - run=None, - filters=None, - no_attributes=False, -): + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + filters: Any = None, + no_attributes: bool = False, +) -> list[LazyState]: """Return the states at a specific point in time.""" - if run is None: - run = recorder.run_information_from_instance(hass, utc_point_in_time) - + if ( + run is None + and (run := (recorder.run_information_from_instance(hass, utc_point_in_time))) + is None + ): # History did not run before utc_point_in_time - if run is None: - return [] + return [] with session_scope(hass=hass) as session: return _get_states_with_session( @@ -352,26 +379,27 @@ def get_states( def _get_states_with_session( - hass, - session, - utc_point_in_time, - entity_ids=None, - run=None, - filters=None, - no_attributes=False, -): + hass: HomeAssistant, + session: Session, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + filters: Any | None = None, + no_attributes: bool = False, +) -> list[LazyState]: """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: return _get_single_entity_states_with_session( hass, session, utc_point_in_time, entity_ids[0], no_attributes ) - if run is None: - run = recorder.run_information_with_session(session, utc_point_in_time) - + if ( + run is None + and (run := (recorder.run_information_from_instance(hass, utc_point_in_time))) + is None + ): # History did not run before utc_point_in_time - if run is None: - return [] + return [] # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. @@ -444,13 +472,17 @@ def _get_states_with_session( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) - attr_cache = {} + attr_cache: dict[str, dict[str, Any]] = {} return [LazyState(row, attr_cache) for row in execute(query)] def _get_single_entity_states_with_session( - hass, session, utc_point_in_time, entity_id, no_attributes=False -): + hass: HomeAssistant, + session: Session, + utc_point_in_time: datetime, + entity_id: str, + no_attributes: bool = False, +) -> list[LazyState]: # Use an entirely different (and extremely fast) query if we only # have a single entity id baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) @@ -473,16 +505,16 @@ def _get_single_entity_states_with_session( def _sorted_states_to_dict( - hass, - session, - states, - start_time, - entity_ids, - filters=None, - include_start_time_state=True, - minimal_response=False, - no_attributes=False, -): + hass: HomeAssistant, + session: Session, + states: Iterable[States], + start_time: datetime, + entity_ids: list[str] | None, + filters: Any = None, + include_start_time_state: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data @@ -494,7 +526,7 @@ def _sorted_states_to_dict( each list of states, otherwise our graphs won't start on the Y axis correctly. """ - result = defaultdict(list) + result: dict[str, list[LazyState | dict[str, Any]]] = defaultdict(list) # Set all entity IDs to empty lists in result set to maintain the order if entity_ids is not None: for ent_id in entity_ids: @@ -526,10 +558,10 @@ def _sorted_states_to_dict( _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat # Append all changes to it - for ent_id, group in groupby(states, lambda state: state.entity_id): + for ent_id, group in groupby(states, lambda state: state.entity_id): # type: ignore[no-any-return] domain = split_entity_id(ent_id)[0] ent_results = result[ent_id] - attr_cache = {} + attr_cache: dict[str, dict[str, Any]] = {} if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: ent_results.extend(LazyState(db_state, attr_cache) for db_state in group) @@ -542,6 +574,7 @@ def _sorted_states_to_dict( ent_results.append(LazyState(next(group), attr_cache)) prev_state = ent_results[-1] + assert isinstance(prev_state, LazyState) initial_state_count = len(ent_results) for db_state in group: @@ -570,7 +603,13 @@ def _sorted_states_to_dict( return {key: val for key, val in result.items() if val} -def get_state(hass, utc_point_in_time, entity_id, run=None, no_attributes=False): +def get_state( + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_id: str, + run: RecorderRuns | None = None, + no_attributes: bool = False, +) -> LazyState | None: """Return a state at a specific point in time.""" - states = get_states(hass, utc_point_in_time, (entity_id,), run, None, no_attributes) + states = get_states(hass, utc_point_in_time, [entity_id], run, None, no_attributes) return states[0] if states else None diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 292abf87fd7..660b6355911 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -22,6 +22,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.engine.row import Row from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session @@ -536,7 +537,9 @@ class LazyState(State): "_attr_cache", ] - def __init__(self, row, attr_cache=None): # pylint: disable=super-init-not-called + def __init__( # pylint: disable=super-init-not-called + self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + ) -> None: """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index d1d16be4ae2..1f9d8bfaa26 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -114,7 +114,7 @@ def commit(session, work): def execute( qry: Query, to_native: bool = False, validate_entity_ids: bool = True -) -> list | None: +) -> list: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. @@ -157,7 +157,7 @@ def execute( raise time.sleep(QUERY_RETRY_WAIT) - return None + assert False # unreachable def validate_or_move_away_sqlite_database(dburl: str) -> bool: diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 4afc514d457..ae148d45e72 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -2,12 +2,12 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, MutableMapping import datetime import itertools import logging import math -from typing import Any +from typing import Any, cast from sqlalchemy.orm.session import Session @@ -19,6 +19,7 @@ from homeassistant.components.recorder import ( ) from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.models import ( + LazyState, StatisticData, StatisticMetaData, StatisticResult, @@ -416,9 +417,9 @@ def _compile_statistics( # noqa: C901 entities_full_history = [ i.entity_id for i in sensor_states if "sum" in wanted_statistics[i.entity_id] ] - history_list = {} + history_list: MutableMapping[str, Iterable[LazyState | State | dict[str, Any]]] = {} if entities_full_history: - history_list = history.get_significant_states_with_session( # type: ignore[no-untyped-call] + history_list = history.get_significant_states_with_session( hass, session, start - datetime.timedelta.resolution, @@ -432,7 +433,7 @@ def _compile_statistics( # noqa: C901 if "sum" not in wanted_statistics[i.entity_id] ] if entities_significant_history: - _history_list = history.get_significant_states_with_session( # type: ignore[no-untyped-call] + _history_list = history.get_significant_states_with_session( hass, session, start - datetime.timedelta.resolution, @@ -455,7 +456,14 @@ def _compile_statistics( # noqa: C901 device_class = _state.attributes.get(ATTR_DEVICE_CLASS) entity_history = history_list[entity_id] unit, fstates = _normalize_states( - hass, session, old_metadatas, entity_history, device_class, entity_id + hass, + session, + old_metadatas, + # entity_history does not contain minimal responses + # so we must cast here + cast(list[State], entity_history), + device_class, + entity_id, ) if not fstates: diff --git a/mypy.ini b/mypy.ini index 293b8a62c37..1f868ce05a9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1595,6 +1595,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.history] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.purge] check_untyped_defs = true disallow_incomplete_defs = true From 34ace2e1cddfa47017e1e6b33de87a4e2942bb75 Mon Sep 17 00:00:00 2001 From: RDFurman Date: Thu, 24 Mar 2022 19:17:36 -0600 Subject: [PATCH 0693/1054] Honeywell away temps (#54704) --- .../components/honeywell/__init__.py | 33 +++++++++++- homeassistant/components/honeywell/climate.py | 4 +- .../components/honeywell/config_flow.py | 48 ++++++++++++++++- homeassistant/components/honeywell/const.py | 2 + .../components/honeywell/strings.json | 12 +++++ .../components/honeywell/translations/en.json | 12 +++++ .../components/honeywell/test_config_flow.py | 51 ++++++++++++++++++- tests/components/honeywell/test_init.py | 33 ++++++++++++ 8 files changed, 188 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index bafd4c470db..d141822e2ea 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -6,19 +6,48 @@ import somecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util import Throttle -from .const import _LOGGER, CONF_DEV_ID, CONF_LOC_ID, DOMAIN +from .const import ( + _LOGGER, + CONF_COOL_AWAY_TEMPERATURE, + CONF_DEV_ID, + CONF_HEAT_AWAY_TEMPERATURE, + CONF_LOC_ID, + DOMAIN, +) UPDATE_LOOP_SLEEP_TIME = 5 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) PLATFORMS = [Platform.CLIMATE] +MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} + + +@callback +def _async_migrate_data_to_options( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + if not MIGRATE_OPTIONS_KEYS.intersection(config_entry.data): + return + hass.config_entries.async_update_entry( + config_entry, + data={ + k: v for k, v in config_entry.data.items() if k not in MIGRATE_OPTIONS_KEYS + }, + options={ + **config_entry.options, + **{k: config_entry.data.get(k) for k in MIGRATE_OPTIONS_KEYS}, + }, + ) + async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: """Set up the Honeywell thermostat.""" + _async_migrate_data_to_options(hass, config) + username = config.data[CONF_USERNAME] password = config.data[CONF_PASSWORD] diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index f0e18953402..f8c07de7184 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -81,8 +81,8 @@ PARALLEL_UPDATES = 1 async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): """Set up the Honeywell thermostat.""" - cool_away_temp = config.data.get(CONF_COOL_AWAY_TEMPERATURE) - heat_away_temp = config.data.get(CONF_HEAT_AWAY_TEMPERATURE) + cool_away_temp = config.options.get(CONF_COOL_AWAY_TEMPERATURE) + heat_away_temp = config.options.get(CONF_HEAT_AWAY_TEMPERATURE) data = hass.data[DOMAIN][config.entry_id] diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 505a49f062b..e6fdd9b54bd 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -3,9 +3,16 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from . import get_somecomfort_client -from .const import DOMAIN +from .const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DEFAULT_COOL_AWAY_TEMPERATURE, + DEFAULT_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -42,3 +49,42 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return client is not None + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Options callback for Honeywell.""" + return HoneywellOptionsFlowHandler(config_entry) + + +class HoneywellOptionsFlowHandler(config_entries.OptionsFlow): + """Config flow options for Honeywell.""" + + def __init__(self, entry: config_entries.ConfigEntry) -> None: + """Initialize Honeywell options flow.""" + self.config_entry = entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title=DOMAIN, data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_COOL_AWAY_TEMPERATURE, + default=self.config_entry.options.get( + CONF_COOL_AWAY_TEMPERATURE, DEFAULT_COOL_AWAY_TEMPERATURE + ), + ): int, + vol.Required( + CONF_HEAT_AWAY_TEMPERATURE, + default=self.config_entry.options.get( + CONF_HEAT_AWAY_TEMPERATURE, DEFAULT_HEAT_AWAY_TEMPERATURE + ), + ): int, + } + ), + ) diff --git a/homeassistant/components/honeywell/const.py b/homeassistant/components/honeywell/const.py index 2dce56046a3..08e7c3fc14e 100644 --- a/homeassistant/components/honeywell/const.py +++ b/homeassistant/components/honeywell/const.py @@ -5,6 +5,8 @@ DOMAIN = "honeywell" CONF_COOL_AWAY_TEMPERATURE = "away_cool_temperature" CONF_HEAT_AWAY_TEMPERATURE = "away_heat_temperature" +DEFAULT_COOL_AWAY_TEMPERATURE = 88 +DEFAULT_HEAT_AWAY_TEMPERATURE = 61 CONF_DEV_ID = "thermostat" CONF_LOC_ID = "location" diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index ce76b571996..1e20c0b1e81 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -13,5 +13,17 @@ "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } + }, + "options": { + "step": { + "init": { + "title": "Honeywell Options", + "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit.", + "data": { + "away_cool_temperature": "Away cool temperature", + "away_heat_temperature": "Away heat temperature" + } + } + } } } diff --git a/homeassistant/components/honeywell/translations/en.json b/homeassistant/components/honeywell/translations/en.json index 454093c5b3e..168d3a5b93d 100644 --- a/homeassistant/components/honeywell/translations/en.json +++ b/homeassistant/components/honeywell/translations/en.json @@ -13,5 +13,17 @@ "title": "Honeywell Total Connect Comfort (US)" } } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Away cool temperature", + "away_heat_temperature": "Away heat temperature" + }, + "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit.", + "title": "Honeywell Options" + } + } } } \ No newline at end of file diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 47897cf246d..b93e359c1ac 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -4,10 +4,16 @@ from unittest.mock import patch import somecomfort from homeassistant import data_entry_flow -from homeassistant.components.honeywell.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.components.honeywell.const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry + FAKE_CONFIG = { "username": "fake", "password": "user", @@ -49,3 +55,44 @@ async def test_create_entry(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == FAKE_CONFIG + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_show_option_form( + hass: HomeAssistant, config_entry: MockConfigEntry, location +) -> None: + """Test that the option form is shown.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + +@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) +async def test_create_option_entry( + hass: HomeAssistant, config_entry: MockConfigEntry, location +) -> None: + """Test that the config entry is created.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + options_form = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_configure( + options_form["flow_id"], + user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + } diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 49917aae151..83be3a05873 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -4,11 +4,19 @@ from unittest.mock import create_autospec, patch import somecomfort +from homeassistant.components.honeywell.const import ( + CONF_COOL_AWAY_TEMPERATURE, + CONF_HEAT_AWAY_TEMPERATURE, + DOMAIN, +) from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} + @patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_setup_entry(hass: HomeAssistant, config_entry: MockConfigEntry): @@ -48,3 +56,28 @@ async def test_setup_multiple_thermostats_with_same_deviceid( assert config_entry.state is ConfigEntryState.LOADED assert hass.states.async_entity_ids_count() == 1 assert "Platform honeywell does not generate unique IDs" not in caplog.text + + +async def test_away_temps_migration(hass: HomeAssistant) -> None: + """Test away temps migrate to config options.""" + legacy_config = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "fake", + CONF_PASSWORD: "user", + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + }, + options={}, + ) + + with patch( + "homeassistant.components.honeywell.somecomfort.SomeComfort", + ): + legacy_config.add_to_hass(hass) + await hass.config_entries.async_setup(legacy_config.entry_id) + await hass.async_block_till_done() + assert legacy_config.options == { + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + } From 1ce32ad3c07c55dd531715f1f7aa03b78453436a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 08:50:10 +0100 Subject: [PATCH 0694/1054] Update mypy to 0.942 (#68652) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8b56e50b9e2..a78f7d57a7b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.3.2 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.941 +mypy==0.942 pre-commit==2.17.0 pylint==2.12.2 pipdeptree==2.2.1 From 70648d6e3b42bb0910e33e5c6a4e1e8c0789d82c Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Fri, 25 Mar 2022 06:39:13 -0400 Subject: [PATCH 0695/1054] Bump Blinkpy to 0.19.0 (#68653) --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 58e2d3a1b52..fbe63557902 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,7 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": ["blinkpy==0.18.0"], + "requirements": ["blinkpy==0.19.0"], "codeowners": ["@fronzbot"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 6bbd9c38ce1..fc4522c6e5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -395,7 +395,7 @@ bizkaibus==0.1.1 blebox_uniapi==1.3.3 # homeassistant.components.blink -blinkpy==0.18.0 +blinkpy==0.19.0 # homeassistant.components.blinksticklight blinkstick==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 014b1acdf81..5f493a5fe07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,7 +298,7 @@ bimmer_connected==0.8.11 blebox_uniapi==1.3.3 # homeassistant.components.blink -blinkpy==0.18.0 +blinkpy==0.19.0 # homeassistant.components.bond bond-api==0.1.16 From b3632f3efea19ffff326a0214f512239ca561e30 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 17:04:12 +0100 Subject: [PATCH 0696/1054] Simplify zha IEEE validation schema (#68645) --- homeassistant/components/zha/api.py | 42 +++++++++++++++-------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 060e8a5d5eb..b572b9e8775 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -104,12 +104,14 @@ SERVICE_WARNING_DEVICE_WARN = "warning_device_warn" SERVICE_ZIGBEE_BIND = "service_zigbee_bind" IEEE_SERVICE = "ieee_based_service" +IEEE_SCHEMA = vol.All(cv.string, EUI64.convert) + SERVICE_PERMIT_PARAMS = { - vol.Optional(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Optional(ATTR_IEEE): IEEE_SCHEMA, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), - vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): vol.All(cv.string, EUI64.convert), + vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): IEEE_SCHEMA, vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): vol.All( cv.string, convert_install_code ), @@ -126,12 +128,12 @@ SERVICE_SCHEMAS = { IEEE_SERVICE: vol.Schema( vol.All( cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE), - {vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert)}, + {vol.Required(ATTR_IEEE): IEEE_SCHEMA}, ) ), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema( { - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -142,7 +144,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( { - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED ): cv.positive_int, @@ -156,7 +158,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_WARN: vol.Schema( { - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY ): cv.positive_int, @@ -177,7 +179,7 @@ SERVICE_SCHEMAS = { ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -322,7 +324,7 @@ async def websocket_get_groups( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/device", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, } ) @websocket_api.async_response @@ -503,7 +505,7 @@ async def websocket_remove_group_members( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, } ) @websocket_api.async_response @@ -553,7 +555,7 @@ async def websocket_update_topology( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, } ) @websocket_api.async_response @@ -594,7 +596,7 @@ async def websocket_device_clusters( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -641,7 +643,7 @@ async def websocket_device_cluster_attributes( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -701,7 +703,7 @@ async def websocket_device_cluster_commands( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -756,7 +758,7 @@ async def websocket_read_zigbee_cluster_attributes( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bindable", - vol.Required(ATTR_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_IEEE): IEEE_SCHEMA, } ) @websocket_api.async_response @@ -789,8 +791,8 @@ async def websocket_get_bindable_devices( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", - vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), - vol.Required(ATTR_TARGET_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, + vol.Required(ATTR_TARGET_IEEE): IEEE_SCHEMA, } ) @websocket_api.async_response @@ -817,8 +819,8 @@ async def websocket_bind_devices( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", - vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), - vol.Required(ATTR_TARGET_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, + vol.Required(ATTR_TARGET_IEEE): IEEE_SCHEMA, } ) @websocket_api.async_response @@ -862,7 +864,7 @@ def is_cluster_binding(value: Any) -> ClusterBinding: @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/bind", - vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, vol.Required(GROUP_ID): cv.positive_int, vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), } @@ -884,7 +886,7 @@ async def websocket_bind_group( @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/unbind", - vol.Required(ATTR_SOURCE_IEEE): vol.All(cv.string, EUI64.convert), + vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA, vol.Required(GROUP_ID): cv.positive_int, vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), } From 931d5b56976f0850871ad60807c29d9965e03bf9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 17:44:49 +0100 Subject: [PATCH 0697/1054] Add zha typing [core.gateway] (2) (#68644) --- homeassistant/components/zha/__init__.py | 17 ++++++++++------- homeassistant/components/zha/core/const.py | 9 +++++---- homeassistant/components/zha/core/gateway.py | 16 +++++++++++----- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 4f1e80e0a7b..c512e104ae8 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -109,8 +109,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_registry = await hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee))}, # type: ignore[attr-defined] - identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, # type: ignore[attr-defined] + connections={(CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee))}, + identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, name="Zigbee Coordinator", manufacturer="ZHA", model=zha_gateway.radio_description, @@ -120,8 +120,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_zha_shutdown(event): """Handle shutdown tasks.""" - await zha_data[DATA_ZHA_GATEWAY].shutdown() - await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() + zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] + await zha_gateway.shutdown() + await zha_gateway.async_update_device_storage() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown @@ -132,8 +133,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload ZHA config entry.""" - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_update_device_storage() + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + await zha_gateway.shutdown() + await zha_gateway.async_update_device_storage() GROUP_PROBE.cleanup() api.async_unload_api(hass) @@ -153,7 +155,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_load_entities(hass: HomeAssistant) -> None: """Load entities after integration was setup.""" - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_initialize_devices_and_entities() + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + await zha_gateway.async_initialize_devices_and_entities() to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] results = await asyncio.gather(*to_setup, return_exceptions=True) for res in results: diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 9216b6dd600..b3f06b9eba6 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -6,6 +6,7 @@ import logging import bellows.zigbee.application import voluptuous as vol +import zigpy.application from zigpy.config import CONF_DEVICE_PATH # noqa: F401 # pylint: disable=unused-import import zigpy.types as t import zigpy_deconz.zigbee.application @@ -16,8 +17,6 @@ import zigpy_znp.zigbee.application from homeassistant.const import Platform import homeassistant.helpers.config_validation as cv -from .typing import CALLABLE_T - ATTR_ARGS = "args" ATTR_ATTRIBUTE = "attribute" ATTR_ATTRIBUTE_ID = "attribute_id" @@ -224,6 +223,8 @@ ZHA_CONFIG_SCHEMAS = { ZHA_ALARM_OPTIONS: CONF_ZHA_ALARM_SCHEMA, } +_ControllerClsType = type[zigpy.application.ControllerApplication] + class RadioType(enum.Enum): """Possible options for radio type.""" @@ -262,13 +263,13 @@ class RadioType(enum.Enum): return radio.name raise ValueError - def __init__(self, description: str, controller_cls: CALLABLE_T) -> None: + def __init__(self, description: str, controller_cls: _ControllerClsType) -> None: """Init instance.""" self._desc = description self._ctrl_cls = controller_cls @property - def controller(self) -> CALLABLE_T: + def controller(self) -> _ControllerClsType: """Return controller class.""" return self._ctrl_cls diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 636e161d45c..6c600bf93d6 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -14,6 +14,7 @@ import traceback from typing import TYPE_CHECKING, Any, Union from serial import SerialException +from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE import zigpy.device import zigpy.endpoint @@ -26,10 +27,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import ( CONNECTION_ZIGBEE, + DeviceRegistry, async_get_registry as get_dev_reg, ) from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import ( + EntityRegistry, async_entries_for_device, async_get_registry as get_ent_reg, ) @@ -93,6 +96,7 @@ if TYPE_CHECKING: from logging import Filter, LogRecord from ..entity import ZhaEntity + from .store import ZhaStorage _LogFilterType = Union[Filter, Callable[[LogRecord], int]] @@ -116,6 +120,13 @@ class DevicePairingStatus(Enum): class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" + # -- Set in async_initialize -- + zha_storage: ZhaStorage + ha_device_registry: DeviceRegistry + ha_entity_registry: EntityRegistry + application_controller: ControllerApplication + radio_description: str + def __init__( self, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry ) -> None: @@ -128,11 +139,6 @@ class ZHAGateway: self._device_registry: collections.defaultdict[ EUI64, list[EntityReference] ] = collections.defaultdict(list) - self.zha_storage = None - self.ha_device_registry = None - self.ha_entity_registry = None - self.application_controller = None - self.radio_description = None self._log_levels: dict[str, dict[str, int]] = { DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), DEBUG_LEVEL_CURRENT: async_capture_log_levels(), From 6ac9c105c1414c71f6608e956a4fbd11746df921 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 17:45:47 +0100 Subject: [PATCH 0698/1054] Improve zha websocket api logic (#68648) --- homeassistant/components/zha/api.py | 51 +++++++++++++---------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index b572b9e8775..c1115d4cccb 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -334,17 +334,17 @@ async def websocket_get_device( """Get ZHA devices.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = msg[ATTR_IEEE] - device = None - if ieee in zha_gateway.devices: - device = zha_gateway.devices[ieee].zha_device_info - if not device: + + if not (zha_device := zha_gateway.devices.get(ieee)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Device not found" ) ) return - connection.send_result(msg[ID], device) + + device_info = zha_device.zha_device_info + connection.send_result(msg[ID], device_info) @websocket_api.require_admin @@ -361,18 +361,17 @@ async def websocket_get_group( """Get ZHA group.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_id: int = msg[GROUP_ID] - group = None - if group_id in zha_gateway.groups: - group = zha_gateway.groups.get(group_id).group_info - if not group: + if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return - connection.send_result(msg[ID], group) + + group_info = zha_group.group_info + connection.send_result(msg[ID], group_info) def cv_group_member(value: Any) -> GroupMember: @@ -453,18 +452,16 @@ async def websocket_add_group_members( zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_id: int = msg[GROUP_ID] members: list[GroupMember] = msg[ATTR_MEMBERS] - zha_group = None - if group_id in zha_gateway.groups: - zha_group = zha_gateway.groups.get(group_id) - await zha_group.async_add_members(members) - if not zha_group: + if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return + + await zha_group.async_add_members(members) ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @@ -485,18 +482,16 @@ async def websocket_remove_group_members( zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] group_id: int = msg[GROUP_ID] members: list[GroupMember] = msg[ATTR_MEMBERS] - zha_group = None - if group_id in zha_gateway.groups: - zha_group = zha_gateway.groups.get(group_id) - await zha_group.async_remove_members(members) - if not zha_group: + if not (zha_group := zha_gateway.groups.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return + + await zha_group.async_remove_members(members) ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @@ -620,10 +615,8 @@ async def websocket_device_cluster_attributes( endpoint_id, cluster_id, cluster_type ) if attributes is not None: - for attr_id in attributes: - cluster_attributes.append( - {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} - ) + for attr_id, attr in attributes.items(): + cluster_attributes.append({ID: attr_id, ATTR_NAME: attr[0]}) _LOGGER.debug( "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", ATTR_CLUSTER_ID, @@ -660,7 +653,7 @@ async def websocket_device_cluster_commands( cluster_id: int = msg[ATTR_CLUSTER_ID] cluster_type: str = msg[ATTR_CLUSTER_TYPE] zha_device = zha_gateway.get_device(ieee) - cluster_commands = [] + cluster_commands: list[dict[str, Any]] = [] commands = None if zha_device is not None: commands = zha_device.async_get_cluster_commands( @@ -668,20 +661,20 @@ async def websocket_device_cluster_commands( ) if commands is not None: - for cmd_id in commands[CLUSTER_COMMANDS_CLIENT]: + for cmd_id, cmd in commands[CLUSTER_COMMANDS_CLIENT].items(): cluster_commands.append( { TYPE: CLIENT, ID: cmd_id, - ATTR_NAME: commands[CLUSTER_COMMANDS_CLIENT][cmd_id][0], + ATTR_NAME: cmd[0], } ) - for cmd_id in commands[CLUSTER_COMMANDS_SERVER]: + for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items(): cluster_commands.append( { TYPE: CLUSTER_COMMAND_SERVER, ID: cmd_id, - ATTR_NAME: commands[CLUSTER_COMMANDS_SERVER][cmd_id][0], + ATTR_NAME: cmd[0], } ) _LOGGER.debug( From 3ef912b7a0c95765f3930d19dce8961b45c9b2e1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 18:09:15 +0100 Subject: [PATCH 0699/1054] Improve zha typing [api] (4) (#68649) --- homeassistant/components/zha/api.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index c1115d4cccb..da094363e0a 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -701,7 +701,7 @@ async def websocket_device_cluster_commands( vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, vol.Required(ATTR_ATTRIBUTE): int, - vol.Optional(ATTR_MANUFACTURER): object, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ) @websocket_api.async_response @@ -715,12 +715,13 @@ async def websocket_read_zigbee_cluster_attributes( cluster_id: int = msg[ATTR_CLUSTER_ID] cluster_type: str = msg[ATTR_CLUSTER_TYPE] attribute: int = msg[ATTR_ATTRIBUTE] - manufacturer: Any | None = msg.get(ATTR_MANUFACTURER) + manufacturer: int | None = msg.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) - if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: - manufacturer = zha_device.manufacturer_code - success = failure = None + success = {} + failure = {} if zha_device is not None: + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + manufacturer = zha_device.manufacturer_code cluster = zha_device.async_get_cluster( endpoint_id, cluster_id, cluster_type=cluster_type ) @@ -1075,10 +1076,10 @@ def async_load_api(hass: HomeAssistant) -> None: value: int | bool | str = service.data[ATTR_VALUE] manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) - if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: - manufacturer = zha_device.manufacturer_code response = None if zha_device is not None: + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + manufacturer = zha_device.manufacturer_code response = await zha_device.write_zigbee_attribute( endpoint_id, cluster_id, @@ -1124,10 +1125,10 @@ def async_load_api(hass: HomeAssistant) -> None: args: list = service.data[ATTR_ARGS] manufacturer: int | None = service.data.get(ATTR_MANUFACTURER) zha_device = zha_gateway.get_device(ieee) - if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: - manufacturer = zha_device.manufacturer_code response = None if zha_device is not None: + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + manufacturer = zha_device.manufacturer_code response = await zha_device.issue_cluster_command( endpoint_id, cluster_id, From 4b22f04505e4a18d96b1d4ec05fd8123c9f32f53 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Fri, 25 Mar 2022 18:09:49 +0100 Subject: [PATCH 0700/1054] Add typing to test files for Wallbox (#68635) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- tests/components/wallbox/__init__.py | 7 ++++--- tests/components/wallbox/test_config_flow.py | 10 +++++----- tests/components/wallbox/test_init.py | 10 +++++----- tests/components/wallbox/test_lock.py | 6 +++--- tests/components/wallbox/test_number.py | 5 +++-- tests/components/wallbox/test_sensor.py | 3 ++- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index fe9aa1ef3d6..8a31d2ebcd5 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -23,6 +23,7 @@ from homeassistant.components.wallbox.const import ( DOMAIN, ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from .const import CONF_ERROR, CONF_JWT, CONF_STATUS, CONF_TTL, CONF_USER_ID @@ -71,7 +72,7 @@ entry = MockConfigEntry( ) -async def setup_integration(hass): +async def setup_integration(hass: HomeAssistant) -> None: """Test wallbox sensor class setup.""" entry.add_to_hass(hass) @@ -99,7 +100,7 @@ async def setup_integration(hass): await hass.async_block_till_done() -async def setup_integration_connection_error(hass): +async def setup_integration_connection_error(hass: HomeAssistant) -> None: """Test wallbox sensor class setup with a connection error.""" with requests_mock.Mocker() as mock_request: @@ -125,7 +126,7 @@ async def setup_integration_connection_error(hass): await hass.async_block_till_done() -async def setup_integration_read_only(hass): +async def setup_integration_read_only(hass: HomeAssistant) -> None: """Test wallbox sensor class setup for read only.""" with requests_mock.Mocker() as mock_request: diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 01993d88968..93f3fe77e10 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -75,7 +75,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_form_cannot_authenticate(hass): +async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -105,7 +105,7 @@ async def test_form_cannot_authenticate(hass): assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): +async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -135,7 +135,7 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_validate_input(hass): +async def test_form_validate_input(hass: HomeAssistant) -> None: """Test we can validate input.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -165,7 +165,7 @@ async def test_form_validate_input(hass): assert result2["data"]["station"] == "12345" -async def test_form_reauth(hass): +async def test_form_reauth(hass: HomeAssistant) -> None: """Test we handle reauth flow.""" await setup_integration(hass) assert entry.state == config_entries.ConfigEntryState.LOADED @@ -205,7 +205,7 @@ async def test_form_reauth(hass): await hass.config_entries.async_unload(entry.entry_id) -async def test_form_reauth_invalid(hass): +async def test_form_reauth_invalid(hass: HomeAssistant) -> None: """Test we handle reauth invalid flow.""" await setup_integration(hass) assert entry.state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index 66f0701e42e..79e6871faca 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -37,7 +37,7 @@ authorisation_response = json.loads( ) -async def test_wallbox_setup_unload_entry(hass: HomeAssistant): +async def test_wallbox_setup_unload_entry(hass: HomeAssistant) -> None: """Test Wallbox Unload.""" await setup_integration(hass) @@ -47,7 +47,7 @@ async def test_wallbox_setup_unload_entry(hass: HomeAssistant): assert entry.state == ConfigEntryState.NOT_LOADED -async def test_wallbox_unload_entry_connection_error(hass: HomeAssistant): +async def test_wallbox_unload_entry_connection_error(hass: HomeAssistant) -> None: """Test Wallbox Unload Connection Error.""" await setup_integration_connection_error(hass) @@ -57,7 +57,7 @@ async def test_wallbox_unload_entry_connection_error(hass: HomeAssistant): assert entry.state == ConfigEntryState.NOT_LOADED -async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant): +async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant) -> None: """Test Wallbox setup with authentication error.""" await setup_integration(hass) @@ -83,7 +83,7 @@ async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant): assert entry.state == ConfigEntryState.NOT_LOADED -async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant): +async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant) -> None: """Test Wallbox setup with connection error.""" await setup_integration(hass) @@ -109,7 +109,7 @@ async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant): assert entry.state == ConfigEntryState.NOT_LOADED -async def test_wallbox_refresh_failed_read_only(hass: HomeAssistant): +async def test_wallbox_refresh_failed_read_only(hass: HomeAssistant) -> None: """Test Wallbox setup for read-only user.""" await setup_integration_read_only(hass) diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index 7e24f997825..4ea9132c675 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -36,7 +36,7 @@ authorisation_response = json.loads( ) -async def test_wallbox_lock_class(hass: HomeAssistant): +async def test_wallbox_lock_class(hass: HomeAssistant) -> None: """Test wallbox lock class.""" await setup_integration(hass) @@ -78,7 +78,7 @@ async def test_wallbox_lock_class(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -async def test_wallbox_lock_class_connection_error(hass: HomeAssistant): +async def test_wallbox_lock_class_connection_error(hass: HomeAssistant) -> None: """Test wallbox lock class connection error.""" await setup_integration(hass) @@ -117,7 +117,7 @@ async def test_wallbox_lock_class_connection_error(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -async def test_wallbox_lock_class_authentication_error(hass: HomeAssistant): +async def test_wallbox_lock_class_authentication_error(hass: HomeAssistant) -> None: """Test wallbox lock not loaded on authentication error.""" await setup_integration_read_only(hass) diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 989cb1b3c31..e247aa59ece 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -7,6 +7,7 @@ import requests_mock from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE from homeassistant.components.wallbox import CONF_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant from tests.components.wallbox import entry, setup_integration from tests.components.wallbox.const import ( @@ -31,7 +32,7 @@ authorisation_response = json.loads( ) -async def test_wallbox_number_class(hass): +async def test_wallbox_number_class(hass: HomeAssistant) -> None: """Test wallbox sensor class.""" await setup_integration(hass) @@ -60,7 +61,7 @@ async def test_wallbox_number_class(hass): await hass.config_entries.async_unload(entry.entry_id) -async def test_wallbox_number_class_connection_error(hass): +async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> None: """Test wallbox sensor class.""" await setup_integration(hass) diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index 2551eed6a2e..360040e1c2b 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -1,5 +1,6 @@ """Test Wallbox Switch component.""" from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, POWER_KILO_WATT +from homeassistant.core import HomeAssistant from tests.components.wallbox import entry, setup_integration from tests.components.wallbox.const import ( @@ -9,7 +10,7 @@ from tests.components.wallbox.const import ( ) -async def test_wallbox_sensor_class(hass): +async def test_wallbox_sensor_class(hass: HomeAssistant) -> None: """Test wallbox sensor class.""" await setup_integration(hass) From f5923be4e4ac0d9d48970732a68c005aa90bf8b8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 18:13:28 +0100 Subject: [PATCH 0701/1054] Improve zha typing [core.decorators] (#68650) --- .../components/zha/core/decorators.py | 23 ++++++++----------- .../components/zha/core/registries.py | 12 ++++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index a27e4cc0bfc..c57cad7d65e 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -2,37 +2,32 @@ from __future__ import annotations from collections.abc import Callable -from typing import TypeVar +from typing import Any, TypeVar, Union -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +_TypeT = TypeVar("_TypeT", bound=type[Any]) -class DictRegistry(dict): +class DictRegistry(dict[Union[int, str], _TypeT]): """Dict Registry of items.""" - def register( - self, name: int | str, item: str | CALLABLE_T = None - ) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: """Return decorator to register item with a specific name.""" - def decorator(channel: CALLABLE_T) -> CALLABLE_T: + def decorator(channel: _TypeT) -> _TypeT: """Register decorated channel or item.""" - if item is None: - self[name] = channel - else: - self[name] = item + self[name] = channel return channel return decorator -class SetRegistry(set): +class SetRegistry(set[Union[int, str]]): """Set Registry of items.""" - def register(self, name: int | str) -> Callable[[CALLABLE_T], CALLABLE_T]: + def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: """Return decorator to register item with a specific name.""" - def decorator(channel: CALLABLE_T) -> CALLABLE_T: + def decorator(channel: _TypeT) -> _TypeT: """Register decorated channel or item.""" self.add(name) return channel diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 1480469ce2c..1d3482cd8f4 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,6 +4,7 @@ from __future__ import annotations import collections from collections.abc import Callable import dataclasses +from typing import TYPE_CHECKING import attr from zigpy import zcl @@ -15,8 +16,11 @@ from homeassistant.const import Platform # importing channels updates registries from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import -from .decorators import CALLABLE_T, DictRegistry, SetRegistry -from .typing import ChannelType +from .decorators import DictRegistry, SetRegistry +from .typing import CALLABLE_T, ChannelType + +if TYPE_CHECKING: + from .channels.base import ClientChannel, ZigbeeChannel GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] @@ -98,8 +102,8 @@ DEVICE_CLASS = { } DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) -CLIENT_CHANNELS_REGISTRY = DictRegistry() -ZIGBEE_CHANNEL_REGISTRY = DictRegistry() +CLIENT_CHANNELS_REGISTRY: DictRegistry[type[ClientChannel]] = DictRegistry() +ZIGBEE_CHANNEL_REGISTRY: DictRegistry[type[ZigbeeChannel]] = DictRegistry() def set_or_callable(value): From 67cf053260b91e007c7183dbe94744887726f1ae Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 25 Mar 2022 20:30:28 +0100 Subject: [PATCH 0702/1054] Implement config flow for filesize (#67668) Co-authored-by: J. Nick Koston --- .coveragerc | 1 - .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/filesize/__init__.py | 35 ++++- .../components/filesize/config_flow.py | 80 ++++++++++ homeassistant/components/filesize/const.py | 8 + .../components/filesize/manifest.json | 5 +- homeassistant/components/filesize/sensor.py | 66 ++++---- .../components/filesize/services.yaml | 3 - .../components/filesize/strings.json | 18 +++ .../components/filesize/translations/en.json | 18 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ tests/components/filesize/__init__.py | 13 ++ tests/components/filesize/conftest.py | 45 ++++++ .../filesize/fixtures/configuration.yaml | 4 - tests/components/filesize/test_config_flow.py | 130 +++++++++++++++ tests/components/filesize/test_init.py | 110 +++++++++++++ tests/components/filesize/test_sensor.py | 148 ++++++------------ 19 files changed, 551 insertions(+), 148 deletions(-) create mode 100644 homeassistant/components/filesize/config_flow.py create mode 100644 homeassistant/components/filesize/const.py delete mode 100644 homeassistant/components/filesize/services.yaml create mode 100644 homeassistant/components/filesize/strings.json create mode 100644 homeassistant/components/filesize/translations/en.json create mode 100644 tests/components/filesize/conftest.py delete mode 100644 tests/components/filesize/fixtures/configuration.yaml create mode 100644 tests/components/filesize/test_config_flow.py create mode 100644 tests/components/filesize/test_init.py diff --git a/.coveragerc b/.coveragerc index 06d5265c9f2..f38836f3a65 100644 --- a/.coveragerc +++ b/.coveragerc @@ -336,7 +336,6 @@ omit = homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/* - homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py homeassistant/components/fireservicerota/__init__.py homeassistant/components/fireservicerota/binary_sensor.py diff --git a/.strict-typing b/.strict-typing index 6b5d1e5d751..d0d81ddc5c0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -82,6 +82,7 @@ homeassistant.components.esphome.* homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* homeassistant.components.fastdotcom.* +homeassistant.components.filesize.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* homeassistant.components.flux_led.* diff --git a/CODEOWNERS b/CODEOWNERS index bc801a5f8f0..97ffa74b143 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -308,6 +308,8 @@ tests/components/fan/* @home-assistant/core homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff tests/components/file/* @fabaff +homeassistant/components/filesize/* @gjohansson-ST +tests/components/filesize/* @gjohansson-ST homeassistant/components/filter/* @dgomes tests/components/filter/* @dgomes homeassistant/components/fireservicerota/* @cyberjunky diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 9e2ab2477a9..8f5b098d221 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -1,6 +1,35 @@ """The filesize component.""" +from __future__ import annotations -from homeassistant.const import Platform +import logging +import pathlib -DOMAIN = "filesize" -PLATFORMS = [Platform.SENSOR] +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + + path = entry.data[CONF_FILE_PATH] + get_path = await hass.async_add_executor_job(pathlib.Path, path) + + if not get_path.exists() and not get_path.is_file(): + raise ConfigEntryNotReady(f"Can not access file {path}") + + if not hass.config.is_allowed_path(path): + raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/filesize/config_flow.py b/homeassistant/components/filesize/config_flow.py new file mode 100644 index 00000000000..7838353fa12 --- /dev/null +++ b/homeassistant/components/filesize/config_flow.py @@ -0,0 +1,80 @@ +"""The filesize config flow.""" +from __future__ import annotations + +import logging +import pathlib +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_FILE_PATH): str}) + +_LOGGER = logging.getLogger(__name__) + + +def validate_path(hass: HomeAssistant, path: str) -> pathlib.Path: + """Validate path.""" + try: + get_path = pathlib.Path(path) + except OSError as error: + _LOGGER.error("Can not access file %s, error %s", path, error) + raise NotValidError from error + + if not hass.config.is_allowed_path(path): + _LOGGER.error("Filepath %s is not valid or allowed", path) + raise NotAllowedError + + return get_path + + +class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Filesize.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, Any] = {} + + if user_input is not None: + try: + get_path = validate_path(self.hass, user_input[CONF_FILE_PATH]) + except NotValidError: + errors["base"] = "not_valid" + except NotAllowedError: + errors["base"] = "not_allowed" + else: + fullpath = str(get_path.absolute()) + await self.async_set_unique_id(fullpath) + self._abort_if_unique_id_configured() + + name = str(user_input[CONF_FILE_PATH]).rsplit("/", maxsplit=1)[-1] + return self.async_create_entry( + title=name, + data={CONF_FILE_PATH: user_input[CONF_FILE_PATH]}, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_user(user_input) + + +class NotValidError(Exception): + """Path is not valid error.""" + + +class NotAllowedError(Exception): + """Path is not allowed error.""" diff --git a/homeassistant/components/filesize/const.py b/homeassistant/components/filesize/const.py new file mode 100644 index 00000000000..a47f1f99d38 --- /dev/null +++ b/homeassistant/components/filesize/const.py @@ -0,0 +1,8 @@ +"""The filesize constants.""" + +from homeassistant.const import Platform + +DOMAIN = "filesize" +PLATFORMS = [Platform.SENSOR] + +CONF_FILE_PATHS = "file_paths" diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index 1db5009b7e4..c84fd2f0cc1 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -2,6 +2,7 @@ "domain": "filesize", "name": "File Size", "documentation": "https://www.home-assistant.io/integrations/filesize", - "codeowners": [], - "iot_class": "local_polling" + "codeowners": ["@gjohansson-ST"], + "iot_class": "local_polling", + "config_flow": true } diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 56542a0aadd..b08b797aa4b 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -12,19 +12,17 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorEntity, ) -from homeassistant.const import DATA_MEGABYTES +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_FILE_PATH, DATA_MEGABYTES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, PLATFORMS +from .const import CONF_FILE_PATHS, DOMAIN _LOGGER = logging.getLogger(__name__) - -CONF_FILE_PATHS = "file_paths" ICON = "mdi:file" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( @@ -32,49 +30,53 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the file size sensor.""" - - setup_reload_service(hass, DOMAIN, PLATFORMS) - - sensors = [] - paths = set() + _LOGGER.warning( + # Filesize config flow added in 2022.4 and should be removed in 2022.8 + "Loading filesize via platform setup is deprecated; Please remove it from your configuration" + ) for path in config[CONF_FILE_PATHS]: - try: - fullpath = str(pathlib.Path(path).absolute()) - except OSError as error: - _LOGGER.error("Can not access file %s, error %s", path, error) - continue - - if fullpath in paths: - continue - paths.add(fullpath) - - if not hass.config.is_allowed_path(path): - _LOGGER.error("Filepath %s is not valid or allowed", path) - continue - - sensors.append(Filesize(fullpath)) - - if sensors: - add_entities(sensors, True) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_FILE_PATH: path}, + ) + ) -class Filesize(SensorEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform from config entry.""" + + path = entry.data[CONF_FILE_PATH] + get_path = await hass.async_add_executor_job(pathlib.Path, path) + fullpath = str(get_path.absolute()) + + if get_path.exists() and get_path.is_file(): + async_add_entities([FilesizeEntity(fullpath, entry.entry_id)], True) + + +class FilesizeEntity(SensorEntity): """Encapsulates file size information.""" _attr_native_unit_of_measurement = DATA_MEGABYTES _attr_icon = ICON - def __init__(self, path: str) -> None: + def __init__(self, path: str, entry_id: str) -> None: """Initialize the data object.""" self._path = path # Need to check its a valid path self._attr_name = path.split("/")[-1] + self._attr_unique_id = entry_id def update(self) -> None: """Update the sensor.""" diff --git a/homeassistant/components/filesize/services.yaml b/homeassistant/components/filesize/services.yaml deleted file mode 100644 index a794303f8f1..00000000000 --- a/homeassistant/components/filesize/services.yaml +++ /dev/null @@ -1,3 +0,0 @@ -reload: - name: Reload - description: Reload all filesize entities. diff --git a/homeassistant/components/filesize/strings.json b/homeassistant/components/filesize/strings.json new file mode 100644 index 00000000000..1096d7c888a --- /dev/null +++ b/homeassistant/components/filesize/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "file_path": "Path to file" + } + } + }, + "error": { + "not_valid": "Path is not valid", + "not_allowed": "Path is not allowed" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } + } \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/en.json b/homeassistant/components/filesize/translations/en.json new file mode 100644 index 00000000000..76f3b2bbb47 --- /dev/null +++ b/homeassistant/components/filesize/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "file_path": "Path to file" + } + } + }, + "error": { + "not_valid": "Path is not valid", + "not_allowed": "Path is not allowed" + }, + "abort": { + "already_configured": "Filepath is already configured" + } + } + } \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f16f69cbede..5a8ea33c7bd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -97,6 +97,7 @@ FLOWS = { "evil_genius_labs", "ezviz", "faa_delays", + "filesize", "fireservicerota", "fivem", "fjaraskupan", diff --git a/mypy.ini b/mypy.ini index 1f868ce05a9..344cb82b372 100644 --- a/mypy.ini +++ b/mypy.ini @@ -704,6 +704,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.filesize.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fitbit.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/filesize/__init__.py b/tests/components/filesize/__init__.py index 02876267482..d7aa8dd2481 100644 --- a/tests/components/filesize/__init__.py +++ b/tests/components/filesize/__init__.py @@ -1 +1,14 @@ """Tests for the filesize component.""" +import os + +TEST_DIR = os.path.join(os.path.dirname(__file__)) +TEST_FILE_NAME = "mock_file_test_filesize.txt" +TEST_FILE_NAME2 = "mock_file_test_filesize2.txt" +TEST_FILE = os.path.join(TEST_DIR, TEST_FILE_NAME) +TEST_FILE2 = os.path.join(TEST_DIR, TEST_FILE_NAME2) + + +def create_file(path) -> None: + """Create a test file.""" + with open(path, "w", encoding="utf-8") as test_file: + test_file.write("test") diff --git a/tests/components/filesize/conftest.py b/tests/components/filesize/conftest.py new file mode 100644 index 00000000000..6584ebc95df --- /dev/null +++ b/tests/components/filesize/conftest.py @@ -0,0 +1,45 @@ +"""Fixtures for Filesize integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +import os +from unittest.mock import patch + +import pytest + +from homeassistant.components.filesize.const import DOMAIN +from homeassistant.const import CONF_FILE_PATH + +from . import TEST_FILE, TEST_FILE2, TEST_FILE_NAME + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title=TEST_FILE_NAME, + domain=DOMAIN, + data={CONF_FILE_PATH: TEST_FILE}, + unique_id=TEST_FILE, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.filesize.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture(autouse=True) +def remove_file() -> None: + """Remove test file.""" + yield + if os.path.isfile(TEST_FILE): + os.remove(TEST_FILE) + if os.path.isfile(TEST_FILE2): + os.remove(TEST_FILE2) diff --git a/tests/components/filesize/fixtures/configuration.yaml b/tests/components/filesize/fixtures/configuration.yaml deleted file mode 100644 index ea73be72b80..00000000000 --- a/tests/components/filesize/fixtures/configuration.yaml +++ /dev/null @@ -1,4 +0,0 @@ -sensor: - - platform: filesize - file_paths: - - "/dev/null" diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py new file mode 100644 index 00000000000..0209444ec42 --- /dev/null +++ b/tests/components/filesize/test_config_flow.py @@ -0,0 +1,130 @@ +"""Tests for the Filesize config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.filesize.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, create_file + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {TEST_DIR} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_FILE_PATH: TEST_FILE}, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == TEST_FILE_NAME + assert result2.get("data") == {CONF_FILE_PATH: TEST_FILE} + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_unique_path( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we abort if already setup.""" + hass.config.allowlist_external_dirs = {TEST_DIR} + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data={CONF_FILE_PATH: TEST_FILE} + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_import_flow(hass: HomeAssistant) -> None: + """Test the import configuration flow.""" + create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {TEST_DIR} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_FILE_PATH: TEST_FILE}, + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == TEST_FILE_NAME + assert result.get("data") == {CONF_FILE_PATH: TEST_FILE} + + +async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: + """Test config flow errors.""" + create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "homeassistant.components.filesize.config_flow.pathlib.Path", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) + + assert result2["errors"] == {"base": "not_valid"} + + with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( + "homeassistant.components.filesize.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) + + assert result2["errors"] == {"base": "not_allowed"} + + hass.config.allowlist_external_dirs = {TEST_DIR} + with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( + "homeassistant.components.filesize.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_FILE_NAME + assert result2["data"] == { + CONF_FILE_PATH: TEST_FILE, + } diff --git a/tests/components/filesize/test_init.py b/tests/components/filesize/test_init.py new file mode 100644 index 00000000000..fecd3033c91 --- /dev/null +++ b/tests/components/filesize/test_init.py @@ -0,0 +1,110 @@ +"""Tests for the Filesize integration.""" +from unittest.mock import AsyncMock + +from homeassistant.components.filesize.const import CONF_FILE_PATHS, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_FILE_PATH +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + TEST_DIR, + TEST_FILE, + TEST_FILE2, + TEST_FILE_NAME, + TEST_FILE_NAME2, + create_file, +) + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, tmpdir: str +) -> None: + """Test the Filesize configuration entry loading/unloading.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_cannot_access_file( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, tmpdir: str +) -> None: + """Test that an file not exist is caught.""" + mock_config_entry.add_to_hass(hass) + testfile = f"{tmpdir}/file_not_exist.txt" + hass.config.allowlist_external_dirs = {tmpdir} + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_not_valid_path_to_file( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, tmpdir: str +) -> None: + """Test that an invalid path is caught.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_import_config( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test Filesize being set up from config via import.""" + create_file(TEST_FILE) + create_file(TEST_FILE2) + hass.config.allowlist_external_dirs = {TEST_DIR} + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + { + SENSOR_DOMAIN: { + "platform": DOMAIN, + CONF_FILE_PATHS: [TEST_FILE, TEST_FILE2], + } + }, + ) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 2 + + entry = config_entries[0] + assert entry.title == TEST_FILE_NAME + assert entry.unique_id == TEST_FILE + assert entry.data == {CONF_FILE_PATH: TEST_FILE} + entry2 = config_entries[1] + assert entry2.title == TEST_FILE_NAME2 + assert entry2.unique_id == TEST_FILE2 + assert entry2.data == {CONF_FILE_PATH: TEST_FILE2} diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index fa85ce41437..74a6f056783 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,130 +1,72 @@ """The tests for the filesize sensor.""" import os -from unittest.mock import patch -import pytest - -from homeassistant import config as hass_config -from homeassistant.components.filesize import DOMAIN -from homeassistant.components.filesize.sensor import CONF_FILE_PATHS -from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.const import CONF_FILE_PATH, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.setup import async_setup_component -from tests.common import get_fixture_path +from . import TEST_FILE, TEST_FILE_NAME, create_file -TEST_DIR = os.path.join(os.path.dirname(__file__)) -TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") +from tests.common import MockConfigEntry -def create_file(path) -> None: - """Create a test file.""" - with open(path, "w") as test_file: - test_file.write("test") - - -@pytest.fixture(autouse=True) -def remove_file() -> None: - """Remove test file.""" - yield - if os.path.isfile(TEST_FILE): - os.remove(TEST_FILE) - - -async def test_invalid_path(hass: HomeAssistant) -> None: +async def test_invalid_path( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test that an invalid path is caught.""" - config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: ["invalid_path"]}} - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("sensor")) == 0 + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=TEST_FILE, data={CONF_FILE_PATH: TEST_FILE} + ) + + state = hass.states.get("sensor." + TEST_FILE_NAME) + assert not state -async def test_cannot_access_file(hass: HomeAssistant) -> None: - """Test that an invalid path is caught.""" - config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} - - with patch( - "homeassistant.components.filesize.sensor.pathlib", - side_effect=OSError("Can not access"), - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids("sensor")) == 0 - - -async def test_valid_path(hass: HomeAssistant) -> None: +async def test_valid_path( + hass: HomeAssistant, tmpdir: str, mock_config_entry: MockConfigEntry +) -> None: """Test for a valid path.""" - create_file(TEST_FILE) - config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} - hass.config.allowlist_external_dirs = {TEST_DIR} - assert await async_setup_component(hass, "sensor", config) + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("sensor")) == 1 - state = hass.states.get("sensor.mock_file_test_filesize_txt") + + state = hass.states.get("sensor.file_txt") + assert state assert state.state == "0.0" assert state.attributes.get("bytes") == 4 + await hass.async_add_executor_job(os.remove, testfile) -async def test_state_unknown(hass: HomeAssistant, tmpdir: str) -> None: + +async def test_state_unknown( + hass: HomeAssistant, tmpdir: str, mock_config_entry: MockConfigEntry +) -> None: """Verify we handle state unavailable.""" - create_file(TEST_FILE) testfile = f"{tmpdir}/file" - await hass.async_add_executor_job(create_file, testfile) - with patch.object(hass.config, "is_allowed_path", return_value=True): - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "filesize", - "file_paths": [testfile], - } - }, - ) - await hass.async_block_till_done() + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} + ) - assert hass.states.get("sensor.file") + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.file") + assert state + assert state.state == "0.0" await hass.async_add_executor_job(os.remove, testfile) await async_update_entity(hass, "sensor.file") state = hass.states.get("sensor.file") assert state.state == STATE_UNKNOWN - - -async def test_reload(hass: HomeAssistant, tmpdir: str) -> None: - """Verify we can reload filesize sensors.""" - testfile = f"{tmpdir}/file" - await hass.async_add_executor_job(create_file, testfile) - with patch.object(hass.config, "is_allowed_path", return_value=True): - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "filesize", - "file_paths": [testfile], - } - }, - ) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - - assert hass.states.get("sensor.file") - - yaml_path = get_fixture_path("configuration.yaml", "filesize") - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch.object( - hass.config, "is_allowed_path", return_value=True - ): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert hass.states.get("sensor.file") is None From faf1f229e1117090df69c0af526c1320f2e495c3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 25 Mar 2022 21:10:04 +0100 Subject: [PATCH 0703/1054] Motion allow changing ip (#68589) Co-authored-by: J. Nick Koston --- .../components/motion_blinds/config_flow.py | 8 +++- .../components/motion_blinds/strings.json | 2 +- .../motion_blinds/translations/en.json | 2 +- .../motion_blinds/test_config_flow.py | 47 +++++++++++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 90ee92ccff3..d93a1abd865 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -138,7 +138,13 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address = motion_gateway.mac await self.async_set_unique_id(mac_address) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self._host, + CONF_API_KEY: key, + CONF_INTERFACE: multicast_interface, + } + ) return self.async_create_entry( title=DEFAULT_GATEWAY_NAME, diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index e5c86c2a45e..627ae72ffe3 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -29,7 +29,7 @@ "invalid_interface": "Invalid network interface" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%], connection settings are updated", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "[%key:common::config_flow::error::cannot_connect%]" } diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 8f0d21addce..5e111278a8d 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured", + "already_configured": "Device is already configured, connection settings are updated", "already_in_progress": "Configuration flow is already in progress", "connection_error": "Failed to connect" }, diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index b5e2f8fb717..77f5e242974 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -14,7 +14,9 @@ from tests.common import MockConfigEntry TEST_HOST = "1.2.3.4" TEST_HOST2 = "5.6.7.8" TEST_HOST_HA = "9.10.11.12" +TEST_HOST_ANY = "any" TEST_API_KEY = "12ab345c-d67e-8f" +TEST_API_KEY2 = "f8e76dc5-43ba-21" TEST_MAC = "ab:cd:ef:gh" TEST_MAC2 = "ij:kl:mn:op" TEST_DEVICE_LIST = {TEST_MAC: Mock()} @@ -76,6 +78,9 @@ def motion_blinds_connect_fixture(mock_get_source_ip): ), patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", TEST_DEVICE_LIST, + ), patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.mac", + TEST_MAC, ), patch( "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", return_value=TEST_DISCOVERY_1, @@ -362,3 +367,45 @@ async def test_options_flow(hass): assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } + + +async def test_change_connection_settings(hass): + """Test changing connection settings by issuing a second user config flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=TEST_MAC, + data={ + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, + }, + title=DEFAULT_GATEWAY_NAME, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + const.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"], + {CONF_HOST: TEST_HOST2}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY2, const.CONF_INTERFACE: TEST_HOST_ANY}, + ) + + assert result["type"] == "abort" + assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_API_KEY] == TEST_API_KEY2 + assert config_entry.data[const.CONF_INTERFACE] == TEST_HOST_ANY From 4dc8aff3d564145d8d28492e2c3510e3f238cb45 Mon Sep 17 00:00:00 2001 From: Mike Fugate Date: Fri, 25 Mar 2022 16:47:28 -0400 Subject: [PATCH 0704/1054] Bump asyncsleepiq to 1.2.1 (#68680) --- .../components/sleepiq/manifest.json | 2 +- homeassistant/components/sleepiq/select.py | 25 +++---------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sleepiq/test_select.py | 6 ++--- 5 files changed, 8 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index bb386f1cf42..16881506cb8 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -3,7 +3,7 @@ "name": "SleepIQ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": ["asyncsleepiq==1.2.0"], + "requirements": ["asyncsleepiq==1.2.1"], "codeowners": ["@mfugate1", "@kbickar"], "dhcp": [ { diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py index 2bf305007ca..711f228d7f2 100644 --- a/homeassistant/components/sleepiq/select.py +++ b/homeassistant/components/sleepiq/select.py @@ -1,16 +1,7 @@ """Support for SleepIQ foundation preset selection.""" from __future__ import annotations -from asyncsleepiq import ( - FAVORITE, - FLAT, - READ, - SNORE, - WATCH_TV, - ZERO_G, - SleepIQBed, - SleepIQPreset, -) +from asyncsleepiq import BED_PRESETS, SleepIQBed, SleepIQPreset from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -22,16 +13,6 @@ from .const import DOMAIN from .coordinator import SleepIQData from .entity import SleepIQBedEntity -FOUNDATION_PRESET_NAMES = { - "Not at preset": 0, - "Favorite": FAVORITE, - "Read": READ, - "Watch TV": WATCH_TV, - "Flat": FLAT, - "Zero G": ZERO_G, - "Snore": SNORE, -} - async def async_setup_entry( hass: HomeAssistant, @@ -50,7 +31,7 @@ async def async_setup_entry( class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): """Representation of a SleepIQ select entity.""" - _attr_options = list(FOUNDATION_PRESET_NAMES.keys()) + _attr_options = list(BED_PRESETS) def __init__( self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, preset: SleepIQPreset @@ -77,6 +58,6 @@ class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the current preset.""" - await self.preset.set_preset(FOUNDATION_PRESET_NAMES[option]) + await self.preset.set_preset(option) self._attr_current_option = option self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index fc4522c6e5c..c0e2336941f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -335,7 +335,7 @@ async-upnp-client==0.27.0 asyncpysupla==0.0.5 # homeassistant.components.sleepiq -asyncsleepiq==1.2.0 +asyncsleepiq==1.2.1 # homeassistant.components.aten_pe atenpdu==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f493a5fe07..b78f5c70383 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ arcam-fmj==0.12.0 async-upnp-client==0.27.0 # homeassistant.components.sleepiq -asyncsleepiq==1.2.0 +asyncsleepiq==1.2.1 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/sleepiq/test_select.py b/tests/components/sleepiq/test_select.py index 1b0c34d167c..855ca518f2d 100644 --- a/tests/components/sleepiq/test_select.py +++ b/tests/components/sleepiq/test_select.py @@ -1,8 +1,6 @@ """Tests for the SleepIQ select platform.""" from unittest.mock import MagicMock -from asyncsleepiq import ZERO_G - from homeassistant.components.select import DOMAIN, SERVICE_SELECT_OPTION from homeassistant.const import ( ATTR_ENTITY_ID, @@ -75,7 +73,7 @@ async def test_split_foundation_preset( mock_asyncsleepiq.beds[BED_ID].foundation.presets[0].set_preset.assert_called_once() mock_asyncsleepiq.beds[BED_ID].foundation.presets[0].set_preset.assert_called_with( - ZERO_G + "Zero G" ) @@ -116,4 +114,4 @@ async def test_single_foundation_preset( ].set_preset.assert_called_once() mock_asyncsleepiq_single_foundation.beds[BED_ID].foundation.presets[ 0 - ].set_preset.assert_called_with(ZERO_G) + ].set_preset.assert_called_with("Zero G") From 225f7a989b95f786454e001f8f307df5be107da3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 25 Mar 2022 12:03:46 -1000 Subject: [PATCH 0705/1054] Add strict typing for recorder util (#68681) --- .strict-typing | 1 + homeassistant/components/recorder/util.py | 54 ++++++++++++++--------- mypy.ini | 11 +++++ 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/.strict-typing b/.strict-typing index d0d81ddc5c0..866e8bfd95c 100644 --- a/.strict-typing +++ b/.strict-typing @@ -168,6 +168,7 @@ homeassistant.components.recorder.history homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics +homeassistant.components.recorder.util homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.ridwell.* diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 1f9d8bfaa26..487b8dd22f7 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections.abc import Callable, Generator from contextlib import contextmanager -from datetime import timedelta +from datetime import datetime, timedelta import functools import logging import os import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from awesomeversion import ( AwesomeVersion, @@ -16,6 +16,7 @@ from awesomeversion import ( AwesomeVersionStrategy, ) from sqlalchemy import text +from sqlalchemy.engine.cursor import CursorFetchStrategy from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session @@ -95,7 +96,7 @@ def session_scope( session.close() -def commit(session, work): +def commit(session: Session, work: Any) -> bool: """Commit & retry work: Either a model or in a function.""" for _ in range(0, RETRIES): try: @@ -175,12 +176,12 @@ def validate_or_move_away_sqlite_database(dburl: str) -> bool: return True -def dburl_to_path(dburl): +def dburl_to_path(dburl: str) -> str: """Convert the db url into a filesystem path.""" return dburl[len(SQLITE_URL_PREFIX) :] -def last_run_was_recently_clean(cursor): +def last_run_was_recently_clean(cursor: CursorFetchStrategy) -> bool: """Verify the last recorder run was recently clean.""" cursor.execute("SELECT end FROM recorder_runs ORDER BY start DESC LIMIT 1;") @@ -190,6 +191,7 @@ def last_run_was_recently_clean(cursor): return False last_run_end_time = process_timestamp(dt_util.parse_datetime(end_time[0])) + assert last_run_end_time is not None now = dt_util.utcnow() _LOGGER.debug("The last run ended at: %s (now: %s)", last_run_end_time, now) @@ -200,7 +202,7 @@ def last_run_was_recently_clean(cursor): return True -def basic_sanity_check(cursor): +def basic_sanity_check(cursor: CursorFetchStrategy) -> bool: """Check tables to make sure select does not fail.""" for table in ALL_TABLES: @@ -235,7 +237,7 @@ def validate_sqlite_database(dbpath: str) -> bool: return True -def run_checks_on_open_db(dbpath, cursor): +def run_checks_on_open_db(dbpath: str, cursor: CursorFetchStrategy) -> None: """Run checks that will generate a sqlite3 exception if there is corruption.""" sanity_check_passed = basic_sanity_check(cursor) last_run_was_clean = last_run_was_recently_clean(cursor) @@ -278,14 +280,14 @@ def move_away_broken_database(dbfile: str) -> None: os.rename(path, f"{path}{corrupt_postfix}") -def execute_on_connection(dbapi_connection, statement): +def execute_on_connection(dbapi_connection: Any, statement: str) -> None: """Execute a single statement with a dbapi connection.""" cursor = dbapi_connection.cursor() cursor.execute(statement) cursor.close() -def query_on_connection(dbapi_connection, statement): +def query_on_connection(dbapi_connection: Any, statement: str) -> Any: """Execute a single statement with a dbapi connection and return the result.""" cursor = dbapi_connection.cursor() cursor.execute(statement) @@ -294,30 +296,34 @@ def query_on_connection(dbapi_connection, statement): return result -def _warn_unsupported_dialect(dialect): +def _warn_unsupported_dialect(dialect_name: str) -> None: """Warn about unsupported database version.""" _LOGGER.warning( "Database %s is not supported; Home Assistant supports %s. " "Starting with Home Assistant 2022.2 this will prevent the recorder from " "starting. Please migrate your database to a supported software before then", - dialect, + dialect_name, "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", ) -def _warn_unsupported_version(server_version, dialect, minimum_version): +def _warn_unsupported_version( + server_version: str, dialect_name: str, minimum_version: str +) -> None: """Warn about unsupported database version.""" _LOGGER.warning( "Version %s of %s is not supported; minimum supported version is %s. " "Starting with Home Assistant 2022.2 this will prevent the recorder from " "starting. Please upgrade your database software before then", server_version, - dialect, + dialect_name, minimum_version, ) -def _extract_version_from_server_response(server_response): +def _extract_version_from_server_response( + server_response: str, +) -> AwesomeVersion | None: """Attempt to extract version from server response.""" try: return AwesomeVersion( @@ -330,8 +336,11 @@ def _extract_version_from_server_response(server_response): def setup_connection_for_dialect( - instance, dialect_name, dbapi_connection, first_connection -): + instance: Recorder, + dialect_name: str, + dbapi_connection: Any, + first_connection: bool, +) -> None: """Execute statements needed for dialect connection.""" # Returns False if the the connection needs to be setup # on the next connection, returns True if the connection @@ -406,7 +415,7 @@ def setup_connection_for_dialect( _warn_unsupported_dialect(dialect_name) -def end_incomplete_runs(session, start_time): +def end_incomplete_runs(session: Session, start_time: datetime) -> None: """End any incomplete recorder runs.""" for run in session.query(RecorderRuns).filter_by(end=None): run.closed_incorrect = True @@ -423,9 +432,9 @@ def retryable_database_job(description: str) -> Callable: The job should return True if it finished, and False if it needs to be rescheduled. """ - def decorator(job: Callable) -> Callable: + def decorator(job: Callable[[Any], bool]) -> Callable: @functools.wraps(job) - def wrapper(instance: Recorder, *args, **kwargs): + def wrapper(instance: Recorder, *args: Any, **kwargs: Any) -> bool: try: return job(instance, *args, **kwargs) except OperationalError as err: @@ -451,7 +460,7 @@ def retryable_database_job(description: str) -> Callable: return decorator -def perodic_db_cleanups(instance: Recorder): +def perodic_db_cleanups(instance: Recorder) -> None: """Run any database cleanups that need to happen perodiclly. These cleanups will happen nightly or after any purge. @@ -465,7 +474,7 @@ def perodic_db_cleanups(instance: Recorder): @contextmanager -def write_lock_db_sqlite(instance: Recorder): +def write_lock_db_sqlite(instance: Recorder) -> Generator[None, None, None]: """Lock database for writes.""" assert instance.engine is not None with instance.engine.connect() as connection: @@ -490,4 +499,5 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: """ if DATA_INSTANCE not in hass.data: return False - return hass.data[DATA_INSTANCE].migration_in_progress + instance: Recorder = hass.data[DATA_INSTANCE] + return instance.migration_in_progress diff --git a/mypy.ini b/mypy.ini index 344cb82b372..98a765d407c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1650,6 +1650,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.util] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.remote.*] check_untyped_defs = true disallow_incomplete_defs = true From 53245c65238e3009dd1f3412f7f9bef10385f64e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 Mar 2022 23:14:48 +0100 Subject: [PATCH 0706/1054] Update pylint to 2.13.0 (#68656) --- .../components/alexa/capabilities.py | 46 ++++++++----------- homeassistant/components/alexa/resources.py | 8 ++-- .../components/broadlink/config_flow.py | 8 +++- .../components/color_extractor/__init__.py | 2 +- .../components/harmony/subscriber.py | 1 - .../components/horizon/media_player.py | 8 ++-- homeassistant/components/http/__init__.py | 4 +- .../components/icloud/config_flow.py | 4 +- homeassistant/components/recorder/models.py | 2 - .../components/solaredge_local/sensor.py | 1 - homeassistant/components/sonos/__init__.py | 1 + homeassistant/components/tuya/__init__.py | 4 ++ homeassistant/components/zha/core/gateway.py | 2 + homeassistant/components/zha/sensor.py | 5 +- homeassistant/config_entries.py | 2 +- homeassistant/core.py | 2 +- .../helpers/helper_config_entry_flow.py | 1 - pyproject.toml | 1 - requirements_test.txt | 2 +- tests/pylint/test_enforce_type_hints.py | 4 ++ 20 files changed, 53 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 327d5973892..db122e23f13 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -75,6 +75,8 @@ class AlexaCapability: https://developer.amazon.com/docs/device-apis/message-guide.html """ + # pylint: disable=no-self-use + supported_locales = {"en-US"} def __init__(self, entity: State, instance: str | None = None) -> None: @@ -86,28 +88,23 @@ class AlexaCapability: """Return the Alexa API name of this interface.""" raise NotImplementedError - @staticmethod - def properties_supported() -> list[dict]: + def properties_supported(self) -> list[dict]: """Return what properties this entity supports.""" return [] - @staticmethod - def properties_proactively_reported() -> bool: + def properties_proactively_reported(self) -> bool: """Return True if properties asynchronously reported.""" return False - @staticmethod - def properties_retrievable() -> bool: + def properties_retrievable(self) -> bool: """Return True if properties can be retrieved.""" return False - @staticmethod - def properties_non_controllable() -> bool | None: + def properties_non_controllable(self) -> bool | None: """Return True if non controllable.""" return None - @staticmethod - def get_property(name): + def get_property(self, name): """Read and return a property. Return value should be a dict, or raise UnsupportedProperty. @@ -117,13 +114,11 @@ class AlexaCapability: """ raise UnsupportedProperty(name) - @staticmethod - def supports_deactivation(): + def supports_deactivation(self): """Applicable only to scenes.""" return None - @staticmethod - def capability_proactively_reported(): + def capability_proactively_reported(self): """Return True if the capability is proactively reported. Set properties_proactively_reported() for proactively reported properties. @@ -131,16 +126,14 @@ class AlexaCapability: """ return None - @staticmethod - def capability_resources(): + def capability_resources(self): """Return the capability object. Applicable to ToggleController, RangeController, and ModeController interfaces. """ return [] - @staticmethod - def configuration(): + def configuration(self): """Return the configuration object. Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, @@ -148,8 +141,7 @@ class AlexaCapability: """ return [] - @staticmethod - def configurations(): + def configurations(self): """Return the configurations object. The plural configurations object is different that the singular configuration object. @@ -157,31 +149,29 @@ class AlexaCapability: """ return [] - @staticmethod - def inputs(): + def inputs(self): """Applicable only to media players.""" return [] - @staticmethod - def semantics(): + def semantics(self): """Return the semantics object. Applicable to ToggleController, RangeController, and ModeController interfaces. """ return [] - @staticmethod - def supported_operations(): + def supported_operations(self): """Return the supportedOperations object.""" return [] - @staticmethod - def camera_stream_configurations(): + def camera_stream_configurations(self): """Applicable only to CameraStreamController.""" return None def serialize_discovery(self): """Serialize according to the Discovery API.""" + # pylint: disable=assignment-from-none + # Methods may be overridden and return a value. result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} if (instance := self.instance) is not None: diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 5c02eca4fb2..aec43f41f9a 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -200,6 +200,8 @@ class AlexaCapabilityResource: https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources """ + # pylint: disable=no-self-use + def __init__(self, labels): """Initialize an Alexa resource.""" self._resource_labels = [] @@ -210,13 +212,11 @@ class AlexaCapabilityResource: """Return capabilityResources object serialized for an API response.""" return self.serialize_labels(self._resource_labels) - @staticmethod - def serialize_configuration(): + def serialize_configuration(self): """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" return [] - @staticmethod - def serialize_labels(resources): + def serialize_labels(self, resources): """Return resource label objects for friendlyNames serialized for an API response.""" labels = [] for label in resources: diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 8a32ba02ee8..da8b489a98b 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -188,7 +188,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device.mac.hex()) _LOGGER.error( - "Failed to authenticate to the device at %s: %s", device.host[0], err_msg + "Failed to authenticate to the device at %s: %s", + device.host[0], + err_msg, # pylint: disable=used-before-assignment ) return self.async_show_form(step_id="auth", errors=errors) @@ -251,7 +253,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish() _LOGGER.error( - "Failed to unlock the device at %s: %s", device.host[0], err_msg + "Failed to unlock the device at %s: %s", + device.host[0], + err_msg, # pylint: disable=used-before-assignment ) else: diff --git a/homeassistant/components/color_extractor/__init__.py b/homeassistant/components/color_extractor/__init__.py index cbc5b0c8821..b0884643be9 100644 --- a/homeassistant/components/color_extractor/__init__.py +++ b/homeassistant/components/color_extractor/__init__.py @@ -80,7 +80,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: except UnidentifiedImageError as ex: _LOGGER.error( "Bad image from %s '%s' provided, are you sure it's an image? %s", - image_type, + image_type, # pylint: disable=used-before-assignment image_reference, ex, ) diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index f71b486fd16..2731d8555f0 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -3,7 +3,6 @@ import asyncio import logging -# pylint: disable-next=deprecated-typing-alias # Issue with Python 3.9.0 and 3.9.1 with collections.abc.Callable # https://bugs.python.org/issue42965 from typing import Any, Callable, NamedTuple, Optional diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 31e905b7864..fd6b7b3d3cc 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -202,12 +202,12 @@ class HorizonDevice(MediaPlayerEntity): try: self._client.connect() self._client.authorize() - except AuthenticationError as msg: - _LOGGER.error("Authentication to %s failed: %s", self._name, msg) + except AuthenticationError as msg2: + _LOGGER.error("Authentication to %s failed: %s", self._name, msg2) return - except OSError as msg: + except OSError as msg2: # occurs if horizon box is offline - _LOGGER.error("Reconnect to %s failed: %s", self._name, msg) + _LOGGER.error("Reconnect to %s failed: %s", self._name, msg2) return self._send(key=key, channel=channel) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a41329a1548..374e69975ce 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -365,10 +365,10 @@ class HomeAssistantHTTP: ) try: context = self._create_emergency_ssl_context() - except OSError as error: + except OSError as error2: _LOGGER.error( "Could not create an emergency self signed ssl certificate: %s", - error, + error2, ) context = None else: diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 9bfa12a76a2..3eb6ced782c 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -288,8 +288,8 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._with_family, ) return await self.async_step_verification_code(None, errors) - except PyiCloudFailedLoginException as error: - _LOGGER.error("Error logging into iCloud service: %s", error) + except PyiCloudFailedLoginException as error_login: + _LOGGER.error("Error logging into iCloud service: %s", error_login) self.api = None errors = {CONF_PASSWORD: "invalid_auth"} return self._show_setup_form(user_input, errors, "user") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 660b6355911..71ac332d34e 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -528,8 +528,6 @@ class LazyState(State): __slots__ = [ "_row", - "entity_id", - "state", "_attributes", "_last_changed", "_last_updated", diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 368121e79e9..d07a95683eb 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -209,7 +209,6 @@ def setup_platform( _LOGGER.debug("Credentials correct and site is active") except AttributeError: _LOGGER.error("Missing details data in solaredge status") - _LOGGER.debug("Status is: %s", status) return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 27d51d8f3e6..bb4c61fdadf 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -188,6 +188,7 @@ class SonosDiscoveryManager: _ = soco.volume return soco except NotSupportedException as exc: + # pylint: disable-next=used-before-assignment _LOGGER.debug("Device %s is not supported, ignoring: %s", uid, exc) self.data.discovery_ignored.add(ip_address) except (OSError, SoCoException) as ex: diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index ad6c8142828..97422179960 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -234,6 +234,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DeviceListener(TuyaDeviceListener): """Device Update Listener.""" + # pylint: disable=arguments-differ + # Library incorrectly defines methods as 'classmethod' + # https://github.com/tuya/tuya-iot-python-sdk/pull/48 + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 6c600bf93d6..18213c396cc 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -98,6 +98,8 @@ if TYPE_CHECKING: from ..entity import ZhaEntity from .store import ZhaStorage + # pylint: disable-next=broken-collections-callable + # Safe inside TYPE_CHECKING block _LogFilterType = Union[Filter, Callable[[LogRecord], int]] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 1e7d4f28a38..302bbc2a054 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -231,7 +231,7 @@ class Battery(Sensor): return cls(unique_id, zha_device, channels, **kwargs) @staticmethod - def formatter(value: int) -> int: + def formatter(value: int) -> int: # pylint: disable=arguments-differ """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1: @@ -391,8 +391,7 @@ class Illuminance(Sensor): _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _unit = LIGHT_LUX - @staticmethod - def formatter(value: int) -> float: + def formatter(self, value: int) -> float: """Convert illumination data.""" return round(pow(10, ((value - 1) / 10000)), 1) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7bc37bcd305..0e94aa458a3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1187,7 +1187,7 @@ async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]: class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" - def __init_subclass__(cls, domain: str | None = None, **kwargs: Any) -> None: + def __init_subclass__(cls, *, domain: str | None = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) if domain is not None: diff --git a/homeassistant/core.py b/homeassistant/core.py index 11d90d6d05b..733281aa5e6 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -101,7 +101,7 @@ block_async_io.enable() _T = TypeVar("_T") _R = TypeVar("_R") -_R_co = TypeVar("_R_co", covariant=True) # pylint: disable=invalid-name +_R_co = TypeVar("_R_co", covariant=True) # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index ba62d5d59e5..16471c13e41 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -181,7 +181,6 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): VERSION = 1 - # pylint: disable-next=arguments-differ def __init_subclass__(cls, **kwargs: Any) -> None: """Initialize a subclass.""" super().__init_subclass__(**kwargs) diff --git a/pyproject.toml b/pyproject.toml index c1a08a3f0c3..fbe9fa4b6e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ good-names = [ "j", "k", "Run", - "T", "ip", ] diff --git a/requirements_test.txt b/requirements_test.txt index a78f7d57a7b..34a4936b3d8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.942 pre-commit==2.17.0 -pylint==2.12.2 +pylint==2.13.0 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 81fdd2fa916..64be85c9a44 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -137,6 +137,10 @@ def test_invalid_discovery_info( msg_id="hass-argument-type", node=discovery_info_node, args=(4, "DiscoveryInfoType | None"), + line=6, + col_offset=4, + end_line=6, + end_col_offset=41, ), ): type_hint_checker.visit_asyncfunctiondef(func_node) From d645e80ccd5acb92c5ee6bce30c20bc634fc3e77 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Mar 2022 23:22:58 +0100 Subject: [PATCH 0707/1054] Clean up async_update_entity helper usage (#68641) --- .../components/homeassistant/__init__.py | 4 +- .../components/androidtv/test_media_player.py | 41 ++++++++++--------- tests/components/awair/test_sensor.py | 5 +-- tests/components/broadlink/test_sensors.py | 17 +++----- .../canary/test_alarm_control_panel.py | 9 ++-- tests/components/canary/test_sensor.py | 5 ++- tests/components/dexcom/test_sensor.py | 17 +++----- tests/components/freedompro/test_cover.py | 3 +- tests/components/freedompro/test_fan.py | 5 ++- tests/components/freedompro/test_lock.py | 3 +- tests/components/freedompro/test_switch.py | 3 +- tests/components/history_stats/test_sensor.py | 5 ++- tests/components/homeassistant/test_init.py | 2 +- tests/components/konnected/test_panel.py | 9 ++-- tests/components/kulersky/test_light.py | 3 +- tests/components/nzbget/test_switch.py | 3 +- .../components/plugwise/test_binary_sensor.py | 5 +-- tests/components/plugwise/test_sensor.py | 5 +-- .../ruckus_unleashed/test_device_tracker.py | 7 ++-- .../components/template/test_binary_sensor.py | 13 +++--- tests/components/template/test_sensor.py | 17 ++++---- tests/components/template/test_vacuum.py | 5 +-- tests/helpers/test_entity_component.py | 4 +- 23 files changed, 87 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index c2d855d3135..f79213e2484 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv, recorder, restore_state +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, @@ -199,8 +200,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no ) tasks = [ - hass.helpers.entity_component.async_update_entity(entity) - for entity in call.data[ATTR_ENTITY_ID] + async_update_entity(hass, entity) for entity in call.data[ATTR_ENTITY_ID] ] if tasks: diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index a0bab1736ff..59f31085ad2 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -60,6 +60,7 @@ from homeassistant.const import ( STATE_STANDBY, STATE_UNAVAILABLE, ) +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import slugify from tests.common import MockConfigEntry @@ -170,7 +171,7 @@ async def test_reconnect(hass, caplog, config): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -182,7 +183,7 @@ async def test_reconnect(hass, caplog, config): patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: for _ in range(5): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNAVAILABLE @@ -195,7 +196,7 @@ async def test_reconnect(hass, caplog, config): with patchers.patch_connect(True)[patch_key], patchers.patch_shell( SHELL_RESPONSE_STANDBY )[patch_key], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -238,7 +239,7 @@ async def test_adb_shell_returns_none(hass, config): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state != STATE_UNAVAILABLE @@ -246,7 +247,7 @@ async def test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNAVAILABLE @@ -267,7 +268,7 @@ async def test_setup_with_adbkey(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -301,7 +302,7 @@ async def test_sources(hass, config0): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -325,7 +326,7 @@ async def test_sources(hass, config0): ) with patch_update: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_PLAYING @@ -351,7 +352,7 @@ async def test_sources(hass, config0): ) with patch_update: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_PLAYING @@ -380,7 +381,7 @@ async def _test_exclude_sources(hass, config0, expected_sources): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -416,7 +417,7 @@ async def _test_exclude_sources(hass, config0, expected_sources): ) with patch_update: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_PLAYING @@ -461,7 +462,7 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -688,7 +689,7 @@ async def test_setup_fail(hass, config): assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is None @@ -851,7 +852,7 @@ async def test_update_lock_not_acquired(hass): await hass.async_block_till_done() with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF @@ -860,13 +861,13 @@ async def test_update_lock_not_acquired(hass): "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", side_effect=LockNotAcquiredException, ), patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_STANDBY @@ -1003,7 +1004,7 @@ async def test_get_image(hass, hass_ws_client): await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) client = await hass_ws_client(hass) @@ -1208,20 +1209,20 @@ async def test_exception(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF # When an unforeseen exception occurs, we close the ADB connection and raise the exception with patchers.PATCH_ANDROIDTV_UPDATE_EXCEPTION, pytest.raises(Exception): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_UNAVAILABLE # On the next update, HA will reconnect to the device - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 658ba802e8e..5f30be81d6f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from .const import ( AWAIR_UUID, @@ -336,9 +337,7 @@ async def test_awair_unavailable(hass): ) with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): - await hass.helpers.entity_component.async_update_entity( - "sensor.living_room_awair_score" - ) + await async_update_entity(hass, "sensor.living_room_awair_score") assert_expected_properties( hass, registry, diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index 28caa212278..b5a49fdae15 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -4,6 +4,7 @@ from datetime import timedelta from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.broadlink.updater import BroadlinkSP4UpdateManager from homeassistant.const import Platform +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.util import dt @@ -81,9 +82,7 @@ async def test_a1_sensor_update(hass): "light": 3, "noise": 2, } - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors_raw.call_count == 2 sensors_and_states = { @@ -144,9 +143,7 @@ async def test_rm_pro_sensor_update(hass): assert len(sensors) == 1 mock_setup.api.check_sensors.return_value = {"temperature": 25.8} - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { @@ -178,9 +175,7 @@ async def test_rm_pro_filter_crazy_temperature(hass): assert len(sensors) == 1 mock_setup.api.check_sensors.return_value = {"temperature": -7} - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { @@ -258,9 +253,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): assert len(sensors) == 2 mock_setup.api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} - await hass.helpers.entity_component.async_update_entity( - next(iter(sensors)).entity_id - ) + await async_update_entity(hass, next(iter(sensors)).entity_id) assert mock_setup.api.check_sensors.call_count == 2 sensors_and_states = { diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 84cef7e81ff..5034792d389 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_UNKNOWN, ) +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from . import mock_device, mock_location, mock_mode @@ -59,7 +60,7 @@ async def test_alarm_control_panel(hass, canary) -> None: # test private system type(mocked_location).is_private = PropertyMock(return_value=True) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -74,7 +75,7 @@ async def test_alarm_control_panel(hass, canary) -> None: return_value=mock_mode(4, LOCATION_MODE_HOME) ) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -86,7 +87,7 @@ async def test_alarm_control_panel(hass, canary) -> None: return_value=mock_mode(5, LOCATION_MODE_AWAY) ) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -98,7 +99,7 @@ async def test_alarm_control_panel(hass, canary) -> None: return_value=mock_mode(6, LOCATION_MODE_NIGHT) ) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 0c95152338c..d9946cab6a0 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -126,7 +127,7 @@ async def test_sensors_attributes_pro(hass, canary) -> None: future = utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, future) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state2 = hass.states.get(entity_id) @@ -142,7 +143,7 @@ async def test_sensors_attributes_pro(hass, canary) -> None: future += timedelta(seconds=30) async_fire_time_changed(hass, future) - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state3 = hass.states.get(entity_id) diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index 15de72e9c95..ff1256a9bc0 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.helpers.entity_component import async_update_entity from tests.components.dexcom import GLUCOSE_READING, init_integration @@ -36,12 +37,8 @@ async def test_sensors_unknown(hass): "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", return_value=None, ): - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_value" - ) - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_trend" - ) + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_value") + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_trend") test_username_glucose_value = hass.states.get( "sensor.dexcom_test_username_glucose_value" @@ -61,12 +58,8 @@ async def test_sensors_update_failed(hass): "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", side_effect=SessionError, ): - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_value" - ) - await hass.helpers.entity_component.async_update_entity( - "sensor.dexcom_test_username_glucose_trend" - ) + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_value") + await async_update_entity(hass, "sensor.dexcom_test_username_glucose_trend") test_username_glucose_value = hass.states.get( "sensor.dexcom_test_username_glucose_value" diff --git a/tests/components/freedompro/test_cover.py b/tests/components/freedompro/test_cover.py index 11dc35b374c..ed14da90789 100644 --- a/tests/components/freedompro/test_cover.py +++ b/tests/components/freedompro/test_cover.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_OPEN, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -149,7 +150,7 @@ async def test_cover_close( "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_fan.py b/tests/components/freedompro/test_fan.py index 3404c5d17e4..3192398a323 100644 --- a/tests/components/freedompro/test_fan.py +++ b/tests/components/freedompro/test_fan.py @@ -10,6 +10,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -78,7 +79,7 @@ async def test_fan_set_off(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() @@ -107,7 +108,7 @@ async def test_fan_set_off(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_lock.py b/tests/components/freedompro/test_lock.py index e0d25ce91d9..44811faffe8 100644 --- a/tests/components/freedompro/test_lock.py +++ b/tests/components/freedompro/test_lock.py @@ -9,6 +9,7 @@ from homeassistant.components.lock import ( ) from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -73,7 +74,7 @@ async def test_lock_set_unlock(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/freedompro/test_switch.py b/tests/components/freedompro/test_switch.py index 6b62e028d72..f71ec4b896f 100644 --- a/tests/components/freedompro/test_switch.py +++ b/tests/components/freedompro/test_switch.py @@ -5,6 +5,7 @@ from unittest.mock import ANY, patch from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @@ -61,7 +62,7 @@ async def test_switch_set_off(hass, init_integration): "homeassistant.components.freedompro.get_states", return_value=states_response, ): - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 105943d4444..c018553efc6 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.history_stats import DOMAIN from homeassistant.components.history_stats.sensor import HistoryStatsSensor from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -339,7 +340,7 @@ async def test_measure_multiple(hass): return_value=fake_states, ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): - await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") + await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" @@ -416,7 +417,7 @@ async def async_test_measure(hass): return_value=fake_states, ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): - await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") + await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index fb4a0f4c1da..8e89be00433 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -280,7 +280,7 @@ async def test_entity_update(hass): await async_setup_component(hass, "homeassistant", {}) with patch( - "homeassistant.helpers.entity_component.async_update_entity", + "homeassistant.components.homeassistant.async_update_entity", return_value=None, ) as mock_update: await hass.services.async_call( diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index bf2da15b7a4..e7e5784ba71 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components.konnected import config_flow, panel +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component from homeassistant.util import utcnow @@ -654,15 +655,11 @@ async def test_connect_retry(hass, mock_panel): # confirm switch is unavailable after second attempt async_fire_time_changed(hass, utcnow() + timedelta(seconds=11)) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity( - "switch.konnected_445566_actuator_6" - ) + await async_update_entity(hass, "switch.konnected_445566_actuator_6") assert hass.states.get("switch.konnected_445566_actuator_6").state == "unavailable" # confirm switch is available after third attempt async_fire_time_changed(hass, utcnow() + timedelta(seconds=21)) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity( - "switch.konnected_445566_actuator_6" - ) + await async_update_entity(hass, "switch.konnected_445566_actuator_6") assert hass.states.get("switch.konnected_445566_actuator_6").state == "off" diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 3315286967d..fabd263771f 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers.entity_component import async_update_entity import homeassistant.util.dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -102,7 +103,7 @@ async def test_update_exception(hass, mock_light): """Test platform setup.""" mock_light.get_color.side_effect = pykulersky.PykulerskyException - await hass.helpers.entity_component.async_update_entity("light.bedroom") + await async_update_entity(hass, "light.bedroom") state = hass.states.get("light.bedroom") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/nzbget/test_switch.py b/tests/components/nzbget/test_switch.py index debfd9a1be8..d2bad2a46e1 100644 --- a/tests/components/nzbget/test_switch.py +++ b/tests/components/nzbget/test_switch.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from . import init_integration @@ -32,7 +33,7 @@ async def test_download_switch(hass, nzbget_api) -> None: # test download paused instance.status.return_value["DownloadPaused"] = True - await hass.helpers.entity_component.async_update_entity(entity_id) + await async_update_entity(hass, entity_id) await hass.async_block_till_done() state = hass.states.get(entity_id) diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index aacb9e469bb..1e4df8fb673 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry @@ -41,9 +42,7 @@ async def test_anna_climate_binary_sensor_change( assert state assert state.state == STATE_ON - await hass.helpers.entity_component.async_update_entity( - "binary_sensor.opentherm_dhw_state" - ) + await async_update_entity(hass, "binary_sensor.opentherm_dhw_state") state = hass.states.get("binary_sensor.opentherm_dhw_state") assert state diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index edb479354bb..d5edeae3508 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry @@ -27,9 +28,7 @@ async def test_adam_climate_sensor_entities( assert state assert float(state.state) == 7.37 - await hass.helpers.entity_component.async_update_entity( - "sensor.zone_lisa_wk_battery" - ) + await async_update_entity(hass, "sensor.zone_lisa_wk_battery") state = hass.states.get("sensor.zone_lisa_wk_battery") assert state diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index 2c64bd3d0a8..b6a991f4c75 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import utcnow from tests.common import async_fire_time_changed @@ -33,7 +34,7 @@ async def test_client_connected(hass): ): async_fire_time_changed(hass, future) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(TEST_CLIENT_ENTITY_ID) + await async_update_entity(hass, TEST_CLIENT_ENTITY_ID) test_client = hass.states.get(TEST_CLIENT_ENTITY_ID) assert test_client.state == STATE_HOME @@ -51,7 +52,7 @@ async def test_client_disconnected(hass): async_fire_time_changed(hass, future) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(TEST_CLIENT_ENTITY_ID) + await async_update_entity(hass, TEST_CLIENT_ENTITY_ID) test_client = hass.states.get(TEST_CLIENT_ENTITY_ID) assert test_client.state == STATE_NOT_HOME @@ -68,7 +69,7 @@ async def test_clients_update_failed(hass): async_fire_time_changed(hass, future) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(TEST_CLIENT_ENTITY_ID) + await async_update_entity(hass, TEST_CLIENT_ENTITY_ID) test_client = hass.states.get(TEST_CLIENT_ENTITY_ID) assert test_client.state == STATE_UNAVAILABLE diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 1d8d9473f3c..4bd6c9ce305 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -753,14 +754,10 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_entity_picture").state == OFF assert hass.states.get("binary_sensor.all_attribute").state == OFF - await hass.helpers.entity_component.async_update_entity("binary_sensor.all_state") - await hass.helpers.entity_component.async_update_entity("binary_sensor.all_icon") - await hass.helpers.entity_component.async_update_entity( - "binary_sensor.all_entity_picture" - ) - await hass.helpers.entity_component.async_update_entity( - "binary_sensor.all_attribute" - ) + await async_update_entity(hass, "binary_sensor.all_state") + await async_update_entity(hass, "binary_sensor.all_icon") + await async_update_entity(hass, "binary_sensor.all_entity_picture") + await async_update_entity(hass, "binary_sensor.all_attribute") assert hass.states.get("binary_sensor.all_state").state == ON assert hass.states.get("binary_sensor.all_icon").state == OFF diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index b9b1a0cd93b..cffb714da9a 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, callback from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.template import Template from homeassistant.setup import ATTR_COMPONENT, async_setup_component import homeassistant.util.dt as dt_util @@ -407,7 +408,7 @@ async def test_invalid_attribute_template(hass, caplog, start_ha, caplog_setup_t hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity("sensor.invalid_template") + await async_update_entity(hass, "sensor.invalid_template") assert "TemplateError" in caplog_setup_text assert "test_attribute" in caplog.text @@ -506,15 +507,11 @@ async def test_no_template_match_all(hass, caplog): assert hass.states.get("sensor.invalid_friendly_name").state == "hello" assert hass.states.get("sensor.invalid_attribute").state == "hello" - await hass.helpers.entity_component.async_update_entity("sensor.invalid_state") - await hass.helpers.entity_component.async_update_entity("sensor.invalid_icon") - await hass.helpers.entity_component.async_update_entity( - "sensor.invalid_entity_picture" - ) - await hass.helpers.entity_component.async_update_entity( - "sensor.invalid_friendly_name" - ) - await hass.helpers.entity_component.async_update_entity("sensor.invalid_attribute") + await async_update_entity(hass, "sensor.invalid_state") + await async_update_entity(hass, "sensor.invalid_icon") + await async_update_entity(hass, "sensor.invalid_entity_picture") + await async_update_entity(hass, "sensor.invalid_friendly_name") + await async_update_entity(hass, "sensor.invalid_attribute") assert hass.states.get("sensor.invalid_state").state == "2" assert hass.states.get("sensor.invalid_icon").state == "hello" diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 8b283f247e5..1fd875f2df8 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -11,6 +11,7 @@ from homeassistant.components.vacuum import ( STATE_RETURNING, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.helpers.entity_component import async_update_entity from tests.common import assert_setup_component from tests.components.vacuum import common @@ -244,9 +245,7 @@ async def test_attribute_templates(hass, start_ha): hass.states.async_set("sensor.test_state", "Works") await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity( - "vacuum.test_template_vacuum" - ) + await async_update_entity(hass, "vacuum.test_template_vacuum") state = hass.states.get("vacuum.test_template_vacuum") assert state.attributes["test_attribute"] == "It Works." diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 1d18111b0d3..4df78317700 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -16,7 +16,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import discovery -from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_component import EntityComponent, async_update_entity from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -385,7 +385,7 @@ async def test_update_entity(hass): # Called as part of async_add_entities assert len(entity.async_write_ha_state.mock_calls) == 1 - await hass.helpers.entity_component.async_update_entity(entity.entity_id) + await async_update_entity(hass, entity.entity_id) assert len(entity.async_update_ha_state.mock_calls) == 1 assert entity.async_update_ha_state.mock_calls[-1][1][0] is True From 911b159281088d927a474a8b3d56dae630325be8 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 26 Mar 2022 00:34:12 +0100 Subject: [PATCH 0708/1054] Cleanup after pylint update (#68657) --- homeassistant/backports/enum.py | 2 +- homeassistant/components/androidtv/media_player.py | 1 - homeassistant/components/apcupsd/__init__.py | 1 - homeassistant/components/apcupsd/sensor.py | 1 - homeassistant/components/decora/light.py | 1 - homeassistant/components/deluge/__init__.py | 2 +- homeassistant/components/deluge/config_flow.py | 2 +- homeassistant/components/deluge/coordinator.py | 4 ++-- homeassistant/components/esphome/__init__.py | 1 - homeassistant/components/fan/__init__.py | 2 -- homeassistant/components/google/api.py | 6 +++--- homeassistant/components/google_pubsub/__init__.py | 4 +--- .../components/here_travel_time/__init__.py | 10 ++++------ homeassistant/components/isy994/light.py | 1 - homeassistant/components/light/__init__.py | 8 ++------ homeassistant/components/limitlessled/light.py | 1 - homeassistant/components/minio/minio_helper.py | 1 - homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/plex/config_flow.py | 4 +--- homeassistant/components/plex/media_browser.py | 6 +++--- homeassistant/components/recorder/purge.py | 8 +++----- homeassistant/components/recorder/statistics.py | 2 +- homeassistant/components/recorder/util.py | 12 +++--------- homeassistant/components/solax/__init__.py | 2 +- homeassistant/components/spotify/media_player.py | 1 - homeassistant/components/switch_as_x/fan.py | 1 - homeassistant/components/synology_dsm/__init__.py | 2 -- homeassistant/components/vlc_telnet/media_player.py | 1 - homeassistant/components/websocket_api/decorators.py | 1 - homeassistant/config_entries.py | 2 +- homeassistant/helpers/helper_config_entry_flow.py | 1 - 31 files changed, 29 insertions(+), 64 deletions(-) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 1b5a6507847..9a96704a836 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -23,7 +23,7 @@ class StrEnum(str, Enum): return str(self.value) @staticmethod - def _generate_next_value_( # pylint: disable=arguments-differ # https://github.com/PyCQA/pylint/issues/5371 + def _generate_next_value_( name: str, start: int, count: int, last_values: list[Any] ) -> Any: """ diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 19cae59a1b4..8e4b04cb507 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -187,7 +187,6 @@ def adb_decorator(override_available=False): @functools.wraps(func) async def _adb_exception_catcher(self, *args, **kwargs): """Call an ADB-related method and catch exceptions.""" - # pylint: disable=protected-access if not self.available and not override_available: return None diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index a032430e1bc..7cbf33f8b47 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,5 +1,4 @@ """Support for APCUPSd via its Network Information Server (NIS).""" -# pylint: disable=import-error from datetime import timedelta import logging diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 2fae17ac922..b7e7366796b 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,5 +1,4 @@ """Support for APCUPSd sensors.""" -# pylint: disable=import-error from __future__ import annotations import logging diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index e29222c8eee..f4b48ccb01b 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -70,7 +70,6 @@ def retry(method): "Decora connect error for device %s. Reconnecting", device.name, ) - # pylint: disable=protected-access device._switch.connect() return wrapper_retry diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index d8d6945be2a..2253eee43d5 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(api.connect) except ( ConnectionRefusedError, - socket.timeout, # pylint:disable=no-member + socket.timeout, SSLError, ) as ex: raise ConfigEntryNotReady("Connection to Deluge Daemon failed") from ex diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index a15c0608029..fc0dd5ff300 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -114,7 +114,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): await self.hass.async_add_executor_job(api.connect) except ( ConnectionRefusedError, - socket.timeout, # pylint:disable=no-member + socket.timeout, SSLError, ): return "cannot_connect" diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 36c2d5d2240..0ac97e77674 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -53,12 +53,12 @@ class DelugeDataUpdateCoordinator(DataUpdateCoordinator): ) except ( ConnectionRefusedError, - socket.timeout, # pylint:disable=no-member + socket.timeout, SSLError, FailedToReconnectException, ) as ex: raise UpdateFailed(f"Connection to Deluge Daemon Lost: {ex}") from ex - except Exception as ex: # pylint:disable=broad-except + except Exception as ex: if type(ex).__name__ == "BadLoginError": raise ConfigEntryAuthFailed( "Credentials for Deluge client are not valid" diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index fd9b5dfd6d2..eba60a3d5a1 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -633,7 +633,6 @@ def esphome_state_property(func: _PropT) -> _PropT: @property # type: ignore[misc] @functools.wraps(func) def _wrapper(self): # type: ignore[no-untyped-def] - # pylint: disable=protected-access if not self._has_state: return None val = func(self) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 5ed4420c592..89de0cabb10 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -239,7 +239,6 @@ class FanEntity(ToggleEntity): """Set the direction of the fan.""" await self.hass.async_add_executor_job(self.set_direction, direction) - # pylint: disable=arguments-differ def turn_on( self, percentage: int | None = None, @@ -249,7 +248,6 @@ class FanEntity(ToggleEntity): """Turn on the fan.""" raise NotImplementedError() - # pylint: disable=arguments-differ async def async_turn_on( self, percentage: int | None = None, diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index c279aefef88..10b4a35e25f 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -194,7 +194,7 @@ class GoogleCalendarService: service = await self._async_get_service() def _list_calendars() -> list[dict[str, Any]]: - cal_list = service.calendarList() # pylint: disable=no-member + cal_list = service.calendarList() return cal_list.list().execute()["items"] return await self._hass.async_add_executor_job(_list_calendars) @@ -206,7 +206,7 @@ class GoogleCalendarService: service = await self._async_get_service() def _create_event() -> dict[str, Any]: - events = service.events() # pylint: disable=no-member + events = service.events() return events.insert(calendarId=calendar_id, body=event).execute() return await self._hass.async_add_executor_job(_create_event) @@ -223,7 +223,7 @@ class GoogleCalendarService: service = await self._async_get_service() def _list_events() -> tuple[list[dict[str, Any]], str | None]: - events = service.events() # pylint: disable=no-member + events = service.events() result = events.list( calendarId=calendar_id, timeMin=_api_time_format(start_time if start_time else dt.now()), diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index cf1b6da704f..22b7b358dbc 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -54,9 +54,7 @@ def setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: publisher = PublisherClient.from_service_account_json(service_principal_path) - topic_path = publisher.topic_path( # pylint: disable=no-member - project_id, topic_name - ) + topic_path = publisher.topic_path(project_id, topic_name) encoder = DateTimeJSONEncoder() diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 9bee2fff9a1..dde0b28632e 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -110,16 +110,14 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): departure=departure, ) - _LOGGER.debug( - "Raw response is: %s", response.response # pylint: disable=no-member - ) + _LOGGER.debug("Raw response is: %s", response.response) attribution: str | None = None - if "sourceAttribution" in response.response: # pylint: disable=no-member + if "sourceAttribution" in response.response: attribution = build_hass_attribution( response.response.get("sourceAttribution") - ) # pylint: disable=no-member - route: list = response.response["route"] # pylint: disable=no-member + ) + route: list = response.response["route"] summary: dict = route[0]["summary"] waypoint: list = route[0]["waypoint"] distance: float = summary["distance"] diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 640442c3f19..13df6d513b7 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -91,7 +91,6 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): self._last_brightness = self._node.status super().async_on_update(event) - # pylint: disable=arguments-differ async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: """Send the turn on command to the ISY994 light device.""" if self._restore_light_state and brightness is None and self._last_brightness: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 36588704c85..5e8bf473da3 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -295,9 +295,7 @@ def filter_turn_on_params(light, params): if not supported_features & SUPPORT_WHITE_VALUE: params.pop(ATTR_WHITE_VALUE, None) - supported_color_modes = ( - light._light_internal_supported_color_modes # pylint:disable=protected-access - ) + supported_color_modes = light._light_internal_supported_color_modes if not brightness_supported(supported_color_modes): params.pop(ATTR_BRIGHTNESS, None) if COLOR_MODE_COLOR_TEMP not in supported_color_modes: @@ -368,9 +366,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ): profiles.apply_default(light.entity_id, light.is_on, params) - legacy_supported_color_modes = ( - light._light_internal_supported_color_modes # pylint: disable=protected-access - ) + legacy_supported_color_modes = light._light_internal_supported_color_modes supported_color_modes = light.supported_color_modes # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W # for legacy lights diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index e45a4a2ced0..35bfafd602d 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -189,7 +189,6 @@ def state(new_state): def wrapper(self, **kwargs): """Wrap a group state change.""" - # pylint: disable=protected-access pipeline = Pipeline() transition_time = DEFAULT_TRANSITION diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 4f10da10998..cc7b7f8a3e7 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -42,7 +42,6 @@ def get_minio_notification_response( ): """Start listening to minio events. Copied from minio-py.""" query = {"prefix": prefix, "suffix": suffix, "events": events} - # pylint: disable=protected-access return minio_client._url_open( "GET", bucket_name=bucket_name, query=query, preload_content=False ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 48da8fd7e73..22d99d1b7be 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -111,7 +111,7 @@ from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally # because integrations should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index dffbd7a1930..7baee63477a 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -98,9 +98,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.client_id = None self._manual = False - async def async_step_user( - self, user_input=None, errors=None - ): # pylint: disable=arguments-differ + async def async_step_user(self, user_input=None, errors=None): """Handle a flow initialized by the user.""" if user_input is not None: return await self.async_step_plex_website_auth() diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 4b2bb502d69..59895c0d613 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -329,7 +329,7 @@ def library_section_payload(section): children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE] except KeyError as err: raise UnknownMediaType(f"Unknown type received: {section.TYPE}") from err - server_id = section._server.machineIdentifier # pylint: disable=protected-access + server_id = section._server.machineIdentifier return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, @@ -362,7 +362,7 @@ def hub_payload(hub): media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}" else: media_content_id = f"server/{hub.hubIdentifier}" - server_id = hub._server.machineIdentifier # pylint: disable=protected-access + server_id = hub._server.machineIdentifier payload = { "title": hub.title, "media_class": MEDIA_CLASS_DIRECTORY, @@ -376,7 +376,7 @@ def hub_payload(hub): def station_payload(station): """Create response payload for a music station.""" - server_id = station._server.machineIdentifier # pylint: disable=protected-access + server_id = station._server.machineIdentifier return BrowseMedia( title=station.title, media_class=ITEM_TYPE_MEDIA_CLASS[station.type], diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index a15d22810f4..532aef3c53d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -204,7 +204,7 @@ def _evict_purged_states_from_old_states_cache( ) -> None: """Evict purged states from the old states cache.""" # Make a map from old_state_id to entity_id - old_states = instance._old_states # pylint: disable=protected-access + old_states = instance._old_states old_state_reversed = { old_state.state_id: entity_id for entity_id, old_state in old_states.items() @@ -221,9 +221,7 @@ def _evict_purged_attributes_from_attributes_cache( ) -> None: """Evict purged attribute ids from the attribute ids cache.""" # Make a map from attributes_id to the attributes json - state_attributes_ids = ( - instance._state_attributes_ids # pylint: disable=protected-access - ) + state_attributes_ids = instance._state_attributes_ids state_attributes_ids_reversed = { attributes_id: attributes for attributes, attributes_id in state_attributes_ids.items() @@ -378,7 +376,7 @@ def _purge_filtered_events( _purge_event_ids(session, event_ids) if EVENT_STATE_CHANGED in excluded_event_types: session.query(StateAttributes).delete(synchronize_session=False) - instance._state_attributes_ids = {} # pylint: disable=protected-access + instance._state_attributes_ids = {} @retryable_database_job("purge") diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index f01190097df..2ad2530fb83 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -448,7 +448,7 @@ def compile_hourly_statistics( } # Get last hour's last sum - if instance._db_supports_row_number: # pylint: disable=[protected-access] + if instance._db_supports_row_number: subquery = ( session.query(*QUERY_STATISTICS_SUMMARY_SUM) .filter(StatisticsShortTerm.start >= bindparam("start_time")) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 487b8dd22f7..9d12f1d7473 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -359,9 +359,7 @@ def setup_connection_for_dialect( version = _extract_version_from_server_response(version_string) if version and version < MIN_VERSION_SQLITE_ROWNUM: - instance._db_supports_row_number = ( # pylint: disable=[protected-access] - False - ) + instance._db_supports_row_number = False if not version or version < MIN_VERSION_SQLITE: _warn_unsupported_version( version or version_string, "SQLite", MIN_VERSION_SQLITE @@ -383,18 +381,14 @@ def setup_connection_for_dialect( if is_maria_db: if version and version < MIN_VERSION_MARIA_DB_ROWNUM: - instance._db_supports_row_number = ( # pylint: disable=[protected-access] - False - ) + instance._db_supports_row_number = False if not version or version < MIN_VERSION_MARIA_DB: _warn_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) else: if version and version < MIN_VERSION_MYSQL_ROWNUM: - instance._db_supports_row_number = ( # pylint: disable=[protected-access] - False - ) + instance._db_supports_row_number = False if not version or version < MIN_VERSION_MYSQL: _warn_unsupported_version( version or version_string, "MySQL", MIN_VERSION_MYSQL diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py index 2f9d4509dd2..3915f414d0e 100644 --- a/homeassistant/components/solax/__init__.py +++ b/homeassistant/components/solax/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], ) await api.get_data() - except Exception as err: # pylint: disable=broad-except + except Exception as err: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 2b62fdd78c4..3e77c5893c0 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -96,7 +96,6 @@ def spotify_exception_handler(func): """ def wrapper(self, *args, **kwargs): - # pylint: disable=protected-access try: result = func(self, *args, **kwargs) self._attr_available = True diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index e87f49b1b7b..5ebc8902d06 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -49,7 +49,6 @@ class FanSwitch(BaseToggleEntity, FanEntity): """ return self._attr_is_on - # pylint: disable=arguments-differ async def async_turn_on( self, percentage: int | None = None, diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 0503094d1cd..8eed02118c0 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -98,14 +98,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SynologyDSMLoginPermissionDeniedException, ) as err: if err.args[0] and isinstance(err.args[0], dict): - # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryAuthFailed(f"reason: {details}") from err except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: if err.args[0] and isinstance(err.args[0], dict): - # pylint: disable=no-member details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 99b37f97e0c..5b7c5fbb524 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -88,7 +88,6 @@ def catch_vlc_errors( except CommandError as err: LOGGER.error("Command error: %s", err) except ConnectError as err: - # pylint: disable=protected-access if self._available: LOGGER.error("Connection error: %s", err) self._available = False diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 296271c7cfd..223c09eb5fd 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -132,7 +132,6 @@ def websocket_command( def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" - # pylint: disable=protected-access func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] func._ws_command = command # type: ignore[attr-defined] return func diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 0e94aa458a3..e10574bdc4a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1455,7 +1455,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): return await self.async_step_discovery(dataclasses.asdict(discovery_info)) @callback - def async_create_entry( # pylint: disable=arguments-differ + def async_create_entry( self, *, title: str, diff --git a/homeassistant/helpers/helper_config_entry_flow.py b/homeassistant/helpers/helper_config_entry_flow.py index 16471c13e41..d835b74cd54 100644 --- a/homeassistant/helpers/helper_config_entry_flow.py +++ b/homeassistant/helpers/helper_config_entry_flow.py @@ -230,7 +230,6 @@ class HelperConfigFlowHandler(config_entries.ConfigFlow): return _async_step - # pylint: disable-next=no-self-use @abstractmethod @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: From aa013fa8f6abed738097e8045d319117451e09ab Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 26 Mar 2022 00:10:54 -0400 Subject: [PATCH 0709/1054] Add CONF_LOCATION constant (#68474) * Add CONF_LOCATION constant * Update all custom CONF_LOCATION definitions to point to constant * remove CONF_LOCATION from homeassistant/components/totalconnect/const --- homeassistant/components/ambient_station/diagnostics.py | 3 +-- homeassistant/components/pi_hole/__init__.py | 2 +- homeassistant/components/pi_hole/config_flow.py | 2 +- homeassistant/components/pi_hole/const.py | 1 - homeassistant/components/simplisafe/diagnostics.py | 3 +-- homeassistant/components/spaceapi/__init__.py | 2 +- homeassistant/components/tapsaff/binary_sensor.py | 4 +--- homeassistant/components/totalconnect/config_flow.py | 4 +--- homeassistant/components/totalconnect/const.py | 1 - homeassistant/const.py | 1 + tests/components/pi_hole/__init__.py | 3 ++- tests/components/pi_hole/test_init.py | 2 +- 12 files changed, 11 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ambient_station/diagnostics.py b/homeassistant/components/ambient_station/diagnostics.py index cb9c7adab18..6005b206954 100644 --- a/homeassistant/components/ambient_station/diagnostics.py +++ b/homeassistant/components/ambient_station/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_LOCATION from homeassistant.core import HomeAssistant from . import AmbientStation @@ -14,7 +14,6 @@ from .const import CONF_APP_KEY, DOMAIN CONF_API_KEY_CAMEL = "apiKey" CONF_APP_KEY_CAMEL = "appKey" CONF_DEVICE_ID_CAMEL = "deviceId" -CONF_LOCATION = "location" CONF_MAC_ADDRESS = "mac_address" CONF_MAC_ADDRESS_CAMEL = "macAddress" CONF_TZ = "tz" diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 3e18953af84..07b81a2702e 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL, @@ -29,7 +30,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - CONF_LOCATION, CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 39d47ecdd74..40f4555e7d2 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -12,6 +12,7 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_PORT, CONF_SSL, @@ -21,7 +22,6 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_LOCATION, CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 38819d29df4..69e8ecacdaa 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -17,7 +17,6 @@ from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" -CONF_LOCATION = "location" CONF_STATISTICS_ONLY = "statistics_only" DEFAULT_LOCATION = "admin" diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index c7c03467c94..dac89715c10 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_LOCATION from homeassistant.core import HomeAssistant from . import SimpliSafe @@ -13,7 +13,6 @@ from .const import DOMAIN CONF_CREDIT_CARD = "creditCard" CONF_EXPIRES = "expires" -CONF_LOCATION = "location" CONF_LOCATION_NAME = "locationName" CONF_PAYMENT_PROFILE_ID = "paymentProfileId" CONF_SERIAL = "serial" diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index e2ca5b972f8..c18f150a925 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_ADDRESS, CONF_EMAIL, CONF_ENTITY_ID, + CONF_LOCATION, CONF_SENSORS, CONF_STATE, CONF_URL, @@ -55,7 +56,6 @@ CONF_ICON_OPEN = "icon_open" CONF_ICONS = "icons" CONF_IRC = "irc" CONF_ISSUE_REPORT_CHANNELS = "issue_report_channels" -CONF_LOCATION = "location" CONF_SPACEFED = "spacefed" CONF_SPACENET = "spacenet" CONF_SPACESAML = "spacesaml" diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index 3d82da340e9..9608971e78a 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -8,7 +8,7 @@ from tapsaff import TapsAff import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_LOCATION, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,8 +16,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -CONF_LOCATION = "location" - DEFAULT_NAME = "Taps Aff" SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index b529bdd80fd..013b08b50be 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -4,12 +4,10 @@ from total_connect_client.exceptions import AuthenticationError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME from .const import CONF_USERCODES, DOMAIN -CONF_LOCATION = "location" - PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py index 22ecd14281f..ba217bd4ca7 100644 --- a/homeassistant/components/totalconnect/const.py +++ b/homeassistant/components/totalconnect/const.py @@ -2,7 +2,6 @@ DOMAIN = "totalconnect" CONF_USERCODES = "usercodes" -CONF_LOCATION = "location" # Most TotalConnect alarms will work passing '-1' as usercode DEFAULT_USERCODE = "-1" diff --git a/homeassistant/const.py b/homeassistant/const.py index 2c2676d8bb4..cf8f04ed4d9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -169,6 +169,7 @@ CONF_IP_ADDRESS: Final = "ip_address" CONF_LATITUDE: Final = "latitude" CONF_LEGACY_TEMPLATES: Final = "legacy_templates" CONF_LIGHTS: Final = "lights" +CONF_LOCATION: Final = "location" CONF_LONGITUDE: Final = "longitude" CONF_MAC: Final = "mac" CONF_MAXIMUM: Final = "maximum" diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 235cce92a4b..f45c05cfe74 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -3,10 +3,11 @@ from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError -from homeassistant.components.pi_hole.const import CONF_LOCATION, CONF_STATISTICS_ONLY +from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_PORT, CONF_SSL, diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index e96f0d7b33f..9c336f8bb6d 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -6,7 +6,6 @@ from hole.exceptions import HoleError from homeassistant.components import pi_hole, switch from homeassistant.components.pi_hole.const import ( - CONF_LOCATION, CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, @@ -19,6 +18,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_API_KEY, CONF_HOST, + CONF_LOCATION, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL, From 32b2d1e5c96f9d0baebfa4573030f98ae1d5e1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 26 Mar 2022 07:17:11 +0100 Subject: [PATCH 0710/1054] Add backup platform to recorder (#68229) --- homeassistant/components/recorder/backup.py | 28 +++++++++ tests/components/recorder/test_backup.py | 69 +++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 homeassistant/components/recorder/backup.py create mode 100644 tests/components/recorder/test_backup.py diff --git a/homeassistant/components/recorder/backup.py b/homeassistant/components/recorder/backup.py new file mode 100644 index 00000000000..cec9f85748b --- /dev/null +++ b/homeassistant/components/recorder/backup.py @@ -0,0 +1,28 @@ +"""Backup platform for the Recorder integration.""" +from logging import getLogger + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import Recorder +from .const import DATA_INSTANCE +from .util import async_migration_in_progress + +_LOGGER = getLogger(__name__) + + +async def async_pre_backup(hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + _LOGGER.info("Backup start notification, locking database for writes") + instance: Recorder = hass.data[DATA_INSTANCE] + if async_migration_in_progress(hass): + raise HomeAssistantError("Database migration in progress") + await instance.lock_database() + + +async def async_post_backup(hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" + instance: Recorder = hass.data[DATA_INSTANCE] + _LOGGER.info("Backup end notification, releasing write lock") + if not instance.unlock_database(): + raise HomeAssistantError("Could not release database write lock") diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py new file mode 100644 index 00000000000..4667691add8 --- /dev/null +++ b/tests/components/recorder/test_backup.py @@ -0,0 +1,69 @@ +"""Test backup platform for the Recorder integration.""" + + +from unittest.mock import patch + +import pytest + +from homeassistant.components.recorder.backup import async_post_backup, async_pre_backup +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from tests.common import async_init_recorder_component + + +async def test_async_pre_backup(hass: HomeAssistant) -> None: + """Test pre backup.""" + await async_init_recorder_component(hass) + + with patch( + "homeassistant.components.recorder.backup.Recorder.lock_database" + ) as lock_mock: + await async_pre_backup(hass) + assert lock_mock.called + + +async def test_async_pre_backup_with_timeout(hass: HomeAssistant) -> None: + """Test pre backup with timeout.""" + await async_init_recorder_component(hass) + + with patch( + "homeassistant.components.recorder.backup.Recorder.lock_database", + side_effect=TimeoutError(), + ) as lock_mock, pytest.raises(TimeoutError): + await async_pre_backup(hass) + assert lock_mock.called + + +async def test_async_pre_backup_with_migration(hass: HomeAssistant) -> None: + """Test pre backup with migration.""" + await async_init_recorder_component(hass) + + with patch( + "homeassistant.components.recorder.backup.async_migration_in_progress", + return_value=True, + ), pytest.raises(HomeAssistantError): + await async_pre_backup(hass) + + +async def test_async_post_backup(hass: HomeAssistant) -> None: + """Test post backup.""" + await async_init_recorder_component(hass) + + with patch( + "homeassistant.components.recorder.backup.Recorder.unlock_database" + ) as unlock_mock: + await async_post_backup(hass) + assert unlock_mock.called + + +async def test_async_post_backup_failure(hass: HomeAssistant) -> None: + """Test post backup failure.""" + await async_init_recorder_component(hass) + + with patch( + "homeassistant.components.recorder.backup.Recorder.unlock_database", + return_value=False, + ) as unlock_mock, pytest.raises(HomeAssistantError): + await async_post_backup(hass) + assert unlock_mock.called From b9f172899e4d1e55b74984f7c05ed69861a91cbd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Mar 2022 00:59:28 -1000 Subject: [PATCH 0711/1054] Bump objgraph to 3.5.0 to fix stall in profiler tests (#68690) --- homeassistant/components/profiler/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/profiler/manifest.json b/homeassistant/components/profiler/manifest.json index 6e6dde8df1b..390bcecd4d4 100644 --- a/homeassistant/components/profiler/manifest.json +++ b/homeassistant/components/profiler/manifest.json @@ -2,7 +2,7 @@ "domain": "profiler", "name": "Profiler", "documentation": "https://www.home-assistant.io/integrations/profiler", - "requirements": ["pyprof2calltree==1.4.5", "guppy3==3.1.2", "objgraph==3.4.1"], + "requirements": ["pyprof2calltree==1.4.5", "guppy3==3.1.2", "objgraph==3.5.0"], "codeowners": ["@bdraco"], "quality_scale": "internal", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index c0e2336941f..b90804e12b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ oasatelematics==0.3 oauth2client==4.1.3 # homeassistant.components.profiler -objgraph==3.4.1 +objgraph==3.5.0 # homeassistant.components.oem oemthermostat==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b78f5c70383..98dfc5aef0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ numpy==1.21.4 oauth2client==4.1.3 # homeassistant.components.profiler -objgraph==3.4.1 +objgraph==3.5.0 # homeassistant.components.omnilogic omnilogic==0.4.5 From 7198ec06d3c56d6171ed37ba924319971ecf4a8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Mar 2022 01:07:24 -1000 Subject: [PATCH 0712/1054] Fix screenlogic to get the macaddress from discovery (#68687) --- homeassistant/components/screenlogic/config_flow.py | 2 +- homeassistant/components/screenlogic/manifest.json | 2 +- homeassistant/generated/dhcp.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index d9feec629e2..260317dca11 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -79,7 +79,7 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" - mac = _extract_mac_from_name(discovery_info.hostname) + mac = format_mac(discovery_info.macaddress) await self.async_set_unique_id(mac) self._abort_if_unique_id_configured( updates={CONF_IP_ADDRESS: discovery_info.ip} diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 98129e24f01..32ec1872877 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -8,7 +8,7 @@ "dhcp": [ {"registered_devices": true}, { - "hostname": "pentair: *", + "hostname": "pentair*", "macaddress": "00C033*" } ], diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4b4c2d7b979..91e6550919d 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -82,7 +82,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'samsungtv', 'macaddress': '4844F7*'}, {'domain': 'samsungtv', 'macaddress': '8CEA48*'}, {'domain': 'screenlogic', 'registered_devices': True}, - {'domain': 'screenlogic', 'hostname': 'pentair: *', 'macaddress': '00C033*'}, + {'domain': 'screenlogic', 'hostname': 'pentair*', 'macaddress': '00C033*'}, {'domain': 'sense', 'hostname': 'sense-*', 'macaddress': '009D6B*'}, {'domain': 'sense', 'hostname': 'sense-*', 'macaddress': 'DCEFCA*'}, {'domain': 'sense', 'hostname': 'sense-*', 'macaddress': 'A4D578*'}, From 23a567e13542e8ae9ea592f2a1c9d6ae1708c2dd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Mar 2022 12:36:35 +0100 Subject: [PATCH 0713/1054] Update labels for derivative config flow time units (#68665) --- homeassistant/components/derivative/config_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index 1447f943a13..348158ce4e0 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -41,10 +41,10 @@ UNIT_PREFIXES = [ {"value": "T", "label": "P (peta)"}, ] TIME_UNITS = [ - {"value": TIME_SECONDS, "label": "s (seconds)"}, - {"value": TIME_MINUTES, "label": "min (minutes)"}, - {"value": TIME_HOURS, "label": "h (hours)"}, - {"value": TIME_DAYS, "label": "d (days)"}, + {"value": TIME_SECONDS, "label": "Seconds"}, + {"value": TIME_MINUTES, "label": "Minutes"}, + {"value": TIME_HOURS, "label": "Hours"}, + {"value": TIME_DAYS, "label": "Days"}, ] OPTIONS_SCHEMA = vol.Schema( From 25bdb5304d9bf51d51f98f38bb25aee3844fba70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Mar 2022 02:15:47 -1000 Subject: [PATCH 0714/1054] Ensure solaredge can still be setup with an ignored entry (#68688) --- .../components/solaredge/config_flow.py | 22 +++++++-------- .../components/solaredge/test_config_flow.py | 27 ++++++++++++++++++- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 523c75116e9..58e5577ccfa 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -9,22 +9,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN -@callback -def solaredge_entries(hass: HomeAssistant): - """Return the site_ids for the domain.""" - return { - (entry.data[CONF_SITE_ID]) - for entry in hass.config_entries.async_entries(DOMAIN) - } - - class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -34,9 +25,18 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._errors = {} + @callback + def _async_current_site_ids(self) -> set[str]: + """Return the site_ids for the domain.""" + return { + entry.data[CONF_SITE_ID] + for entry in self._async_current_entries(include_ignore=False) + if CONF_SITE_ID in entry.data + } + def _site_in_configuration_exists(self, site_id: str) -> bool: """Return True if site_id exists in configuration.""" - return site_id in solaredge_entries(self.hass) + return site_id in self._async_current_site_ids() def _check_site(self, site_id: str, api_key: str) -> bool: """Check if we can connect to the soleredge api service.""" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 059e10c7662..d0f0ac01235 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -6,7 +6,7 @@ from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import data_entry_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant @@ -66,6 +66,31 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non assert result.get("errors") == {CONF_SITE_ID: "already_configured"} +async def test_ignored_entry_does_not_cause_error( + hass: HomeAssistant, test_api: str +) -> None: + """Test an ignored entry does not cause and error and we can still create an new entry.""" + MockConfigEntry( + domain="solaredge", + data={CONF_NAME: DEFAULT_NAME, CONF_API_KEY: API_KEY}, + source=SOURCE_IGNORE, + ).add_to_hass(hass) + + # user: Should fail, same SITE_ID + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test" + + data = result["data"] + assert data + assert data[CONF_SITE_ID] == SITE_ID + assert data[CONF_API_KEY] == "test" + + async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: """Test the _site_in_configuration_exists method.""" From b96c5696578b631d7af5e28afb5a3863fd510808 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 26 Mar 2022 13:52:55 +0100 Subject: [PATCH 0715/1054] Fix review comments for filesize (#68703) --- homeassistant/components/filesize/sensor.py | 7 +++++-- homeassistant/components/filesize/strings.json | 3 ++- homeassistant/components/filesize/translations/en.json | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index b08b797aa4b..eeb17d3ebc4 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -38,8 +38,11 @@ async def async_setup_platform( ) -> None: """Set up the file size sensor.""" _LOGGER.warning( - # Filesize config flow added in 2022.4 and should be removed in 2022.8 - "Loading filesize via platform setup is deprecated; Please remove it from your configuration" + # Filesize config flow added in 2022.4 and should be removed in 2022.6 + "Configuration of the Filesize sensor platform in YAML is deprecated and " + "will be removed in Home Assistant 2022.6; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" ) for path in config[CONF_FILE_PATHS]: hass.async_create_task( diff --git a/homeassistant/components/filesize/strings.json b/homeassistant/components/filesize/strings.json index 1096d7c888a..01b4ee655b6 100644 --- a/homeassistant/components/filesize/strings.json +++ b/homeassistant/components/filesize/strings.json @@ -14,5 +14,6 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } - } + }, + "title": "Filesize" } \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/en.json b/homeassistant/components/filesize/translations/en.json index 76f3b2bbb47..cd8954e5a71 100644 --- a/homeassistant/components/filesize/translations/en.json +++ b/homeassistant/components/filesize/translations/en.json @@ -14,5 +14,6 @@ "abort": { "already_configured": "Filepath is already configured" } - } + }, + "title": "Filesize" } \ No newline at end of file From ee5e9d09a02e52714291a44148be4722f8e495ac Mon Sep 17 00:00:00 2001 From: kevdliu <1766838+kevdliu@users.noreply.github.com> Date: Sat, 26 Mar 2022 09:17:40 -0400 Subject: [PATCH 0716/1054] Revert "Take Abode camera snapshot before fetching latest image" (#68626) --- homeassistant/components/abode/camera.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 1eb6f859d0c..9885ccb54ef 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -88,8 +88,6 @@ class AbodeCamera(AbodeDevice, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Get a camera image.""" - if not self.capture(): - return None self.refresh_image() if self._response: From 0c2b5b6c12c7b91e5cbcbb7eaf217c2eaaeae240 Mon Sep 17 00:00:00 2001 From: rhpijnacker Date: Sat, 26 Mar 2022 16:46:33 +0100 Subject: [PATCH 0717/1054] Support DSMR data read via RFXtrx with integrated P1 reader (#63529) Co-authored-by: Martin Hjelmare --- homeassistant/components/dsmr/config_flow.py | 42 ++++- homeassistant/components/dsmr/const.py | 4 + homeassistant/components/dsmr/sensor.py | 19 ++- tests/components/dsmr/conftest.py | 64 +++++++ tests/components/dsmr/test_config_flow.py | 168 +++++++++++++++++-- tests/components/dsmr/test_sensor.py | 29 ++++ 6 files changed, 306 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 8bbf6197a10..6152a3756e3 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -9,6 +9,10 @@ from typing import Any from async_timeout import timeout from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.clients.rfxtrx_protocol import ( + create_rfxtrx_dsmr_reader, + create_rfxtrx_tcp_dsmr_reader, +) from dsmr_parser.objects import DSMRObject import serial import serial.tools.list_ports @@ -22,13 +26,16 @@ from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_DSMR_VERSION, + CONF_PROTOCOL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, CONF_TIME_BETWEEN_UPDATE, DEFAULT_TIME_BETWEEN_UPDATE, DOMAIN, + DSMR_PROTOCOL, DSMR_VERSIONS, LOGGER, + RFXTRX_DSMR_PROTOCOL, ) CONF_MANUAL_PATH = "Enter Manually" @@ -37,11 +44,14 @@ CONF_MANUAL_PATH = "Enter Manually" class DSMRConnection: """Test the connection to DSMR and receive telegram to read serial ids.""" - def __init__(self, host: str | None, port: int, dsmr_version: str) -> None: + def __init__( + self, host: str | None, port: int, dsmr_version: str, protocol: str + ) -> None: """Initialize.""" self._host = host self._port = port self._dsmr_version = dsmr_version + self._protocol = protocol self._telegram: dict[str, DSMRObject] = {} self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER if dsmr_version == "5L": @@ -78,16 +88,24 @@ class DSMRConnection: transport.close() if self._host is None: + if self._protocol == DSMR_PROTOCOL: + create_reader = create_dsmr_reader + else: + create_reader = create_rfxtrx_dsmr_reader reader_factory = partial( - create_dsmr_reader, + create_reader, self._port, self._dsmr_version, update_telegram, loop=hass.loop, ) else: + if self._protocol == DSMR_PROTOCOL: + create_reader = create_tcp_dsmr_reader + else: + create_reader = create_rfxtrx_tcp_dsmr_reader reader_factory = partial( - create_tcp_dsmr_reader, + create_reader, self._host, self._port, self._dsmr_version, @@ -113,10 +131,15 @@ class DSMRConnection: async def _validate_dsmr_connection( - hass: core.HomeAssistant, data: dict[str, Any] + hass: core.HomeAssistant, data: dict[str, Any], protocol: str ) -> dict[str, str | None]: """Validate the user input allows us to connect.""" - conn = DSMRConnection(data.get(CONF_HOST), data[CONF_PORT], data[CONF_DSMR_VERSION]) + conn = DSMRConnection( + data.get(CONF_HOST), + data[CONF_PORT], + data[CONF_DSMR_VERSION], + protocol, + ) if not await conn.validate_connect(hass): raise CannotConnect @@ -260,9 +283,14 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = input_data try: - info = await _validate_dsmr_connection(self.hass, data) + try: + protocol = DSMR_PROTOCOL + info = await _validate_dsmr_connection(self.hass, data, protocol) + except CannotCommunicate: + protocol = RFXTRX_DSMR_PROTOCOL + info = await _validate_dsmr_connection(self.hass, data, protocol) - data = {**data, **info} + data = {**data, **info, CONF_PROTOCOL: protocol} if info[CONF_SERIAL_ID]: await self.async_set_unique_id(info[CONF_SERIAL_ID]) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 6d092c1a3ef..2533aa8d025 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -17,6 +17,7 @@ LOGGER = logging.getLogger(__package__) PLATFORMS = [Platform.SENSOR] CONF_DSMR_VERSION = "dsmr_version" +CONF_PROTOCOL = "protocol" CONF_RECONNECT_INTERVAL = "reconnect_interval" CONF_PRECISION = "precision" CONF_TIME_BETWEEN_UPDATE = "time_between_update" @@ -37,6 +38,9 @@ DEVICE_NAME_GAS = "Gas Meter" DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} +DSMR_PROTOCOL = "dsmr_protocol" +RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" + SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key=obis_references.CURRENT_ELECTRICITY_USAGE, diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index e55679f8755..9c684493a3f 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -9,6 +9,10 @@ from functools import partial from dsmr_parser import obis_references as obis_ref from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +from dsmr_parser.clients.rfxtrx_protocol import ( + create_rfxtrx_dsmr_reader, + create_rfxtrx_tcp_dsmr_reader, +) from dsmr_parser.objects import DSMRObject import serial @@ -29,6 +33,7 @@ from homeassistant.util import Throttle from .const import ( CONF_DSMR_VERSION, CONF_PRECISION, + CONF_PROTOCOL, CONF_RECONNECT_INTERVAL, CONF_SERIAL_ID, CONF_SERIAL_ID_GAS, @@ -40,6 +45,7 @@ from .const import ( DEVICE_NAME_ELECTRICITY, DEVICE_NAME_GAS, DOMAIN, + DSMR_PROTOCOL, LOGGER, SENSORS, ) @@ -77,9 +83,14 @@ async def async_setup_entry( # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival + protocol = entry.data.get(CONF_PROTOCOL, DSMR_PROTOCOL) if CONF_HOST in entry.data: + if protocol == DSMR_PROTOCOL: + create_reader = create_tcp_dsmr_reader + else: + create_reader = create_rfxtrx_tcp_dsmr_reader reader_factory = partial( - create_tcp_dsmr_reader, + create_reader, entry.data[CONF_HOST], entry.data[CONF_PORT], dsmr_version, @@ -88,8 +99,12 @@ async def async_setup_entry( keep_alive_interval=60, ) else: + if protocol == DSMR_PROTOCOL: + create_reader = create_dsmr_reader + else: + create_reader = create_rfxtrx_dsmr_reader reader_factory = partial( - create_dsmr_reader, + create_reader, entry.data[CONF_PORT], dsmr_version, update_entities_telegram, diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index e0299d68f2b..8c94c756edc 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -3,6 +3,7 @@ import asyncio from unittest.mock import MagicMock, patch from dsmr_parser.clients.protocol import DSMRProtocol +from dsmr_parser.clients.rfxtrx_protocol import RFXtrxDSMRProtocol from dsmr_parser.obis_references import ( EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS, @@ -36,6 +37,29 @@ async def dsmr_connection_fixture(hass): yield (connection_factory, transport, protocol) +@pytest.fixture +async def rfxtrx_dsmr_connection_fixture(hass): + """Fixture that mocks RFXtrx connection.""" + + transport = MagicMock(spec=asyncio.Transport) + protocol = MagicMock(spec=RFXtrxDSMRProtocol) + + async def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + + with patch( + "homeassistant.components.dsmr.sensor.create_rfxtrx_dsmr_reader", + connection_factory, + ), patch( + "homeassistant.components.dsmr.sensor.create_rfxtrx_tcp_dsmr_reader", + connection_factory, + ): + yield (connection_factory, transport, protocol) + + @pytest.fixture async def dsmr_connection_send_validate_fixture(hass): """Fixture that mocks serial connection.""" @@ -95,3 +119,43 @@ async def dsmr_connection_send_validate_fixture(hass): connection_factory, ): yield (connection_factory, transport, protocol) + + +@pytest.fixture +async def rfxtrx_dsmr_connection_send_validate_fixture(hass): + """Fixture that mocks serial connection.""" + + transport = MagicMock(spec=asyncio.Transport) + protocol = MagicMock(spec=RFXtrxDSMRProtocol) + + protocol.telegram = { + EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), + EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), + P1_MESSAGE_TIMESTAMP: CosemObject([{"value": "12345678", "unit": ""}]), + } + + async def connection_factory(*args, **kwargs): + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + + async def wait_closed(): + if isinstance(connection_factory.call_args_list[0][0][2], str): + # TCP + telegram_callback = connection_factory.call_args_list[0][0][3] + else: + # Serial + telegram_callback = connection_factory.call_args_list[0][0][2] + + telegram_callback(protocol.telegram) + + protocol.wait_closed = wait_closed + + with patch( + "homeassistant.components.dsmr.config_flow.create_rfxtrx_dsmr_reader", + connection_factory, + ), patch( + "homeassistant.components.dsmr.config_flow.create_rfxtrx_tcp_dsmr_reader", + connection_factory, + ): + yield (connection_factory, transport, protocol) diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 669fcfac386..ddec7bda888 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -49,13 +49,70 @@ async def test_setup_network(hass, dsmr_connection_send_validate_fixture): with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"host": "10.10.0.1", "port": 1234, "dsmr_version": "2.2"}, + { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + }, ) + await hass.async_block_till_done() entry_data = { "host": "10.10.0.1", "port": 1234, "dsmr_version": "2.2", + "protocol": "dsmr_protocol", + } + + assert result["type"] == "create_entry" + assert result["title"] == "10.10.0.1:1234" + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +async def test_setup_network_rfxtrx( + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, +): + """Test we can setup network.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + 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"] is None + + 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"] == {} + + # set-up DSMRProtocol to yield no valid telegram, this will retry with RFXtrxDSMRProtocol + protocol.telegram = {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + }, + ) + await hass.async_block_till_done() + + entry_data = { + "host": "10.10.0.1", + "port": 1234, + "dsmr_version": "2.2", + "protocol": "rfxtrx_dsmr_protocol", } assert result["type"] == "create_entry" @@ -87,12 +144,65 @@ async def test_setup_serial(com_mock, hass, dsmr_connection_send_validate_fixtur with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "2.2", + "protocol": "dsmr_protocol", + } + + assert result["type"] == "create_entry" + assert result["title"] == port.device + assert result["data"] == {**entry_data, **SERIAL_DATA} + + +@patch("serial.tools.list_ports.comports", return_value=[com_port()]) +async def test_setup_serial_rfxtrx( + com_mock, + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, +): + """Test we can setup serial.""" + (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + + 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"] is None + + 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"] == {} + + # set-up DSMRProtocol to yield no valid telegram, this will retry with RFXtrxDSMRProtocol + protocol.telegram = {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, + ) + await hass.async_block_till_done() + + entry_data = { + "port": port.device, + "dsmr_version": "2.2", + "protocol": "rfxtrx_dsmr_protocol", } assert result["type"] == "create_entry" @@ -124,12 +234,15 @@ async def test_setup_5L(com_mock, hass, dsmr_connection_send_validate_fixture): with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "5L"} + result["flow_id"], + {"port": port.device, "dsmr_version": "5L"}, ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "5L", + "protocol": "dsmr_protocol", "serial_id": "12345678", "serial_id_gas": "123456789", } @@ -165,10 +278,12 @@ async def test_setup_5S(com_mock, hass, dsmr_connection_send_validate_fixture): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"port": port.device, "dsmr_version": "5S"} ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "5S", + "protocol": "dsmr_protocol", "serial_id": None, "serial_id_gas": None, } @@ -202,12 +317,15 @@ async def test_setup_Q3D(com_mock, hass, dsmr_connection_send_validate_fixture): with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "Q3D"} + result["flow_id"], + {"port": port.device, "dsmr_version": "Q3D"}, ) + await hass.async_block_till_done() entry_data = { "port": port.device, "dsmr_version": "Q3D", + "protocol": "dsmr_protocol", "serial_id": "12345678", "serial_id_gas": None, } @@ -240,7 +358,8 @@ async def test_setup_serial_manual( assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": "Enter Manually", "dsmr_version": "2.2"} + result["flow_id"], + {"port": "Enter Manually", "dsmr_version": "2.2"}, ) assert result["type"] == "form" @@ -251,10 +370,12 @@ async def test_setup_serial_manual( result = await hass.config_entries.flow.async_configure( result["flow_id"], {"port": "/dev/ttyUSB0"} ) + await hass.async_block_till_done() entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", + "protocol": "dsmr_protocol", } assert result["type"] == "create_entry" @@ -297,7 +418,8 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f first_fail_connection_factory, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, ) assert result["type"] == "form" @@ -307,10 +429,18 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_timeout( - com_mock, hass, dsmr_connection_send_validate_fixture + com_mock, + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, ): """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + ( + connection_factory, + transport, + rfxtrx_protocol, + ) = rfxtrx_dsmr_connection_send_validate_fixture port = com_port() @@ -324,6 +454,12 @@ async def test_setup_serial_timeout( ) protocol.wait_closed = first_timeout_wait_closed + first_timeout_wait_closed = AsyncMock( + return_value=True, + side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), + ) + rfxtrx_protocol.wait_closed = first_timeout_wait_closed + assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] is None @@ -349,10 +485,18 @@ async def test_setup_serial_timeout( @patch("serial.tools.list_ports.comports", return_value=[com_port()]) async def test_setup_serial_wrong_telegram( - com_mock, hass, dsmr_connection_send_validate_fixture + com_mock, + hass, + dsmr_connection_send_validate_fixture, + rfxtrx_dsmr_connection_send_validate_fixture, ): """Test failed telegram data.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture + ( + rfxtrx_connection_factory, + transport, + rfxtrx_protocol, + ) = rfxtrx_dsmr_connection_send_validate_fixture port = com_port() @@ -360,8 +504,6 @@ async def test_setup_serial_wrong_telegram( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - protocol.telegram = {} - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] is None @@ -375,8 +517,12 @@ async def test_setup_serial_wrong_telegram( assert result["step_id"] == "setup_serial" assert result["errors"] == {} + protocol.telegram = {} + rfxtrx_protocol.telegram = {} + result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"port": port.device, "dsmr_version": "2.2"} + result["flow_id"], + {"port": port.device, "dsmr_version": "2.2"}, ) assert result["type"] == "form" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 65c52e14d39..93dd78034cc 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -658,6 +658,35 @@ async def test_tcp(hass, dsmr_connection_fixture): "host": "localhost", "port": "1234", "dsmr_version": "2.2", + "protocol": "dsmr_protocol", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", 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 connection_factory.call_args_list[0][0][0] == "localhost" + assert connection_factory.call_args_list[0][0][1] == "1234" + + +async def test_rfxtrx_tcp(hass, rfxtrx_dsmr_connection_fixture): + """If proper config provided RFXtrx TCP connection should be made.""" + (connection_factory, transport, protocol) = rfxtrx_dsmr_connection_fixture + + entry_data = { + "host": "localhost", + "port": "1234", + "dsmr_version": "2.2", + "protocol": "rfxtrx_dsmr_protocol", "precision": 4, "reconnect_interval": 30, "serial_id": "1234", From 00b53502fbad3ec22a53e4c07dea5c420be1a447 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 26 Mar 2022 20:43:15 +0100 Subject: [PATCH 0718/1054] Break out sensors for filesize (#68702) Co-authored-by: J. Nick Koston --- homeassistant/components/filesize/sensor.py | 131 ++++++++++++++++---- tests/components/filesize/test_sensor.py | 39 ++++-- 2 files changed, 138 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index eeb17d3ebc4..97fe5f5511d 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -1,7 +1,7 @@ """Sensor for monitoring the size of a file.""" from __future__ import annotations -import datetime +from datetime import datetime, timedelta import logging import os import pathlib @@ -10,14 +10,25 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, + SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_FILE_PATH, DATA_MEGABYTES +from homeassistant.const import CONF_FILE_PATH, DATA_BYTES, DATA_MEGABYTES from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +import homeassistant.util.dt as dt_util from .const import CONF_FILE_PATHS, DOMAIN @@ -25,6 +36,34 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:file" +SENSOR_TYPES = ( + SensorEntityDescription( + key="file", + icon=ICON, + name="Size", + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="bytes", + entity_registry_enabled_default=False, + icon=ICON, + name="Size bytes", + native_unit_of_measurement=DATA_BYTES, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="last_updated", + entity_registry_enabled_default=False, + icon=ICON, + name="Last Updated", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( {vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])} ) @@ -65,36 +104,82 @@ async def async_setup_entry( get_path = await hass.async_add_executor_job(pathlib.Path, path) fullpath = str(get_path.absolute()) + coordinator = FileSizeCoordinator(hass, fullpath) + await coordinator.async_config_entry_first_refresh() + if get_path.exists() and get_path.is_file(): - async_add_entities([FilesizeEntity(fullpath, entry.entry_id)], True) + async_add_entities( + [ + FilesizeEntity(description, fullpath, entry.entry_id, coordinator) + for description in SENSOR_TYPES + ] + ) -class FilesizeEntity(SensorEntity): - """Encapsulates file size information.""" +class FileSizeCoordinator(DataUpdateCoordinator): + """Filesize coordinator.""" - _attr_native_unit_of_measurement = DATA_MEGABYTES - _attr_icon = ICON + def __init__(self, hass: HomeAssistant, path: str) -> None: + """Initialize filesize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=60), + ) + self._path = path - def __init__(self, path: str, entry_id: str) -> None: - """Initialize the data object.""" - self._path = path # Need to check its a valid path - self._attr_name = path.split("/")[-1] - self._attr_unique_id = entry_id - - def update(self) -> None: - """Update the sensor.""" + async def _async_update_data(self) -> dict[str, float | int | datetime]: + """Fetch file information.""" try: statinfo = os.stat(self._path) except OSError as error: - _LOGGER.error("Can not retrieve file statistics %s", error) - self._attr_native_value = None - return + raise UpdateFailed(f"Can not retrieve file statistics {error}") from error size = statinfo.st_size - last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime).isoformat() - self._attr_native_value = round(size / 1e6, 2) if size else None - self._attr_extra_state_attributes = { - "path": self._path, - "last_updated": last_updated, + last_updated = datetime.fromtimestamp(statinfo.st_mtime).replace( + tzinfo=dt_util.UTC + ) + _LOGGER.debug("size %s, last updated %s", size, last_updated) + data: dict[str, int | float | datetime] = { + "file": round(size / 1e6, 2), "bytes": size, + "last_updated": last_updated, } + + return data + + +class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): + """Encapsulates file size information.""" + + entity_description: SensorEntityDescription + + def __init__( + self, + description: SensorEntityDescription, + path: str, + entry_id: str, + coordinator: FileSizeCoordinator, + ) -> None: + """Initialize the data object.""" + super().__init__(coordinator) + base_name = path.split("/")[-1] + self._attr_name = f"{base_name} {description.name}" + self._attr_unique_id = ( + entry_id if description.key == "file" else f"{entry_id}-{description.key}" + ) + self.entity_description = description + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + name=base_name, + ) + + @property + def native_value(self) -> float | int | datetime: + """Return the value of the sensor.""" + value: float | int | datetime = self.coordinator.data[ + self.entity_description.key + ] + return value diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 74a6f056783..6f21119f95f 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,9 +1,11 @@ """The tests for the filesize sensor.""" import os -from homeassistant.const import CONF_FILE_PATH, STATE_UNKNOWN +from homeassistant.components.filesize.const import DOMAIN +from homeassistant.const import CONF_FILE_PATH, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.setup import async_setup_component from . import TEST_FILE, TEST_FILE_NAME, create_file @@ -38,19 +40,18 @@ async def test_valid_path( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.file_txt") + state = hass.states.get("sensor.file_txt_size") assert state assert state.state == "0.0" - assert state.attributes.get("bytes") == 4 await hass.async_add_executor_job(os.remove, testfile) -async def test_state_unknown( +async def test_state_unavailable( hass: HomeAssistant, tmpdir: str, mock_config_entry: MockConfigEntry ) -> None: """Verify we handle state unavailable.""" - testfile = f"{tmpdir}/file" + testfile = f"{tmpdir}/file.txt" create_file(testfile) hass.config.allowlist_external_dirs = {tmpdir} mock_config_entry.add_to_hass(hass) @@ -61,12 +62,32 @@ async def test_state_unknown( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.file") + state = hass.states.get("sensor.file_txt_size") assert state assert state.state == "0.0" await hass.async_add_executor_job(os.remove, testfile) - await async_update_entity(hass, "sensor.file") + await async_update_entity(hass, "sensor.file_txt_size") - state = hass.states.get("sensor.file") - assert state.state == STATE_UNKNOWN + state = hass.states.get("sensor.file_txt_size") + assert state.state == STATE_UNAVAILABLE + + +async def test_import_query(hass: HomeAssistant, tmpdir: str) -> None: + """Test import from yaml.""" + testfile = f"{tmpdir}/file.txt" + create_file(testfile) + hass.config.allowlist_external_dirs = {tmpdir} + config = { + "sensor": { + "platform": "filesize", + "file_paths": [testfile], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries(DOMAIN) + data = hass.config_entries.async_entries(DOMAIN)[0].data + assert data[CONF_FILE_PATH] == testfile From e844c2380a2f970377bb4481bf3f6abe50ea006b Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 26 Mar 2022 20:50:50 +0100 Subject: [PATCH 0719/1054] Add config flow to fibaro (#65203) Co-authored-by: J. Nick Koston --- .coveragerc | 11 +- CODEOWNERS | 2 + homeassistant/components/fibaro/__init__.py | 166 +++++++++----- .../components/fibaro/binary_sensor.py | 28 +-- homeassistant/components/fibaro/climate.py | 24 +-- .../components/fibaro/config_flow.py | 81 +++++++ homeassistant/components/fibaro/const.py | 4 + homeassistant/components/fibaro/cover.py | 25 +-- homeassistant/components/fibaro/light.py | 33 ++- homeassistant/components/fibaro/lock.py | 25 +-- homeassistant/components/fibaro/manifest.json | 3 +- homeassistant/components/fibaro/scene.py | 17 +- homeassistant/components/fibaro/sensor.py | 25 +-- homeassistant/components/fibaro/strings.json | 22 ++ homeassistant/components/fibaro/switch.py | 25 +-- .../components/fibaro/translations/en.json | 22 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/fibaro/__init__.py | 1 + tests/components/fibaro/test_config_flow.py | 204 ++++++++++++++++++ 20 files changed, 565 insertions(+), 157 deletions(-) create mode 100644 homeassistant/components/fibaro/config_flow.py create mode 100644 homeassistant/components/fibaro/const.py create mode 100644 homeassistant/components/fibaro/strings.json create mode 100644 homeassistant/components/fibaro/translations/en.json create mode 100644 tests/components/fibaro/__init__.py create mode 100644 tests/components/fibaro/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f38836f3a65..254515c3745 100644 --- a/.coveragerc +++ b/.coveragerc @@ -335,7 +335,16 @@ omit = homeassistant/components/faa_delays/binary_sensor.py homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py - homeassistant/components/fibaro/* + homeassistant/components/fibaro/__init__.py + homeassistant/components/fibaro/binary_sensor.py + homeassistant/components/fibaro/climate.py + homeassistant/components/fibaro/cover.py + homeassistant/components/fibaro/light.py + homeassistant/components/fibaro/lock.py + homeassistant/components/fibaro/scene.py + homeassistant/components/fibaro/sensor.py + homeassistant/components/fibaro/switch.py + homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py homeassistant/components/fireservicerota/__init__.py homeassistant/components/fireservicerota/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 97ffa74b143..183d699d2ae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -306,6 +306,8 @@ tests/components/faa_delays/* @ntilley905 homeassistant/components/fan/* @home-assistant/core tests/components/fan/* @home-assistant/core homeassistant/components/fastdotcom/* @rohankapoorcom +homeassistant/components/fibaro/* @rappenze +tests/components/fibaro/* @rappenze homeassistant/components/file/* @fabaff tests/components/file/* @fabaff homeassistant/components/filesize/* @gjohansson-ST diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index cfe39201913..00f8c4da92d 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -3,10 +3,13 @@ from __future__ import annotations from collections import defaultdict import logging +from typing import Any from fiblary3.client.v4.client import Client as FibaroClient, StateHandler +from fiblary3.common.exceptions import HTTPException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ARMED, ATTR_BATTERY_LEVEL, @@ -17,16 +20,17 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, CONF_WHITE_VALUE, - EVENT_HOMEASSISTANT_STOP, Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from homeassistant.util import convert, slugify +from .const import CONF_IMPORT_PLUGINS, DOMAIN + _LOGGER = logging.getLogger(__name__) ATTR_CURRENT_POWER_W = "current_power_w" @@ -37,8 +41,7 @@ CONF_DIMMING = "dimming" CONF_GATEWAYS = "gateways" CONF_PLUGINS = "plugins" CONF_RESET_COLOR = "reset_color" -DOMAIN = "fibaro" -FIBARO_CONTROLLERS = "fibaro_controllers" +FIBARO_CONTROLLER = "fibaro_controller" FIBARO_DEVICES = "fibaro_devices" PLATFORMS = [ Platform.BINARY_SENSOR, @@ -102,11 +105,14 @@ GATEWAY_CONFIG = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])} - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + {vol.Required(CONF_GATEWAYS): vol.All(cv.ensure_list, [GATEWAY_CONFIG])} + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -116,21 +122,19 @@ class FibaroController: def __init__(self, config): """Initialize the Fibaro controller.""" - self._client = FibaroClient( config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] ) self._scene_map = None # Whether to import devices from plugins - self._import_plugins = config[CONF_PLUGINS] - self._device_config = config[CONF_DEVICE_CONFIG] + self._import_plugins = config[CONF_IMPORT_PLUGINS] self._room_map = None # Mapping roomId to room object self._device_map = None # Mapping deviceId to device object self.fibaro_devices = None # List of devices by type self._callbacks = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object - self._excluded_devices = config[CONF_EXCLUDE] self.hub_serial = None # Unique serial number of the hub + self.name = None # The friendly name of the hub def connect(self): """Start the communication with the Fibaro controller.""" @@ -138,6 +142,7 @@ class FibaroController: login = self._client.login.get() info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) + self.name = slugify(info.hcName) except AssertionError: _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False @@ -152,6 +157,23 @@ class FibaroController: self._read_scenes() return True + def connect_with_error_handling(self) -> None: + """Translate connect errors to easily differentiate auth and connect failures. + + When there is a better error handling in the used library this can be improved. + """ + try: + connected = self.connect() + if not connected: + raise FibaroConnectFailed("Connect status is false") + except HTTPException as http_ex: + if http_ex.details == "Forbidden": + raise FibaroAuthFailed from http_ex + + raise FibaroConnectFailed from http_ex + except Exception as ex: + raise FibaroConnectFailed from ex + def enable_state_handler(self): """Start StateHandler thread for monitoring updates.""" self._state_handler = StateHandler(self._client, self._on_state_change) @@ -299,16 +321,11 @@ class FibaroController: device.ha_id = ( f"{slugify(room_name)}_{slugify(device.name)}_{device.id}" ) - if ( - device.enabled - and ( - "isPlugin" not in device - or (not device.isPlugin or self._import_plugins) - ) - and device.ha_id not in self._excluded_devices + if device.enabled and ( + "isPlugin" not in device + or (not device.isPlugin or self._import_plugins) ): device.mapped_type = self._map_device_to_type(device) - device.device_config = self._device_config.get(device.ha_id, {}) else: device.mapped_type = None if (dtype := device.mapped_type) is None: @@ -357,39 +374,78 @@ class FibaroController: pass -def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: - """Set up the Fibaro Component.""" +async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: + """Migrate configuration from configuration.yaml.""" + if DOMAIN not in base_config: + return True gateways = base_config[DOMAIN][CONF_GATEWAYS] - hass.data[FIBARO_CONTROLLERS] = {} - - def stop_fibaro(event): - """Stop Fibaro Thread.""" - _LOGGER.info("Shutting down Fibaro connection") - for controller in hass.data[FIBARO_CONTROLLERS].values(): - controller.disable_state_handler() - - hass.data[FIBARO_DEVICES] = {} - for platform in PLATFORMS: - hass.data[FIBARO_DEVICES][platform] = [] - - for gateway in gateways: - controller = FibaroController(gateway) - if controller.connect(): - hass.data[FIBARO_CONTROLLERS][controller.hub_serial] = controller - for platform in PLATFORMS: - hass.data[FIBARO_DEVICES][platform].extend( - controller.fibaro_devices[platform] - ) - - if hass.data[FIBARO_CONTROLLERS]: - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, base_config) - for controller in hass.data[FIBARO_CONTROLLERS].values(): - controller.enable_state_handler() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_fibaro) + if gateways is None: return True - return False + # check if already configured + if hass.config_entries.async_entries(DOMAIN): + return True + + for gateway in gateways: + # prepare new config based on configuration.yaml + conf = { + CONF_URL: gateway[CONF_URL], + CONF_USERNAME: gateway[CONF_USERNAME], + CONF_PASSWORD: gateway[CONF_PASSWORD], + CONF_IMPORT_PLUGINS: gateway[CONF_PLUGINS], + } + + # import into config flow based configuration + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +def _init_controller(data: dict[str, Any]) -> FibaroController: + """Validate the user input allows us to connect to fibaro.""" + controller = FibaroController(data) + controller.connect_with_error_handling() + return controller + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Fibaro Component.""" + try: + controller = await hass.async_add_executor_job(_init_controller, entry.data) + except FibaroConnectFailed as connect_ex: + raise ConfigEntryNotReady( + f"Could not connect to controller at {entry.data[CONF_URL]}" + ) from connect_ex + except FibaroAuthFailed: + return False + + data: dict[str, Any] = {} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data + data[FIBARO_CONTROLLER] = controller + devices = data[FIBARO_DEVICES] = {} + for platform in PLATFORMS: + devices[platform] = [*controller.fibaro_devices[platform]] + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + controller.enable_state_handler() + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + _LOGGER.info("Shutting down Fibaro connection") + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + hass.data[DOMAIN][entry.entry_id][FIBARO_CONTROLLER].disable_state_handler() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok class FibaroDevice(Entity): @@ -519,3 +575,11 @@ class FibaroDevice(Entity): pass return attr + + +class FibaroConnectFailed(HomeAssistantError): + """Error to indicate we cannot connect to fibaro home center.""" + + +class FibaroAuthFailed(HomeAssistantError): + """Error to indicate that authentication failed on fibaro home center.""" diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index f7b20ec9983..1b07d3671ae 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -2,16 +2,16 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( - DOMAIN, + ENTITY_ID_FORMAT, BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_ICON +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN SENSOR_TYPES = { "com.fibaro.floodSensor": ["Flood", "mdi:water", "flood"], @@ -28,20 +28,18 @@ SENSOR_TYPES = { } -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - - add_entities( + async_add_entities( [ FibaroBinarySensor(device) - for device in hass.data[FIBARO_DEVICES]["binary_sensor"] + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][ + "binary_sensor" + ] ], True, ) @@ -54,9 +52,8 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): """Initialize the binary_sensor.""" self._state = None super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) stype = None - devconf = fibaro_device.device_config if fibaro_device.type in SENSOR_TYPES: stype = fibaro_device.type elif fibaro_device.baseType in SENSOR_TYPES: @@ -67,9 +64,6 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): else: self._device_class = None self._icon = None - # device_config overrides: - self._device_class = devconf.get(CONF_DEVICE_CLASS, self._device_class) - self._icon = devconf.get(CONF_ICON, self._icon) @property def icon(self): diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 1c065ca3fb4..b639324acd0 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -17,12 +17,13 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN PRESET_RESUME = "resume" PRESET_MOIST = "moist" @@ -98,18 +99,17 @@ HA_OPMODES_HVAC = { } -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - - add_entities( - [FibaroThermostat(device) for device in hass.data[FIBARO_DEVICES]["climate"]], + async_add_entities( + [ + FibaroThermostat(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["climate"] + ], True, ) @@ -125,7 +125,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._op_mode_device = None self._fan_mode_device = None self._support_flags = 0 - self.entity_id = f"climate.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self._hvac_support = [] self._preset_support = [] self._fan_support = [] diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py new file mode 100644 index 00000000000..f528fd8a184 --- /dev/null +++ b/homeassistant/components/fibaro/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Fibaro integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType + +from . import FibaroAuthFailed, FibaroConnectFailed, FibaroController +from .const import CONF_IMPORT_PLUGINS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_IMPORT_PLUGINS, default=False): bool, + } +) + + +def _connect_to_fibaro(data: dict[str, Any]) -> FibaroController: + """Validate the user input allows us to connect to fibaro.""" + controller = FibaroController(data) + controller.connect_with_error_handling() + return controller + + +async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + controller = await hass.async_add_executor_job(_connect_to_fibaro, data) + + _LOGGER.debug( + "Successfully connected to fibaro home center %s with name %s", + controller.hub_serial, + controller.name, + ) + return {"serial_number": controller.hub_serial, "name": controller.name} + + +class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fibaro.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + info = await _validate_input(self.hass, user_input) + except FibaroConnectFailed: + errors["base"] = "cannot_connect" + except FibaroAuthFailed: + errors["base"] = "invalid_auth" + else: + await self.async_set_unique_id(info["serial_number"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["name"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: + """Import a config entry.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/fibaro/const.py b/homeassistant/components/fibaro/const.py new file mode 100644 index 00000000000..cf564ab0bfc --- /dev/null +++ b/homeassistant/components/fibaro/const.py @@ -0,0 +1,4 @@ +"""Constants for the Fibaro integration.""" + +DOMAIN = "fibaro" +CONF_IMPORT_PLUGINS = "import_plugins" diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index f98d907945b..b87ae1cbf47 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -4,28 +4,29 @@ from __future__ import annotations from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - DOMAIN, + ENTITY_ID_FORMAT, CoverEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro covers.""" - if discovery_info is None: - return - - add_entities( - [FibaroCover(device) for device in hass.data[FIBARO_DEVICES]["cover"]], True + async_add_entities( + [ + FibaroCover(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["cover"] + ], + True, ) @@ -35,7 +36,7 @@ class FibaroCover(FibaroDevice, CoverEntity): def __init__(self, fibaro_device): """Initialize the Vera device.""" super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @staticmethod def bound(position): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index f3f0ee23181..c825bec28c6 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -8,19 +8,19 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - DOMAIN, + ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.const import CONF_WHITE_VALUE +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.color as color_util -from . import CONF_COLOR, CONF_DIMMING, CONF_RESET_COLOR, FIBARO_DEVICES, FibaroDevice +from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN def scaleto255(value): @@ -40,18 +40,18 @@ def scaleto100(value): return max(0, min(100, ((value * 100.0) / 255.0))) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Perform the setup for Fibaro controller devices.""" - if discovery_info is None: - return - async_add_entities( - [FibaroLight(device) for device in hass.data[FIBARO_DEVICES]["light"]], True + [ + FibaroLight(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["light"] + ], + True, ) @@ -67,8 +67,7 @@ class FibaroLight(FibaroDevice, LightEntity): self._update_lock = asyncio.Lock() self._white = 0 - devconf = fibaro_device.device_config - self._reset_color = devconf.get(CONF_RESET_COLOR, False) + self._reset_color = False supports_color = ( "color" in fibaro_device.properties or "colorComponents" in fibaro_device.properties @@ -91,15 +90,15 @@ class FibaroLight(FibaroDevice, LightEntity): ) # Configuration can override default capability detection - if devconf.get(CONF_DIMMING, supports_dimming): + if supports_dimming: self._supported_flags |= SUPPORT_BRIGHTNESS - if devconf.get(CONF_COLOR, supports_color): + if supports_color: self._supported_flags |= SUPPORT_COLOR - if devconf.get(CONF_WHITE_VALUE, supports_white_v): + if supports_white_v: self._supported_flags |= SUPPORT_WHITE_VALUE super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @property def brightness(self): diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py index bcfd10767e2..5b86a99cbc9 100644 --- a/homeassistant/components/fibaro/lock.py +++ b/homeassistant/components/fibaro/lock.py @@ -1,26 +1,27 @@ """Support for Fibaro locks.""" from __future__ import annotations -from homeassistant.components.lock import DOMAIN, LockEntity +from homeassistant.components.lock import ENTITY_ID_FORMAT, LockEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro locks.""" - if discovery_info is None: - return - - add_entities( - [FibaroLock(device) for device in hass.data[FIBARO_DEVICES]["lock"]], True + async_add_entities( + [ + FibaroLock(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["lock"] + ], + True, ) @@ -31,7 +32,7 @@ class FibaroLock(FibaroDevice, LockEntity): """Initialize the Fibaro device.""" self._state = False super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) def lock(self, **kwargs): """Lock the device.""" diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 7bc7d5a0e49..218a6aad857 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -3,7 +3,8 @@ "name": "Fibaro", "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": ["fiblary3==0.1.8"], - "codeowners": [], + "codeowners": ["@rappenze"], "iot_class": "local_push", + "config_flow": true, "loggers": ["fiblary3"] } diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index 02ebd5cd99e..cc476a29cbb 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -4,25 +4,26 @@ from __future__ import annotations from typing import Any from homeassistant.components.scene import Scene +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: """Perform the setup for Fibaro scenes.""" - if discovery_info is None: - return - async_add_entities( - [FibaroScene(scene) for scene in hass.data[FIBARO_DEVICES]["scene"]], True + [ + FibaroScene(scene) + for scene in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["scene"] + ], + True, ) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 901552d0363..9a7098b018c 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -4,11 +4,12 @@ from __future__ import annotations from contextlib import suppress from homeassistant.components.sensor import ( - DOMAIN, + ENTITY_ID_FORMAT, SensorDeviceClass, SensorEntity, SensorStateClass, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, ENERGY_KILO_WATT_HOUR, @@ -19,10 +20,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN SENSOR_TYPES = { "com.fibaro.temperatureSensor": [ @@ -54,25 +55,21 @@ SENSOR_TYPES = { } -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro controller devices.""" - if discovery_info is None: - return - entities: list[SensorEntity] = [] - for device in hass.data[FIBARO_DEVICES]["sensor"]: + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["sensor"]: entities.append(FibaroSensor(device)) for device_type in ("cover", "light", "switch"): - for device in hass.data[FIBARO_DEVICES][device_type]: + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][device_type]: if "energy" in device.interfaces: entities.append(FibaroEnergySensor(device)) - add_entities(entities, True) + async_add_entities(entities, True) class FibaroSensor(FibaroDevice, SensorEntity): @@ -83,7 +80,7 @@ class FibaroSensor(FibaroDevice, SensorEntity): self.current_value = None self.last_changed_time = None super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) if fibaro_device.type in SENSOR_TYPES: self._unit = SENSOR_TYPES[fibaro_device.type][1] self._icon = SENSOR_TYPES[fibaro_device.type][2] @@ -139,7 +136,7 @@ class FibaroEnergySensor(FibaroDevice, SensorEntity): def __init__(self, fibaro_device): """Initialize the sensor.""" super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}_energy" + self.entity_id = ENTITY_ID_FORMAT.format(f"{self.ha_id}_energy") self._attr_name = f"{fibaro_device.friendly_name} Energy" self._attr_unique_id = f"{fibaro_device.unique_id_str}_energy" diff --git a/homeassistant/components/fibaro/strings.json b/homeassistant/components/fibaro/strings.json new file mode 100644 index 00000000000..c81e41f04dc --- /dev/null +++ b/homeassistant/components/fibaro/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL in the format http://HOST/api/", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "import_plugins": "Import entities from fibaro plugins?" + } + } + }, + "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": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index 3de9235cb0e..a075c4e0704 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -1,27 +1,28 @@ """Support for Fibaro switches.""" from __future__ import annotations -from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice +from .const import DOMAIN -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fibaro switches.""" - if discovery_info is None: - return - - add_entities( - [FibaroSwitch(device) for device in hass.data[FIBARO_DEVICES]["switch"]], True + async_add_entities( + [ + FibaroSwitch(device) + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES]["switch"] + ], + True, ) @@ -32,7 +33,7 @@ class FibaroSwitch(FibaroDevice, SwitchEntity): """Initialize the Fibaro device.""" self._state = False super().__init__(fibaro_device) - self.entity_id = f"{DOMAIN}.{self.ha_id}" + self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) def turn_on(self, **kwargs): """Turn device on.""" diff --git a/homeassistant/components/fibaro/translations/en.json b/homeassistant/components/fibaro/translations/en.json new file mode 100644 index 00000000000..2baeb3a7213 --- /dev/null +++ b/homeassistant/components/fibaro/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "url": "URL in the format http://HOST/api/", + "import_plugins": "Import entities from fibaro plugins?", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5a8ea33c7bd..a8a5f492e9f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -97,6 +97,7 @@ FLOWS = { "evil_genius_labs", "ezviz", "faa_delays", + "fibaro", "filesize", "fireservicerota", "fivem", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98dfc5aef0a..28cfe61801b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -433,6 +433,9 @@ faadelays==0.0.7 # homeassistant.components.feedreader feedparser==6.0.2 +# homeassistant.components.fibaro +fiblary3==0.1.8 + # homeassistant.components.fivem fivem-api==0.1.2 diff --git a/tests/components/fibaro/__init__.py b/tests/components/fibaro/__init__.py new file mode 100644 index 00000000000..3787f00202e --- /dev/null +++ b/tests/components/fibaro/__init__.py @@ -0,0 +1 @@ +"""Tests for the Fibaro integration.""" diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py new file mode 100644 index 00000000000..6f3e035a2f7 --- /dev/null +++ b/tests/components/fibaro/test_config_flow.py @@ -0,0 +1,204 @@ +"""Test the Fibaro config flow.""" +from unittest.mock import Mock, patch + +from fiblary3.common.exceptions import HTTPException +import pytest + +from homeassistant import config_entries +from homeassistant.components.fibaro import DOMAIN +from homeassistant.components.fibaro.const import CONF_IMPORT_PLUGINS +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME + +TEST_SERIALNUMBER = "HC2-111111" +TEST_NAME = "my_fibaro_home_center" +TEST_URL = "http://192.168.1.1/api/" +TEST_USERNAME = "user" +TEST_PASSWORD = "password" + + +@pytest.fixture(name="fibaro_client", autouse=True) +def fibaro_client_fixture(): + """Mock common methods and attributes of fibaro client.""" + info_mock = Mock() + info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME) + + array_mock = Mock() + array_mock.list.return_value = [] + + with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( + "fiblary3.client.v4.client.Client.info", + info_mock, + create=True, + ), patch("fiblary3.client.v4.client.Client.rooms", array_mock, create=True,), patch( + "fiblary3.client.v4.client.Client.devices", + array_mock, + create=True, + ), patch( + "fiblary3.client.v4.client.Client.scenes", + array_mock, + create=True, + ): + yield + + +async def test_config_flow_user_initiated_success(hass): + """Successful flow manually initialized by the user.""" + 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"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=True) + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + } + + +async def test_config_flow_user_initiated_connect_failure(hass): + """Connect failure in flow manually initialized by the user.""" + 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"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=False) + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_user_initiated_auth_failure(hass): + """Authentication failure in flow manually initialized by the user.""" + 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"] == {} + + login_mock = Mock() + login_mock.get.side_effect = HTTPException(details="Forbidden") + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_config_flow_user_initiated_unknown_failure_1(hass): + """Unknown failure in flow manually initialized by the user.""" + 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"] == {} + + login_mock = Mock() + login_mock.get.side_effect = HTTPException(details="Any") + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_user_initiated_unknown_failure_2(hass): + """Unknown failure in flow manually initialized by the user.""" + 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"], + { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_config_flow_import(hass): + """Test for importing config from configuration.yaml.""" + login_mock = Mock() + login_mock.get.return_value = Mock(status=True) + with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + } From b9b81da2ec1b0b9a144906e7bf86cf4dc7274e6c Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 26 Mar 2022 16:01:48 -0700 Subject: [PATCH 0720/1054] Bump androidtv to 0.0.66 (Android 11 support) (#68720) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index cd8e86a42a2..bc082938eeb 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.4.0", - "androidtv[async]==0.0.63", + "androidtv[async]==0.0.66", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion", "@ollo69"], diff --git a/requirements_all.txt b/requirements_all.txt index b90804e12b1..53e086b54a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -292,7 +292,7 @@ ambiclimate==0.2.1 amcrest==1.9.7 # homeassistant.components.androidtv -androidtv[async]==0.0.63 +androidtv[async]==0.0.66 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28cfe61801b..59f774e12d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -246,7 +246,7 @@ amberelectric==1.0.4 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.63 +androidtv[async]==0.0.66 # homeassistant.components.apprise apprise==0.9.7 From 4e9430cba51d67b90c31e87ab1f682d05b3c8f55 Mon Sep 17 00:00:00 2001 From: Nathan Tilley Date: Sat, 26 Mar 2022 19:05:02 -0700 Subject: [PATCH 0721/1054] Fix typo in NMAP Tracker Config Flow (#68712) --- homeassistant/components/nmap_tracker/strings.json | 6 +++--- .../components/nmap_tracker/translations/en.json | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nmap_tracker/strings.json b/homeassistant/components/nmap_tracker/strings.json index ed5a8cb0b05..067ae807125 100644 --- a/homeassistant/components/nmap_tracker/strings.json +++ b/homeassistant/components/nmap_tracker/strings.json @@ -23,9 +23,9 @@ "user": { "description":"Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32).", "data": { - "hosts": "Network addresses (comma seperated) to scan", + "hosts": "Network addresses (comma separated) to scan", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "exclude": "Network addresses (comma seperated) to exclude from scanning", + "exclude": "Network addresses (comma separated) to exclude from scanning", "scan_options": "Raw configurable scan options for Nmap" } } @@ -37,4 +37,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index 9ded6eae4c2..ae4175e0f14 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "Network addresses (comma seperated) to exclude from scanning", + "exclude": "Network addresses (comma separated) to exclude from scanning", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "hosts": "Network addresses (comma seperated) to scan", + "hosts": "Network addresses (comma separated) to scan", "scan_options": "Raw configurable scan options for Nmap" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." @@ -26,12 +26,11 @@ "init": { "data": { "consider_home": "Seconds to wait till marking a device tracker as not home after not being seen.", - "exclude": "Network addresses (comma seperated) to exclude from scanning", + "exclude": "Network addresses (comma separated) to exclude from scanning", "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", - "hosts": "Network addresses (comma seperated) to scan", + "hosts": "Network addresses (comma separated) to scan", "interval_seconds": "Scan interval", - "scan_options": "Raw configurable scan options for Nmap", - "track_new_devices": "Track new devices" + "scan_options": "Raw configurable scan options for Nmap" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } From a2a612c6406be4f34e43e8d9c54926f0e29c84d2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Mar 2022 06:31:22 +0200 Subject: [PATCH 0722/1054] Add enable_day to duration selector (#68705) --- homeassistant/helpers/selector.py | 6 +++++- tests/helpers/test_selector.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 1652e2110f4..3da4ccd1880 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -207,7 +207,11 @@ class DurationSelector(Selector): selector_type = "duration" - CONFIG_SCHEMA = vol.Schema({}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("enable_day"): cv.boolean, + } + ) def __call__(self, data: Any) -> dict[str, float]: """Validate the passed selection.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index e9e70f15fad..5a2bc0c0baa 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -384,6 +384,11 @@ def test_attribute_selector_schema(schema, valid_selections, invalid_selections) ({"seconds": 10},), (None, {}), ), + ( + {"enable_day": True}, + ({"seconds": 10},), + (None, {}), + ), ), ) def test_duration_selector_schema(schema, valid_selections, invalid_selections): From 0899b67578f9407c7b4143333b3fb0386a33d2eb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 27 Mar 2022 06:41:39 +0200 Subject: [PATCH 0723/1054] Update selectors with frontend changes (#68623) --- homeassistant/helpers/selector.py | 99 +++++++++++++++++++++++++++---- tests/helpers/test_selector.py | 94 +++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 3da4ccd1880..764f8ed49af 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -78,13 +78,13 @@ SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( # Integration that provided the entity vol.Optional("integration"): str, # Domain the entity belongs to - vol.Optional("domain"): str, + vol.Optional("domain"): vol.Any(str, [str]), # Device class of the entity vol.Optional("device_class"): str, } ) -DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema( +SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema( { # Integration linked to it with a config entry vol.Optional("integration"): str, @@ -94,7 +94,6 @@ DEVICE_SELECTOR_CONFIG_SCHEMA = vol.Schema( vol.Optional("model"): str, # Device has to contain entities matching this selector vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("multiple", default=False): cv.boolean, } ) @@ -140,7 +139,7 @@ class AreaSelector(Selector): CONFIG_SCHEMA = vol.Schema( { vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("device"): DEVICE_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA, vol.Optional("multiple", default=False): cv.boolean, } ) @@ -183,13 +182,82 @@ class BooleanSelector(Selector): return value +@SELECTORS.register("color_rgb") +class ColorRGBSelector(Selector): + """Selector of an RGB color value.""" + + selector_type = "color_rgb" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> list[int]: + """Validate the passed selection.""" + value: list[int] = vol.All(list, vol.ExactSequence((cv.byte,) * 3))(data) + return value + + +@SELECTORS.register("color_temp") +class ColorTempSelector(Selector): + """Selector of an color temperature.""" + + selector_type = "color_temp" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("max_mireds"): vol.Coerce(int), + vol.Optional("min_mireds"): vol.Coerce(int), + } + ) + + def __call__(self, data: Any) -> int: + """Validate the passed selection.""" + value: int = vol.All( + vol.Coerce(float), + vol.Range( + min=self.config.get("min_mireds"), + max=self.config.get("max_mireds"), + ), + )(data) + return value + + +@SELECTORS.register("date") +class DateSelector(Selector): + """Selector of a date.""" + + selector_type = "date" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + cv.date(data) + return data + + +@SELECTORS.register("datetime") +class DateTimeSelector(Selector): + """Selector of a datetime.""" + + selector_type = "datetime" + + CONFIG_SCHEMA = vol.Schema({}) + + def __call__(self, data: Any) -> Any: + """Validate the passed selection.""" + cv.datetime(data) + return data + + @SELECTORS.register("device") class DeviceSelector(Selector): """Selector of a single or list of devices.""" selector_type = "device" - CONFIG_SCHEMA = DEVICE_SELECTOR_CONFIG_SCHEMA + CONFIG_SCHEMA = SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA.extend( + {vol.Optional("multiple", default=False): cv.boolean} + ) def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" @@ -226,23 +294,34 @@ class EntitySelector(Selector): selector_type = "entity" CONFIG_SCHEMA = SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA.extend( - {vol.Optional("multiple", default=False): cv.boolean} + { + vol.Optional("exclude_entities"): [str], + vol.Optional("include_entities"): [str], + vol.Optional("multiple", default=False): cv.boolean, + } ) def __call__(self, data: Any) -> str | list[str]: """Validate the passed selection.""" + include_entities = self.config.get("include_entities") + exclude_entities = self.config.get("exclude_entities") + def validate(e_or_u: str) -> str: e_or_u = cv.entity_id_or_uuid(e_or_u) if not valid_entity_id(e_or_u): return e_or_u - if allowed_domain := self.config.get("domain"): + if allowed_domains := cv.ensure_list(self.config.get("domain")): domain = split_entity_id(e_or_u)[0] - if domain != allowed_domain: + if domain not in allowed_domains: raise vol.Invalid( f"Entity {e_or_u} belongs to domain {domain}, " - f"expected {allowed_domain}" + f"expected {allowed_domains}" ) + if include_entities: + vol.In(include_entities)(e_or_u) + if exclude_entities: + vol.NotIn(exclude_entities)(e_or_u) return e_or_u if not self.config["multiple"]: @@ -460,7 +539,7 @@ class TargetSelector(Selector): CONFIG_SCHEMA = vol.Schema( { vol.Optional("entity"): SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA, - vol.Optional("device"): DEVICE_SELECTOR_CONFIG_SCHEMA, + vol.Optional("device"): SINGLE_DEVICE_SELECTOR_CONFIG_SCHEMA, } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 5a2bc0c0baa..f1d7c83f211 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -110,6 +110,11 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections): ({}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), ({"integration": "zha"}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), ({"domain": "light"}, ("light.abc123", FAKE_UUID), (None, "sensor.abc123")), + ( + {"domain": ["light", "sensor"]}, + ("light.abc123", "sensor.abc123", FAKE_UUID), + (None, "dog.abc123"), + ), ({"device_class": "motion"}, ("sensor.abc123", FAKE_UUID), (None, "abc123")), ( {"integration": "zha", "domain": "light"}, @@ -132,6 +137,26 @@ def test_device_selector_schema(schema, valid_selections, invalid_selections): ["sensor.abc123", "light.def456"], ), ), + ( + { + "include_entities": ["sensor.abc123", "sensor.def456", "sensor.ghi789"], + "exclude_entities": ["sensor.ghi789", "sensor.jkl123"], + }, + ("sensor.abc123", FAKE_UUID), + ("sensor.ghi789", "sensor.jkl123"), + ), + ( + { + "multiple": True, + "include_entities": ["sensor.abc123", "sensor.def456", "sensor.ghi789"], + "exclude_entities": ["sensor.ghi789", "sensor.jkl123"], + }, + (["sensor.abc123", "sensor.def456"], ["sensor.abc123", FAKE_UUID]), + ( + ["sensor.abc123", "sensor.jkl123"], + ["sensor.abc123", "sensor.ghi789"], + ), + ), ), ) def test_entity_selector_schema(schema, valid_selections, invalid_selections): @@ -490,3 +515,72 @@ def test_location_selector_schema(schema, valid_selections, invalid_selections): """Test location selector.""" _test_selector("location", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ([0, 0, 0], [255, 255, 255], [0.0, 0.0, 0.0], [255.0, 255.0, 255.0]), + (None, "abc", [0, 0, "nil"], (255, 255, 255)), + ), + ), +) +def test_rgb_color_selector_schema(schema, valid_selections, invalid_selections): + """Test color_rgb selector.""" + + _test_selector("color_rgb", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + (100, 100.0), + (None, "abc", [100]), + ), + ( + {"min_mireds": 100, "max_mireds": 200}, + (100, 200), + (99, 201), + ), + ), +) +def test_color_tempselector_schema(schema, valid_selections, invalid_selections): + """Test color_temp selector.""" + + _test_selector("color_temp", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ("2022-03-24",), + (None, "abc", "00:00", "2022-03-24 00:00", "2022-03-32"), + ), + ), +) +def test_date_selector_schema(schema, valid_selections, invalid_selections): + """Test date selector.""" + + _test_selector("date", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {}, + ("2022-03-24 00:00", "2022-03-24"), + (None, "abc", "00:00", "2022-03-24 24:01"), + ), + ), +) +def test_datetime_selector_schema(schema, valid_selections, invalid_selections): + """Test datetime selector.""" + + _test_selector("datetime", schema, valid_selections, invalid_selections) From 9e6bebd27be21c3d1f1731ff80eba4c6a33f576a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 27 Mar 2022 10:54:29 +0200 Subject: [PATCH 0724/1054] Patch out Met in onboarding tests (#68732) --- tests/components/onboarding/test_views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 2ff08f0988b..3044f53cc38 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -372,13 +372,16 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): await hass.async_block_till_done() client = await hass_client() - - resp = await client.post("/api/onboarding/core_config") + with patch( + "homeassistant.components.met.async_setup_entry", return_value=True + ) as mock_setup: + resp = await client.post("/api/onboarding/core_config") assert resp.status == 200 await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 1 + assert len(hass.config_entries.async_entries("met")) == 1 + assert len(mock_setup.mock_calls) == 1 async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_client): From 4f595962b7cd0b2997f55734bdedcdcd65e7e345 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Mar 2022 02:50:13 -0700 Subject: [PATCH 0725/1054] Pause deprecation of legacy works with Nest API (#68715) --- homeassistant/components/nest/legacy/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index e0202c63567..79579a1c3df 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -109,12 +109,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: dict) -> bool: if DOMAIN not in config: return True - _LOGGER.warning( - "The Legacy Works With Nest API is deprecated and support will be removed " - "in Home Assistant Core 2022.5; See instructions for using the Smart Device " - "Management API at https://www.home-assistant.io/integrations/nest/" - ) - conf = config[DOMAIN] local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) From ea2b5a80db62744d661e26fbf0d1c527a4c371a7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 27 Mar 2022 12:29:59 +0200 Subject: [PATCH 0726/1054] Increase zwave_js add-on start attempts before timeout (#68736) --- homeassistant/components/zwave_js/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 186695d8fbb..c76e0692ad5 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -51,7 +51,7 @@ DEFAULT_URL = "ws://localhost:3000" TITLE = "Z-Wave JS" ADDON_SETUP_TIMEOUT = 5 -ADDON_SETUP_TIMEOUT_ROUNDS = 4 +ADDON_SETUP_TIMEOUT_ROUNDS = 40 CONF_EMULATE_HARDWARE = "emulate_hardware" CONF_LOG_LEVEL = "log_level" SERVER_VERSION_TIMEOUT = 10 From 53110f8cb74e3dfbb0381a21b68ef21c9528c8ca Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 27 Mar 2022 16:08:24 +0200 Subject: [PATCH 0727/1054] Update pylint to 2.13.2 (#68704) --- homeassistant/components/androidtv/media_player.py | 1 + homeassistant/components/decora/light.py | 1 + homeassistant/components/esphome/__init__.py | 1 + homeassistant/components/light/__init__.py | 8 ++++++-- homeassistant/components/limitlessled/light.py | 1 + homeassistant/components/minio/minio_helper.py | 1 + homeassistant/components/plex/media_browser.py | 6 +++--- homeassistant/components/recorder/purge.py | 8 +++++--- homeassistant/components/recorder/statistics.py | 2 +- homeassistant/components/recorder/util.py | 12 +++++++++--- homeassistant/components/spotify/media_player.py | 1 + homeassistant/components/tradfri/coordinator.py | 2 +- homeassistant/components/vlc_telnet/media_player.py | 1 + homeassistant/components/websocket_api/decorators.py | 1 + homeassistant/components/zha/core/gateway.py | 2 -- requirements_test.txt | 2 +- 16 files changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 8e4b04cb507..19cae59a1b4 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -187,6 +187,7 @@ def adb_decorator(override_available=False): @functools.wraps(func) async def _adb_exception_catcher(self, *args, **kwargs): """Call an ADB-related method and catch exceptions.""" + # pylint: disable=protected-access if not self.available and not override_available: return None diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index f4b48ccb01b..e29222c8eee 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -70,6 +70,7 @@ def retry(method): "Decora connect error for device %s. Reconnecting", device.name, ) + # pylint: disable=protected-access device._switch.connect() return wrapper_retry diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index eba60a3d5a1..fd9b5dfd6d2 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -633,6 +633,7 @@ def esphome_state_property(func: _PropT) -> _PropT: @property # type: ignore[misc] @functools.wraps(func) def _wrapper(self): # type: ignore[no-untyped-def] + # pylint: disable=protected-access if not self._has_state: return None val = func(self) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 5e8bf473da3..36588704c85 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -295,7 +295,9 @@ def filter_turn_on_params(light, params): if not supported_features & SUPPORT_WHITE_VALUE: params.pop(ATTR_WHITE_VALUE, None) - supported_color_modes = light._light_internal_supported_color_modes + supported_color_modes = ( + light._light_internal_supported_color_modes # pylint:disable=protected-access + ) if not brightness_supported(supported_color_modes): params.pop(ATTR_BRIGHTNESS, None) if COLOR_MODE_COLOR_TEMP not in supported_color_modes: @@ -366,7 +368,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ): profiles.apply_default(light.entity_id, light.is_on, params) - legacy_supported_color_modes = light._light_internal_supported_color_modes + legacy_supported_color_modes = ( + light._light_internal_supported_color_modes # pylint: disable=protected-access + ) supported_color_modes = light.supported_color_modes # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W # for legacy lights diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 35bfafd602d..e45a4a2ced0 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -189,6 +189,7 @@ def state(new_state): def wrapper(self, **kwargs): """Wrap a group state change.""" + # pylint: disable=protected-access pipeline = Pipeline() transition_time = DEFAULT_TRANSITION diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index cc7b7f8a3e7..4f10da10998 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -42,6 +42,7 @@ def get_minio_notification_response( ): """Start listening to minio events. Copied from minio-py.""" query = {"prefix": prefix, "suffix": suffix, "events": events} + # pylint: disable=protected-access return minio_client._url_open( "GET", bucket_name=bucket_name, query=query, preload_content=False ) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 59895c0d613..4b2bb502d69 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -329,7 +329,7 @@ def library_section_payload(section): children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE] except KeyError as err: raise UnknownMediaType(f"Unknown type received: {section.TYPE}") from err - server_id = section._server.machineIdentifier + server_id = section._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, @@ -362,7 +362,7 @@ def hub_payload(hub): media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}" else: media_content_id = f"server/{hub.hubIdentifier}" - server_id = hub._server.machineIdentifier + server_id = hub._server.machineIdentifier # pylint: disable=protected-access payload = { "title": hub.title, "media_class": MEDIA_CLASS_DIRECTORY, @@ -376,7 +376,7 @@ def hub_payload(hub): def station_payload(station): """Create response payload for a music station.""" - server_id = station._server.machineIdentifier + server_id = station._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=station.title, media_class=ITEM_TYPE_MEDIA_CLASS[station.type], diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 532aef3c53d..a15d22810f4 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -204,7 +204,7 @@ def _evict_purged_states_from_old_states_cache( ) -> None: """Evict purged states from the old states cache.""" # Make a map from old_state_id to entity_id - old_states = instance._old_states + old_states = instance._old_states # pylint: disable=protected-access old_state_reversed = { old_state.state_id: entity_id for entity_id, old_state in old_states.items() @@ -221,7 +221,9 @@ def _evict_purged_attributes_from_attributes_cache( ) -> None: """Evict purged attribute ids from the attribute ids cache.""" # Make a map from attributes_id to the attributes json - state_attributes_ids = instance._state_attributes_ids + state_attributes_ids = ( + instance._state_attributes_ids # pylint: disable=protected-access + ) state_attributes_ids_reversed = { attributes_id: attributes for attributes, attributes_id in state_attributes_ids.items() @@ -376,7 +378,7 @@ def _purge_filtered_events( _purge_event_ids(session, event_ids) if EVENT_STATE_CHANGED in excluded_event_types: session.query(StateAttributes).delete(synchronize_session=False) - instance._state_attributes_ids = {} + instance._state_attributes_ids = {} # pylint: disable=protected-access @retryable_database_job("purge") diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2ad2530fb83..f01190097df 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -448,7 +448,7 @@ def compile_hourly_statistics( } # Get last hour's last sum - if instance._db_supports_row_number: + if instance._db_supports_row_number: # pylint: disable=[protected-access] subquery = ( session.query(*QUERY_STATISTICS_SUMMARY_SUM) .filter(StatisticsShortTerm.start >= bindparam("start_time")) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9d12f1d7473..487b8dd22f7 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -359,7 +359,9 @@ def setup_connection_for_dialect( version = _extract_version_from_server_response(version_string) if version and version < MIN_VERSION_SQLITE_ROWNUM: - instance._db_supports_row_number = False + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) if not version or version < MIN_VERSION_SQLITE: _warn_unsupported_version( version or version_string, "SQLite", MIN_VERSION_SQLITE @@ -381,14 +383,18 @@ def setup_connection_for_dialect( if is_maria_db: if version and version < MIN_VERSION_MARIA_DB_ROWNUM: - instance._db_supports_row_number = False + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) if not version or version < MIN_VERSION_MARIA_DB: _warn_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) else: if version and version < MIN_VERSION_MYSQL_ROWNUM: - instance._db_supports_row_number = False + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) if not version or version < MIN_VERSION_MYSQL: _warn_unsupported_version( version or version_string, "MySQL", MIN_VERSION_MYSQL diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 3e77c5893c0..2b62fdd78c4 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -96,6 +96,7 @@ def spotify_exception_handler(func): """ def wrapper(self, *args, **kwargs): + # pylint: disable=protected-access try: result = func(self, *args, **kwargs) self._attr_available = True diff --git a/homeassistant/components/tradfri/coordinator.py b/homeassistant/components/tradfri/coordinator.py index 039ff34c9f7..5a516e8f46e 100644 --- a/homeassistant/components/tradfri/coordinator.py +++ b/homeassistant/components/tradfri/coordinator.py @@ -76,7 +76,7 @@ class TradfriDeviceDataUpdateCoordinator(DataUpdateCoordinator[Device]): if self._exception: exc = self._exception self._exception = None # Clear stored exception - raise exc # pylint: disable-msg=raising-bad-type + raise exc except RequestError as err: raise UpdateFailed(f"Error communicating with API: {err}.") from err diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 5b7c5fbb524..99b37f97e0c 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -88,6 +88,7 @@ def catch_vlc_errors( except CommandError as err: LOGGER.error("Command error: %s", err) except ConnectError as err: + # pylint: disable=protected-access if self._available: LOGGER.error("Connection error: %s", err) self._available = False diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 223c09eb5fd..296271c7cfd 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -132,6 +132,7 @@ def websocket_command( def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" + # pylint: disable=protected-access func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] func._ws_command = command # type: ignore[attr-defined] return func diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 18213c396cc..6c600bf93d6 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -98,8 +98,6 @@ if TYPE_CHECKING: from ..entity import ZhaEntity from .store import ZhaStorage - # pylint: disable-next=broken-collections-callable - # Safe inside TYPE_CHECKING block _LogFilterType = Union[Filter, Callable[[LogRecord], int]] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_test.txt b/requirements_test.txt index 34a4936b3d8..02405093237 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.942 pre-commit==2.17.0 -pylint==2.13.0 +pylint==2.13.2 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From f05a6826de52f25f9b7766cb6cf29666ac65aba4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Mar 2022 08:08:28 -0700 Subject: [PATCH 0728/1054] Add additional type hints for calendar integration (#68660) --- homeassistant/components/calendar/__init__.py | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 3cec3792612..66a3034938e 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -1,11 +1,11 @@ """Support for Google Calendar event device sensors.""" from __future__ import annotations -from datetime import timedelta +import datetime from http import HTTPStatus import logging import re -from typing import cast, final +from typing import Any, cast, final from aiohttp import web @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" -SCAN_INTERVAL = timedelta(seconds=60) +SCAN_INTERVAL = datetime.timedelta(seconds=60) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -62,18 +62,22 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) -def get_date(date): +def get_date(date: dict[str, Any]) -> datetime.datetime: """Get the dateTime from date or dateTime as a local.""" if "date" in date: + parsed_date = dt.parse_date(date["date"]) + assert parsed_date return dt.start_of_local_day( - dt.dt.datetime.combine(dt.parse_date(date["date"]), dt.dt.time.min) + datetime.datetime.combine(parsed_date, datetime.time.min) ) - return dt.as_local(dt.parse_datetime(date["dateTime"])) + parsed_datetime = dt.parse_datetime(date["dateTime"]) + assert parsed_datetime + return dt.as_local(parsed_datetime) -def normalize_event(event): +def normalize_event(event: dict[str, Any]) -> dict[str, Any]: """Normalize a calendar event.""" - normalized_event = {} + normalized_event: dict[str, Any] = {} start = event.get("start") end = event.get("end") @@ -97,7 +101,7 @@ def normalize_event(event): return normalized_event -def calculate_offset(event, offset): +def calculate_offset(event: dict[str, Any], offset: str) -> dict[str, Any]: """Calculate event offset. Return the updated event with the offset_time included. @@ -119,32 +123,33 @@ def calculate_offset(event, offset): summary = (summary[: search.start()] + summary[search.end() :]).strip() event["summary"] = summary else: - offset_time = dt.dt.timedelta() # default it + offset_time = datetime.timedelta() # default it event["offset_time"] = offset_time return event -def is_offset_reached(event): +def is_offset_reached(event: dict[str, Any]) -> bool: """Have we reached the offset time specified in the event title.""" start = get_date(event["start"]) - if start is None or event["offset_time"] == dt.dt.timedelta(): + offset_time: datetime.timedelta = event["offset_time"] + if start is None or offset_time == datetime.timedelta(): return False - return start + event["offset_time"] <= dt.now(start.tzinfo) + return start + offset_time <= dt.now(start.tzinfo) class CalendarEventDevice(Entity): """Base class for calendar event entities.""" @property - def event(self): + def event(self) -> dict[str, Any] | None: """Return the next upcoming event.""" raise NotImplementedError() @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any] | None: """Return the entity state attributes.""" if (event := self.event) is None: return None @@ -160,7 +165,7 @@ class CalendarEventDevice(Entity): } @property - def state(self): + def state(self) -> str | None: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF @@ -179,7 +184,12 @@ class CalendarEventDevice(Entity): return STATE_OFF - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[dict[str, Any]]: """Return calendar events within a datetime range.""" raise NotImplementedError() @@ -194,18 +204,21 @@ class CalendarEventView(http.HomeAssistantView): """Initialize calendar view.""" self.component = component - async def get(self, request, entity_id): + async def get(self, request: web.Request, entity_id: str) -> web.Response: """Return calendar events.""" entity = self.component.get_entity(entity_id) start = request.query.get("start") end = request.query.get("end") - if None in (start, end, entity): + if start is None or end is None or entity is None: return web.Response(status=HTTPStatus.BAD_REQUEST) + assert isinstance(entity, CalendarEventDevice) try: start_date = dt.parse_datetime(start) end_date = dt.parse_datetime(end) except (ValueError, AttributeError): return web.Response(status=HTTPStatus.BAD_REQUEST) + if start_date is None or end_date is None: + return web.Response(status=HTTPStatus.BAD_REQUEST) event_list = await entity.async_get_events( request.app["hass"], start_date, end_date ) From 945028d43d7bad4c3702037d57d8c9005f00edbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 27 Mar 2022 18:16:45 +0300 Subject: [PATCH 0729/1054] Update Huawei LTE config entry data on successful reconfig (#68727) --- homeassistant/components/huawei_lte/config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index be2a149b4d5..e8a02db4f1c 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -183,15 +183,15 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): info, wlan_settings = await self.hass.async_add_executor_job(get_device_info) await self.hass.async_add_executor_job(logout) + user_input[CONF_MAC] = get_device_macs(info, wlan_settings) + if not self.unique_id: if serial_number := info.get("SerialNumber"): await self.async_set_unique_id(serial_number) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=user_input) else: await self._async_handle_discovery_without_unique_id() - user_input[CONF_MAC] = get_device_macs(info, wlan_settings) - title = ( self.context.get("title_placeholders", {}).get(CONF_NAME) or info.get("DeviceName") # device.information From f61c9111741df4e6c3059be522e169e622926e1a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 27 Mar 2022 10:02:19 -0700 Subject: [PATCH 0730/1054] Remove some offset complexity from calendar event (#68724) Simplify the calendar offset calculations to no longer update the event dictionary using extra fields. calculate_offset is renamed to extract_offset and the integration is responsible for overwriting the summary text. This is in prepration for: - Improved calendar event typing, removing unnecessary offset_reached field - Calendar triggers which will remove offsets anyway --- homeassistant/components/caldav/calendar.py | 9 ++++--- homeassistant/components/calendar/__init__.py | 26 +++++++------------ homeassistant/components/google/calendar.py | 12 ++++++--- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index f44a59f18eb..1955751d4fd 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -13,7 +13,7 @@ from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, PLATFORM_SCHEMA, CalendarEventDevice, - calculate_offset, + extract_offset, get_date, is_offset_reached, ) @@ -147,9 +147,12 @@ class WebDavCalendarEventDevice(CalendarEventDevice): if event is None: self._event = event return - event = calculate_offset(event, OFFSET) + (summary, offset) = extract_offset(event["summary"], OFFSET) + event["summary"] = summary self._event = event - self._attr_extra_state_attributes = {"offset_reached": is_offset_reached(event)} + self._attr_extra_state_attributes = { + "offset_reached": is_offset_reached(get_date(event["start"]), offset) + } class WebDavCalendarData: diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 66a3034938e..4449084373b 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -101,15 +101,14 @@ def normalize_event(event: dict[str, Any]) -> dict[str, Any]: return normalized_event -def calculate_offset(event: dict[str, Any], offset: str) -> dict[str, Any]: - """Calculate event offset. +def extract_offset(summary: str, offset_prefix: str) -> tuple[str, datetime.timedelta]: + """Extract the offset from the event summary. - Return the updated event with the offset_time included. + Return a tuple with the updated event summary and offset time. """ - summary = event.get("summary", "") # check if we have an offset tag in the message # time is HH:MM or MM - reg = f"{offset}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)" + reg = f"{offset_prefix}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)" search = re.search(reg, summary) if search and search.group(1): time = search.group(1) @@ -121,21 +120,16 @@ def calculate_offset(event: dict[str, Any], offset: str) -> dict[str, Any]: offset_time = time_period_str(time) summary = (summary[: search.start()] + summary[search.end() :]).strip() - event["summary"] = summary - else: - offset_time = datetime.timedelta() # default it - - event["offset_time"] = offset_time - return event + return (summary, offset_time) + return (summary, datetime.timedelta()) -def is_offset_reached(event: dict[str, Any]) -> bool: +def is_offset_reached( + start: datetime.datetime, offset_time: datetime.timedelta +) -> bool: """Have we reached the offset time specified in the event title.""" - start = get_date(event["start"]) - offset_time: datetime.timedelta = event["offset_time"] - if start is None or offset_time == datetime.timedelta(): + if offset_time == datetime.timedelta(): return False - return start + offset_time <= dt.now(start.tzinfo) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 666096fd8c3..d04d913ae67 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -11,7 +11,8 @@ from httplib2 import ServerNotFoundError from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, CalendarEventDevice, - calculate_offset, + extract_offset, + get_date, is_offset_reached, ) from homeassistant.config_entries import ConfigEntry @@ -178,9 +179,12 @@ class GoogleCalendarEventDevice(CalendarEventDevice): _LOGGER.error("Unable to connect to Google: %s", err) return - # Pick the first visible event. Make a copy since calculate_offset mutates the event + # Pick the first visible event and apply offset calculations. valid_items = filter(self._event_filter, items) self._event = copy.deepcopy(next(valid_items, None)) if self._event: - calculate_offset(self._event, self._offset) - self._offset_reached = is_offset_reached(self._event) + (summary, offset) = extract_offset(self._event["summary"], self._offset) + self._event["summary"] = summary + self._offset_reached = is_offset_reached( + get_date(self._event["start"]), offset + ) From cffc588c6d2108f1b5bf216d03de4c1b71dbd0ab Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 27 Mar 2022 10:49:28 -0700 Subject: [PATCH 0731/1054] Bump adb-shell to 0.4.2 (#68742) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index bc082938eeb..729cac082c7 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.4.0", + "adb-shell[async]==0.4.2", "androidtv[async]==0.0.66", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 53e086b54a2..329671ed5d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -77,7 +77,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.0 +adb-shell[async]==0.4.2 # homeassistant.components.alarmdecoder adext==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59f774e12d1..85ea66b0090 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,7 +61,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.0 +adb-shell[async]==0.4.2 # homeassistant.components.alarmdecoder adext==0.4.2 From b5401ccc4a482b14f03008765b0a0ad6ad6664a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 27 Mar 2022 20:59:15 +0200 Subject: [PATCH 0732/1054] Add Airzone climate platform (#67924) Co-authored-by: J. Nick Koston --- homeassistant/components/airzone/__init__.py | 2 +- homeassistant/components/airzone/climate.py | 190 +++++++++++++++ homeassistant/components/airzone/const.py | 1 + tests/components/airzone/test_climate.py | 238 +++++++++++++++++++ tests/components/airzone/util.py | 14 +- 5 files changed, 437 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/airzone/climate.py create mode 100644 tests/components/airzone/test_climate.py diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index c1fd6deddf2..5d5350d7a29 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER from .coordinator import AirzoneUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py new file mode 100644 index 00000000000..763603a758b --- /dev/null +++ b/homeassistant/components/airzone/climate.py @@ -0,0 +1,190 @@ +"""Support for the Airzone climate.""" +from __future__ import annotations + +import logging +from typing import Final + +from aioairzone.common import OperationMode +from aioairzone.const import ( + API_MODE, + API_ON, + API_SET_POINT, + API_SYSTEM_ID, + API_ZONE_ID, + AZD_DEMAND, + AZD_HUMIDITY, + AZD_MODE, + AZD_MODES, + AZD_NAME, + AZD_ON, + AZD_TEMP, + AZD_TEMP_MAX, + AZD_TEMP_MIN, + AZD_TEMP_SET, + AZD_TEMP_UNIT, + AZD_ZONES, +) +from aioairzone.exceptions import AirzoneError +from aiohttp.client_exceptions import ClientConnectorError + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirzoneEntity +from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS +from .coordinator import AirzoneUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationMode, str]] = { + OperationMode.STOP: CURRENT_HVAC_OFF, + OperationMode.COOLING: CURRENT_HVAC_COOL, + OperationMode.HEATING: CURRENT_HVAC_HEAT, + OperationMode.FAN: CURRENT_HVAC_FAN, + OperationMode.DRY: CURRENT_HVAC_DRY, +} +HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, str]] = { + OperationMode.STOP: HVAC_MODE_OFF, + OperationMode.COOLING: HVAC_MODE_COOL, + OperationMode.HEATING: HVAC_MODE_HEAT, + OperationMode.FAN: HVAC_MODE_FAN_ONLY, + OperationMode.DRY: HVAC_MODE_DRY, + OperationMode.AUTO: HVAC_MODE_HEAT_COOL, +} +HVAC_MODE_HASS_TO_LIB: Final[dict[str, OperationMode]] = { + HVAC_MODE_OFF: OperationMode.STOP, + HVAC_MODE_COOL: OperationMode.COOLING, + HVAC_MODE_HEAT: OperationMode.HEATING, + HVAC_MODE_FAN_ONLY: OperationMode.FAN, + HVAC_MODE_DRY: OperationMode.DRY, + HVAC_MODE_HEAT_COOL: OperationMode.AUTO, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + AirzoneClimate( + coordinator, + entry, + system_zone_id, + zone_data, + ) + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items() + ) + + +class AirzoneClimate(AirzoneEntity, ClimateEntity): + """Define an Airzone sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict, + ) -> None: + """Initialize Airzone climate entity.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + self._attr_name = f"{zone_data[AZD_NAME]}" + self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}" + self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE + self._attr_target_temperature_step = API_TEMPERATURE_STEP + self._attr_max_temp = self.get_zone_value(AZD_TEMP_MAX) + self._attr_min_temp = self.get_zone_value(AZD_TEMP_MIN) + self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ + self.get_zone_value(AZD_TEMP_UNIT) + ] + self._attr_hvac_modes = [ + HVAC_MODE_LIB_TO_HASS[mode] + for mode in self.get_zone_value(AZD_MODES) + or [self.get_zone_value(AZD_MODE)] + ] + if HVAC_MODE_OFF not in self._attr_hvac_modes: + self._attr_hvac_modes.append(HVAC_MODE_OFF) + self._async_update_attrs() + + async def _async_update_hvac_params(self, params) -> None: + """Send HVAC parameters to API.""" + try: + await self.coordinator.airzone.put_hvac(params) + except (AirzoneError, ClientConnectorError) as error: + raise HomeAssistantError( + f"Failed to set zone {self.name}: {error}" + ) from error + else: + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set hvac mode.""" + params = { + API_SYSTEM_ID: self.system_id, + API_ZONE_ID: self.zone_id, + } + if hvac_mode == HVAC_MODE_OFF: + params[API_ON] = 0 + else: + if self.get_zone_value(AZD_MODES): + mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] + if mode != self.get_zone_value(AZD_MODE): + params[API_MODE] = mode + params[API_ON] = 1 + _LOGGER.debug("Set hvac_mode=%s params=%s", hvac_mode, params) + await self._async_update_hvac_params(params) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + params = { + API_SYSTEM_ID: self.system_id, + API_ZONE_ID: self.zone_id, + API_SET_POINT: temp, + } + _LOGGER.debug("Set temp=%s params=%s", temp, params) + await self._async_update_hvac_params(params) + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update climate attributes.""" + self._attr_current_temperature = self.get_zone_value(AZD_TEMP) + self._attr_current_humidity = self.get_zone_value(AZD_HUMIDITY) + if self.get_zone_value(AZD_ON): + mode = self.get_zone_value(AZD_MODE) + self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[mode] + if self.get_zone_value(AZD_DEMAND): + self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[mode] + else: + self._attr_hvac_action = CURRENT_HVAC_IDLE + else: + self._attr_hvac_action = CURRENT_HVAC_OFF + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_target_temperature = self.get_zone_value(AZD_TEMP_SET) diff --git a/homeassistant/components/airzone/const.py b/homeassistant/components/airzone/const.py index f1818334914..094e2319476 100644 --- a/homeassistant/components/airzone/const.py +++ b/homeassistant/components/airzone/const.py @@ -10,6 +10,7 @@ DOMAIN: Final = "airzone" MANUFACTURER: Final = "Airzone" AIOAIRZONE_DEVICE_TIMEOUT_SEC: Final = 10 +API_TEMPERATURE_STEP: Final = 0.5 DEFAULT_LOCAL_API_PORT: Final = 3000 TEMP_UNIT_LIB_TO_HASS: Final[dict[TemperatureUnit, str]] = { diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py new file mode 100644 index 00000000000..bc88321720c --- /dev/null +++ b/tests/components/airzone/test_climate.py @@ -0,0 +1,238 @@ +"""The climate tests for the Airzone platform.""" + +from unittest.mock import patch + +from aioairzone.common import OperationMode +from aioairzone.const import ( + API_DATA, + API_MODE, + API_ON, + API_SET_POINT, + API_SYSTEM_ID, + API_ZONE_ID, +) +from aioairzone.exceptions import AirzoneError + +from homeassistant.components.airzone.const import API_TEMPERATURE_STEP +from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_STEP, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + DOMAIN as CLIMATE_DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + + +async def test_airzone_create_climates(hass): + """Test creation of climates.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.despacho") + assert state.state == HVAC_MODE_OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 36 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 21.2 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.4 + + state = hass.states.get("climate.dorm_1") + assert state.state == HVAC_MODE_HEAT + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 35 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 20.8 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_IDLE + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.3 + + state = hass.states.get("climate.dorm_2") + assert state.state == HVAC_MODE_OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 40 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 20.5 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.5 + + state = hass.states.get("climate.dorm_ppal") + assert state.state == HVAC_MODE_HEAT + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 39 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 21.1 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_HEAT + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.2 + + state = hass.states.get("climate.salon") + assert state.state == HVAC_MODE_OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 34 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 19.6 + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 19.1 + + +async def test_airzone_climate_set_hvac_mode(hass): + """Test setting the target temperature.""" + + await async_init_integration(hass) + + HVAC_MOCK = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_MODE: OperationMode.COOLING.value, + API_ON: 1, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVAC_MODE_COOL, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVAC_MODE_COOL + + HVAC_MOCK_2 = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_ON: 0, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK_2, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVAC_MODE_OFF, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVAC_MODE_OFF + + +async def test_airzone_climate_set_temp(hass): + """Test setting the target temperature.""" + + HVAC_MOCK = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 5, + API_SET_POINT: 20.5, + } + ] + } + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.http_request", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.dorm_2", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.dorm_2") + assert state.attributes.get(ATTR_TEMPERATURE) == 20.5 + + +async def test_airzone_climate_set_temp_error(hass): + """Test error when setting the target temperature.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + side_effect=AirzoneError, + ): + try: + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.dorm_2", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + except HomeAssistantError: + pass + + state = hass.states.get("climate.dorm_2") + assert state.attributes.get(ATTR_TEMPERATURE) == 19.5 diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index bc0332c959c..745daac9269 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -48,7 +48,7 @@ HVAC_MOCK = { API_ON: 0, API_MAX_TEMP: 30, API_MIN_TEMP: 15, - API_SET_POINT: 19.5, + API_SET_POINT: 19.1, API_ROOM_TEMP: 19.6, API_MODES: [1, 4, 2, 3, 5], API_MODE: 3, @@ -66,10 +66,10 @@ HVAC_MOCK = { API_SYSTEM_ID: 1, API_ZONE_ID: 2, API_NAME: "Dorm Ppal", - API_ON: 0, + API_ON: 1, API_MAX_TEMP: 30, API_MIN_TEMP: 15, - API_SET_POINT: 19.5, + API_SET_POINT: 19.2, API_ROOM_TEMP: 21.1, API_MODE: 3, API_COLD_STAGES: 1, @@ -79,17 +79,17 @@ HVAC_MOCK = { API_HUMIDITY: 39, API_UNITS: 0, API_ERRORS: [], - API_AIR_DEMAND: 0, + API_AIR_DEMAND: 1, API_FLOOR_DEMAND: 0, }, { API_SYSTEM_ID: 1, API_ZONE_ID: 3, API_NAME: "Dorm #1", - API_ON: 0, + API_ON: 1, API_MAX_TEMP: 30, API_MIN_TEMP: 15, - API_SET_POINT: 19.5, + API_SET_POINT: 19.3, API_ROOM_TEMP: 20.8, API_MODE: 3, API_COLD_STAGES: 1, @@ -109,7 +109,7 @@ HVAC_MOCK = { API_ON: 0, API_MAX_TEMP: 86, API_MIN_TEMP: 59, - API_SET_POINT: 67.1, + API_SET_POINT: 66.92, API_ROOM_TEMP: 70.16, API_MODE: 3, API_COLD_STAGES: 1, From cc75cebfc5b3e9316cdbaf82c5c72437521f819b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Mar 2022 10:30:45 -1000 Subject: [PATCH 0733/1054] Add support for setting up encrypted samsung tvs from config flow (#68717) Co-authored-by: epenet --- .../components/samsungtv/__init__.py | 2 +- homeassistant/components/samsungtv/bridge.py | 70 +- .../components/samsungtv/config_flow.py | 249 ++++-- homeassistant/components/samsungtv/const.py | 9 +- .../components/samsungtv/manifest.json | 8 + .../components/samsungtv/strings.json | 10 +- .../components/samsungtv/translations/en.json | 10 +- homeassistant/generated/ssdp.py | 8 + tests/components/samsungtv/__init__.py | 2 +- tests/components/samsungtv/conftest.py | 50 +- .../components/samsungtv/test_config_flow.py | 758 ++++++++++++++---- .../components/samsungtv/test_diagnostics.py | 2 +- tests/components/samsungtv/test_init.py | 13 +- .../components/samsungtv/test_media_player.py | 5 + 14 files changed, 923 insertions(+), 273 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 470d7e80584..4363e7d8c44 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -173,7 +173,7 @@ async def _async_create_bridge_with_updated_data( else: # When we imported from yaml we didn't setup the method # because we didn't know it - port, method, info = await async_get_device_info(hass, None, host) + _result, port, method, info = await async_get_device_info(hass, host) load_info_attempted = True if not port or not method: raise ConfigEntryNotReady( diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index b89c76f028e..4e53b0fd0a5 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -23,7 +23,12 @@ from samsungtvws.event import ( MS_ERROR_EVENT, parse_installed_app, ) -from samsungtvws.exceptions import ConnectionFailure, HttpApiError +from samsungtvws.exceptions import ( + ConnectionFailure, + HttpApiError, + ResponseError, + UnauthorizedError, +) from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException @@ -54,6 +59,7 @@ from .const import ( RESULT_CANNOT_CONNECT, RESULT_NOT_SUPPORTED, RESULT_SUCCESS, + SUCCESSFUL_RESULTS, TIMEOUT_REQUEST, TIMEOUT_WEBSOCKET, VALUE_CONF_ID, @@ -66,6 +72,8 @@ KEY_PRESS_TIMEOUT = 1.2 ENCRYPTED_MODEL_USES_POWER_OFF = {"H6400"} ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"} +REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError) + def mac_from_device_info(info: dict[str, Any]) -> str | None: """Extract the mac address from the device info.""" @@ -76,36 +84,39 @@ def mac_from_device_info(info: dict[str, Any]) -> str | None: async def async_get_device_info( hass: HomeAssistant, - bridge: SamsungTVBridge | None, host: str, -) -> tuple[int | None, str | None, dict[str, Any] | None]: +) -> tuple[str, int | None, str | None, dict[str, Any] | None]: """Fetch the port, method, and device info.""" - # Bridge is defined - if bridge and bridge.port: - return bridge.port, bridge.method, await bridge.async_device_info() - - # Try websocket ports + # Try the websocket ssl and non-ssl ports for port in WEBSOCKET_PORTS: bridge = SamsungTVBridge.get_bridge(hass, METHOD_WEBSOCKET, host, port) if info := await bridge.async_device_info(): - return port, METHOD_WEBSOCKET, info - - # Try encrypted websocket port - bridge = SamsungTVBridge.get_bridge( - hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT - ) - result = await bridge.async_try_connect() - if result == RESULT_SUCCESS: - return port, METHOD_ENCRYPTED_WEBSOCKET, await bridge.async_device_info() + LOGGER.debug( + "Fetching rest info via %s was successful: %s, checking for encrypted", + port, + info, + ) + encrypted_bridge = SamsungTVEncryptedBridge( + hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT + ) + result = await encrypted_bridge.async_try_connect() + if result != RESULT_CANNOT_CONNECT: + return ( + result, + ENCRYPTED_WEBSOCKET_PORT, + METHOD_ENCRYPTED_WEBSOCKET, + info, + ) + return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info # Try legacy port bridge = SamsungTVBridge.get_bridge(hass, METHOD_LEGACY, host, LEGACY_PORT) result = await bridge.async_try_connect() - if result in (RESULT_SUCCESS, RESULT_AUTH_MISSING): - return LEGACY_PORT, METHOD_LEGACY, await bridge.async_device_info() + if result in SUCCESSFUL_RESULTS: + return result, LEGACY_PORT, METHOD_LEGACY, await bridge.async_device_info() # Failed to get info - return None, None, None + return result, None, None, None class SamsungTVBridge(ABC): @@ -433,8 +444,11 @@ class SamsungTVWSBridge(SamsungTVBridge): "Working but unsupported config: %s, error: %s", config, err ) result = RESULT_NOT_SUPPORTED - except (OSError, AsyncioTimeoutError, ConnectionFailure) as err: - LOGGER.debug("Failing config: %s, error: %s", config, err) + except UnauthorizedError as err: + LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) + return RESULT_AUTH_MISSING + except (ConnectionFailure, OSError, AsyncioTimeoutError) as err: + LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) # pylint: disable=useless-else-on-loop else: if result: @@ -453,7 +467,7 @@ class SamsungTVWSBridge(SamsungTVBridge): timeout=TIMEOUT_WEBSOCKET, ) - with contextlib.suppress(HttpApiError, AsyncioTimeoutError): + with contextlib.suppress(*REST_EXCEPTIONS): device_info: dict[str, Any] = await rest_api.rest_device_info() LOGGER.debug("Device info on %s is: %s", self.host, device_info) self._device_info = device_info @@ -654,8 +668,7 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): CONF_HOST: self.host, CONF_METHOD: self.method, CONF_PORT: self.port, - # We need this high timeout because waiting for auth popup is just an open socket - CONF_TIMEOUT: TIMEOUT_REQUEST, + CONF_TIMEOUT: TIMEOUT_WEBSOCKET, } try: @@ -669,13 +682,14 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): timeout=TIMEOUT_REQUEST, ) as remote: await remote.start_listening() - LOGGER.debug("Working config: %s", config) - return RESULT_SUCCESS except WebSocketException as err: LOGGER.debug("Working but unsupported config: %s, error: %s", config, err) return RESULT_NOT_SUPPORTED except (OSError, AsyncioTimeoutError, ConnectionFailure) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) + else: + LOGGER.debug("Working config: %s", config) + return RESULT_SUCCESS return RESULT_CANNOT_CONNECT @@ -696,7 +710,7 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): timeout=TIMEOUT_WEBSOCKET, ) - with contextlib.suppress(HttpApiError, AsyncioTimeoutError): + with contextlib.suppress(*REST_EXCEPTIONS): device_info: dict[str, Any] = await rest_api.rest_device_info() LOGGER.debug("Device info on %s is: %s", self.host, device_info) self._device_info = device_info diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 29f46c1cd33..0b87e38b00a 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -30,6 +30,7 @@ from .const import ( CONF_MANUFACTURER, CONF_MODEL, CONF_SESSION_ID, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, ENCRYPTED_WEBSOCKET_PORT, @@ -44,20 +45,34 @@ from .const import ( RESULT_NOT_SUPPORTED, RESULT_SUCCESS, RESULT_UNKNOWN_HOST, + SUCCESSFUL_RESULTS, + UPNP_SVC_RENDERINGCONTROL, WEBSOCKET_PORTS, ) DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) -SUPPORTED_METHODS = [METHOD_LEGACY, METHOD_WEBSOCKET] def _strip_uuid(udn: str) -> str: return udn[5:] if udn.startswith("uuid:") else udn -def _entry_is_complete(entry: config_entries.ConfigEntry) -> bool: - """Return True if the config entry information is complete.""" - return bool(entry.unique_id and entry.data.get(CONF_MAC)) +def _entry_is_complete( + entry: config_entries.ConfigEntry, ssdp_rendering_control_location: str | None +) -> bool: + """Return True if the config entry information is complete. + + If we do not have an ssdp location we consider it complete + as some TVs will not support SSDP/UPNP + """ + return bool( + entry.unique_id + and entry.data.get(CONF_MAC) + and ( + not ssdp_rendering_control_location + or entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) + ) + ) class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -72,22 +87,22 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._mac: str | None = None self._udn: str | None = None self._upnp_udn: str | None = None + self._ssdp_rendering_control_location: str | None = None self._manufacturer: str | None = None self._model: str | None = None + self._connect_result: str | None = None + self._method: str | None = None self._name: str | None = None self._title: str = "" self._id: int | None = None self._bridge: SamsungTVBridge | None = None self._device_info: dict[str, Any] | None = None - self._encrypted_authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = ( - None - ) + self._authenticator: SamsungTVEncryptedWSAsyncAuthenticator | None = None - def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: - """Get device entry.""" - assert self._bridge - - data = { + def _base_config_entry(self) -> dict[str, Any]: + """Generate the base config entry without the method.""" + assert self._bridge is not None + return { CONF_HOST: self._host, CONF_MAC: self._mac, CONF_MANUFACTURER: self._manufacturer or DEFAULT_MANUFACTURER, @@ -95,7 +110,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_MODEL: self._model, CONF_NAME: self._name, CONF_PORT: self._bridge.port, + CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location, } + + def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: + """Get device entry.""" + assert self._bridge + data = self._base_config_entry() if self._bridge.token: data[CONF_TOKEN] = self._bridge.token return self.async_create_entry( @@ -115,48 +136,66 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> None: """Set the unique id from the udn.""" assert self._host is not None - await self.async_set_unique_id(self._udn, raise_on_progress=raise_on_progress) + # Set the unique id without raising on progress in case + # there are two SSDP flows with for each ST + await self.async_set_unique_id(self._udn, raise_on_progress=False) if ( entry := self._async_update_existing_matching_entry() - ) and _entry_is_complete(entry): + ) and _entry_is_complete(entry, self._ssdp_rendering_control_location): raise data_entry_flow.AbortFlow("already_configured") + # Now that we have updated the config entry, we can raise + # if another one is progressing + if raise_on_progress: + await self.async_set_unique_id(self._udn, raise_on_progress=True) def _async_update_and_abort_for_matching_unique_id(self) -> None: """Abort and update host and mac if we have it.""" updates = {CONF_HOST: self._host} if self._mac: updates[CONF_MAC] = self._mac + if self._ssdp_rendering_control_location: + updates[ + CONF_SSDP_RENDERING_CONTROL_LOCATION + ] = self._ssdp_rendering_control_location self._abort_if_unique_id_configured(updates=updates) - async def _try_connect(self) -> None: - """Try to connect and check auth.""" - for method in SUPPORTED_METHODS: - self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) - result = await self._bridge.async_try_connect() - if result == RESULT_SUCCESS: - return - if result != RESULT_CANNOT_CONNECT: - raise data_entry_flow.AbortFlow(result) - LOGGER.debug("No working config found") - raise data_entry_flow.AbortFlow(RESULT_CANNOT_CONNECT) + async def _async_create_bridge(self) -> None: + """Create the bridge.""" + result, method, _info = await self._async_get_device_info_and_method() + if result not in SUCCESSFUL_RESULTS: + LOGGER.debug("No working config found for %s", self._host) + raise data_entry_flow.AbortFlow(result) + assert method is not None + self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) + return + + async def _async_get_device_info_and_method( + self, + ) -> tuple[str, str | None, dict[str, Any] | None]: + """Get device info and method only once.""" + if self._connect_result is None: + result, _, method, info = await async_get_device_info(self.hass, self._host) + self._connect_result = result + self._method = method + self._device_info = info + if not method: + LOGGER.debug("Host:%s did not return device info", self._host) + return result, None, None + return self._connect_result, self._method, self._device_info async def _async_get_and_check_device_info(self) -> bool: """Try to get the device info.""" - _port, _method, info = await async_get_device_info( - self.hass, self._bridge, self._host - ) + result, _method, info = await self._async_get_device_info_and_method() + if result not in SUCCESSFUL_RESULTS: + raise data_entry_flow.AbortFlow(result) if not info: - if not _method: - LOGGER.debug( - "Samsung host %s is not supported by either %s or %s methods", - self._host, - METHOD_LEGACY, - METHOD_WEBSOCKET, - ) - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) return False dev_info = info.get("device", {}) + assert dev_info is not None if (device_type := dev_info.get("type")) != "Samsung SmartTV": + LOGGER.debug( + "Host:%s has type: %s which is not supported", self._host, device_type + ) raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") @@ -169,7 +208,6 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): partial(getmac.get_mac_address, ip=self._host) ): self._mac = mac - self._device_info = info return True async def async_step_import( @@ -209,16 +247,73 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" if user_input is not None: await self._async_set_name_host_from_input(user_input) - await self._try_connect() + await self._async_create_bridge() assert self._bridge self._async_abort_entries_match({CONF_HOST: self._host}) if self._bridge.method != METHOD_LEGACY: # Legacy bridge does not provide device info await self._async_set_device_unique_id(raise_on_progress=False) - return self._get_entry_from_bridge() + if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_encrypted_pairing() + return await self.async_step_pairing({}) return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + async def async_step_pairing( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle a pairing by accepting the message on the TV.""" + assert self._bridge is not None + errors: dict[str, str] = {} + if user_input is not None: + result = await self._bridge.async_try_connect() + if result == RESULT_SUCCESS: + return self._get_entry_from_bridge() + if result != RESULT_AUTH_MISSING: + raise data_entry_flow.AbortFlow(result) + errors = {"base": RESULT_AUTH_MISSING} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="pairing", + errors=errors, + description_placeholders={"device": self._title}, + data_schema=vol.Schema({}), + ) + + async def async_step_encrypted_pairing( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle a encrypted pairing.""" + assert self._host is not None + await self._async_start_encrypted_pairing(self._host) + assert self._authenticator is not None + errors: dict[str, str] = {} + + if user_input is not None: + if ( + (pin := user_input.get("pin")) + and (token := await self._authenticator.try_pin(pin)) + and (session_id := await self._authenticator.get_session_id_and_close()) + ): + return self.async_create_entry( + data={ + **self._base_config_entry(), + CONF_TOKEN: token, + CONF_SESSION_ID: session_id, + }, + title=self._title, + ) + errors = {"base": RESULT_INVALID_PIN} + + self.context["title_placeholders"] = {"device": self._title} + return self.async_show_form( + step_id="encrypted_pairing", + errors=errors, + description_placeholders={"device": self._title}, + data_schema=vol.Schema({vol.Required("pin"): str}), + ) + @callback def _async_get_existing_matching_entry( self, @@ -254,8 +349,21 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): or (is_unique_match and self.unique_id != entry.unique_id) ): entry_kw_args["unique_id"] = self.unique_id - if self._mac and not entry.data.get(CONF_MAC): - entry_kw_args["data"] = {**entry.data, CONF_MAC: self._mac} + data = entry.data + update_ssdp_rendering_control_location = ( + self._ssdp_rendering_control_location + and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) + != self._ssdp_rendering_control_location + ) + update_mac = self._mac and not data.get(CONF_MAC) + if update_ssdp_rendering_control_location or update_mac: + entry_kw_args["data"] = {**entry.data} + if update_ssdp_rendering_control_location: + entry_kw_args["data"][ + CONF_SSDP_RENDERING_CONTROL_LOCATION + ] = self._ssdp_rendering_control_location + if update_mac: + entry_kw_args["data"][CONF_MAC] = self._mac if entry_kw_args: LOGGER.debug("Updating existing config entry with %s", entry_kw_args) self.hass.config_entries.async_update_entry(entry, **entry_kw_args) @@ -294,6 +402,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" + if discovery_info.ssdp_st == UPNP_SVC_RENDERINGCONTROL: + self._ssdp_rendering_control_location = discovery_info.ssdp_location + LOGGER.debug( + "Set SSDP location to: %s", self._ssdp_rendering_control_location + ) self._udn = self._upnp_udn = _strip_uuid( discovery_info.upnp[ssdp.ATTR_UPNP_UDN] ) @@ -345,12 +458,12 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: - - await self._try_connect() + await self._async_create_bridge() assert self._bridge - return self._get_entry_from_bridge() + if self._bridge.method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_encrypted_pairing() + return await self.async_step_pairing({}) - self._set_confirm_only() return self.async_show_form( step_id="confirm", description_placeholders={"device": self._title} ) @@ -378,6 +491,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self._reauth_entry method = self._reauth_entry.data[CONF_METHOD] if user_input is not None: + if method == METHOD_ENCRYPTED_WEBSOCKET: + return await self.async_step_reauth_confirm_encrypted() bridge = SamsungTVBridge.get_bridge( self.hass, method, @@ -399,40 +514,42 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {"base": RESULT_AUTH_MISSING} self.context["title_placeholders"] = {"device": self._title} - step_id = "reauth_confirm" - if method == METHOD_ENCRYPTED_WEBSOCKET: - step_id = "reauth_confirm_encrypted" return self.async_show_form( - step_id=step_id, + step_id="reauth_confirm", errors=errors, description_placeholders={"device": self._title}, ) + async def _async_start_encrypted_pairing(self, host: str) -> None: + if self._authenticator is None: + self._authenticator = SamsungTVEncryptedWSAsyncAuthenticator( + host, + web_session=async_get_clientsession(self.hass), + ) + await self._authenticator.start_pairing() + async def async_step_reauth_confirm_encrypted( self, user_input: dict[str, Any] | None = None ) -> data_entry_flow.FlowResult: """Confirm reauth (encrypted method).""" errors = {} assert self._reauth_entry - if self._encrypted_authenticator is None: - self._encrypted_authenticator = SamsungTVEncryptedWSAsyncAuthenticator( - self._reauth_entry.data[CONF_HOST], - web_session=async_get_clientsession(self.hass), - ) - await self._encrypted_authenticator.start_pairing() + await self._async_start_encrypted_pairing(self._reauth_entry.data[CONF_HOST]) + assert self._authenticator is not None - if user_input is not None and (pin := user_input.get("pin")): - if token := await self._encrypted_authenticator.try_pin(pin): - session_id = ( - await self._encrypted_authenticator.get_session_id_and_close() - ) - new_data = { - **self._reauth_entry.data, - CONF_TOKEN: token, - CONF_SESSION_ID: session_id, - } + if user_input is not None: + if ( + (pin := user_input.get("pin")) + and (token := await self._authenticator.try_pin(pin)) + and (session_id := await self._authenticator.get_session_id_and_close()) + ): self.hass.config_entries.async_update_entry( - self._reauth_entry, data=new_data + self._reauth_entry, + data={ + **self._reauth_entry.data, + CONF_TOKEN: token, + CONF_SESSION_ID: session_id, + }, ) await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index 498b83a0539..ad3300cd1e4 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -15,6 +15,7 @@ VALUE_CONF_ID = "ha.component.samsung" CONF_DESCRIPTION = "description" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" +CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location" CONF_ON_ACTION = "turn_on_action" CONF_SESSION_ID = "session_id" @@ -34,4 +35,10 @@ TIMEOUT_WEBSOCKET = 5 LEGACY_PORT = 55000 ENCRYPTED_WEBSOCKET_PORT = 8000 -WEBSOCKET_PORTS = (8002, 8001) +WEBSOCKET_NO_SSL_PORT = 8001 +WEBSOCKET_SSL_PORT = 8002 +WEBSOCKET_PORTS = (WEBSOCKET_SSL_PORT, WEBSOCKET_NO_SSL_PORT) + +SUCCESSFUL_RESULTS = {RESULT_AUTH_MISSING, RESULT_SUCCESS} + +UPNP_SVC_RENDERINGCONTROL = "urn:schemas-upnp-org:service:RenderingControl:1" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 2a5b2f76da3..44309ddf924 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -11,6 +11,14 @@ "ssdp": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" + }, + { + "manufacturer": "Samsung", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" + }, + { + "manufacturer": "Samsung Electronics", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" } ], "zeroconf": [ diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index f64620638bf..d0f4526335a 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -12,11 +12,17 @@ "confirm": { "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." }, + "pairing": { + "description": "[%key:component::samsungtv::config::step::confirm::description%]" + }, "reauth_confirm": { "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds or input PIN." }, - "reauth_confirm_encrypted": { + "encrypted_pairing": { "description": "Please enter the PIN displayed on {device}." + }, + "reauth_confirm_encrypted": { + "description": "[%key:component::samsungtv::config::step::encrypted_pairing::description%]" } }, "error": { @@ -34,4 +40,4 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 2e3dd88bec7..c4e0e181090 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -6,7 +6,6 @@ "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", "cannot_connect": "Failed to connect", "id_missing": "This Samsung device doesn't have a SerialNumber.", - "missing_config_entry": "This Samsung device doesn't have a configuration entry.", "not_supported": "This Samsung device is currently not supported.", "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" @@ -18,8 +17,13 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", - "title": "Samsung TV" + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + }, + "encrypted_pairing": { + "description": "Please enter the PIN displayed on {device}." + }, + "pairing": { + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." }, "reauth_confirm": { "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds or input PIN." diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 40bb9bf295f..10ea82949b5 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -225,6 +225,14 @@ SSDP = { "samsungtv": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" + }, + { + "manufacturer": "Samsung", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" + }, + { + "manufacturer": "Samsung Electronics", + "st": "urn:schemas-upnp-org:service:RenderingControl:1" } ], "songpal": [ diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 7cace81f7a6..1358e8e0bb5 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1,5 +1,5 @@ """Tests for the samsungtv component.""" - +from __future__ import annotations from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.config_entries import ConfigEntry diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index 3fc11d7c07a..b602a3a9c52 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -12,8 +12,10 @@ from samsungtvws.async_remote import SamsungTVWSAsyncRemote from samsungtvws.command import SamsungTVCommand from samsungtvws.encrypted.remote import SamsungTVEncryptedWSAsyncRemote from samsungtvws.event import ED_INSTALLED_APP_EVENT +from samsungtvws.exceptions import ResponseError from samsungtvws.remote import ChannelEmitCommand +from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT import homeassistant.util.dt as dt_util from .const import SAMPLE_DEVICE_INFO_WIFI @@ -47,7 +49,7 @@ def remote_fixture() -> Mock: yield remote -@pytest.fixture(name="rest_api", autouse=True) +@pytest.fixture(name="rest_api") def rest_api_fixture() -> Mock: """Patch the samsungtvws SamsungTVAsyncRest.""" with patch( @@ -60,6 +62,52 @@ def rest_api_fixture() -> Mock: yield rest_api_class.return_value +@pytest.fixture(name="rest_api_non_ssl_only") +def rest_api_fixture_non_ssl_only() -> Mock: + """Patch the samsungtvws SamsungTVAsyncRest non-ssl only.""" + + class MockSamsungTVAsyncRest: + """Mock for a MockSamsungTVAsyncRest.""" + + def __init__(self, host, session, port, timeout): + """Mock a MockSamsungTVAsyncRest.""" + self.port = port + self.host = host + + async def rest_device_info(self): + """Mock rest_device_info to fail for ssl and work for non-ssl.""" + if self.port == WEBSOCKET_SSL_PORT: + raise ResponseError + return SAMPLE_DEVICE_INFO_WIFI + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + MockSamsungTVAsyncRest, + ): + yield + + +@pytest.fixture(name="rest_api_failing") +def rest_api_failure_fixture() -> Mock: + """Patch the samsungtvws SamsungTVAsyncRest.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", + autospec=True, + ) as rest_api_class: + rest_api_class.return_value.rest_device_info.side_effect = ResponseError + yield + + +@pytest.fixture(name="remoteencws_failing") +def remoteencws_failing_fixture(): + """Patch the samsungtvws SamsungTVEncryptedWSAsyncRemote.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=OSError, + ): + yield + + @pytest.fixture(name="remotews") def remotews_fixture() -> Mock: """Patch the samsungtvws SamsungTVWS.""" diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 567b53646d7..1620b46ee23 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -5,7 +5,12 @@ from unittest.mock import ANY, AsyncMock, Mock, call, patch import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.async_remote import SamsungTVWSAsyncRemote -from samsungtvws.exceptions import ConnectionFailure, HttpApiError +from samsungtvws.exceptions import ( + ConnectionFailure, + HttpApiError, + ResponseError, + UnauthorizedError, +) from websockets import frames from websockets.exceptions import ( ConnectionClosedError, @@ -19,6 +24,7 @@ from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_MODEL, CONF_SESSION_ID, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, LEGACY_PORT, @@ -49,6 +55,11 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry @@ -88,6 +99,18 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", }, ) + +MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1", + ssdp_location="https://fake_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake_model", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", + }, +) MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", @@ -194,9 +217,15 @@ DEVICEINFO_WEBSOCKET_SSL = { "port": 8002, "timeout": TIMEOUT_WEBSOCKET, } +DEVICEINFO_WEBSOCKET_NO_SSL = { + "host": "fake_host", + "session": ANY, + "port": 8001, + "timeout": TIMEOUT_WEBSOCKET, +} -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote", "rest_api_failing") async def test_user_legacy(hass: HomeAssistant) -> None: """Test starting a flow by user.""" # show form @@ -221,7 +250,42 @@ async def test_user_legacy(hass: HomeAssistant) -> None: assert result["result"].unique_id is None -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("rest_api_failing") +async def test_user_legacy_does_not_ok_first_time(hass: HomeAssistant) -> None: + """Test starting a flow by user.""" + # show form + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=AccessDenied("Boom"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + # entry was added + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + with patch("homeassistant.components.samsungtv.bridge.Remote"): + # entry was added + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={} + ) + + # legacy tv entry created + assert result3["type"] == "create_entry" + assert result3["title"] == "fake_name" + assert result3["data"][CONF_HOST] == "fake_host" + assert result3["data"][CONF_NAME] == "fake_name" + assert result3["data"][CONF_METHOD] == "legacy" + assert result3["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER + assert result3["data"][CONF_MODEL] is None + assert result3["result"].unique_id is None + + +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_user_websocket(hass: HomeAssistant) -> None: """Test starting a flow by user.""" with patch( @@ -249,7 +313,59 @@ async def test_user_websocket(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only") +async def test_user_encrypted_websocket( + hass: HomeAssistant, +) -> None: + """Test starting a flow from ssdp for a supported device populates the mac.""" + # show form + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.samsungtv.config_flow.SamsungTVEncryptedWSAsyncAuthenticator", + autospec=True, + ) as authenticator_mock: + authenticator_mock.return_value.try_pin.side_effect = [ + None, + "037739871315caef138547b03e348b72", + ] + authenticator_mock.return_value.get_session_id_and_close.return_value = "1" + + # entry was added + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result2["type"] == "form" + assert result2["step_id"] == "encrypted_pairing" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"pin": "invalid"} + ) + assert result3["step_id"] == "encrypted_pairing" + assert result3["errors"] == {"base": "invalid_pin"} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], user_input={"pin": "1234"} + ) + + assert result4["type"] == "create_entry" + assert result4["title"] == "Living Room (82GXARRS)" + assert result4["data"][CONF_HOST] == "fake_host" + assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" + assert result4["data"][CONF_MANUFACTURER] == "Samsung" + assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] is None + assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" + assert result4["data"][CONF_SESSION_ID] == "1" + assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("rest_api_failing") async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: """Test starting a flow by user with authentication.""" with patch( @@ -260,10 +376,23 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" - assert result["reason"] == RESULT_AUTH_MISSING + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pairing" + assert result["errors"] == {"base": "auth_missing"} + + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError, + ): + # legacy device fails to connect after auth failed + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == RESULT_CANNOT_CONNECT +@pytest.mark.usefixtures("rest_api_failing") async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" with patch( @@ -278,6 +407,7 @@ async def test_user_legacy_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED +@pytest.mark.usefixtures("rest_api", "remoteencws_failing") async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: """Test starting a flow by user for not supported device.""" with patch( @@ -295,6 +425,7 @@ async def test_user_websocket_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED +@pytest.mark.usefixtures("rest_api", "remoteencws_failing") async def test_user_websocket_access_denied( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -315,6 +446,42 @@ async def test_user_websocket_access_denied( assert "Please check the Device Connection Manager on your TV" in caplog.text +@pytest.mark.usefixtures("rest_api", "remoteencws_failing") +async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: + """Test starting a flow by user for not supported device.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + side_effect=UnauthorizedError, + ): + # websocket device not supported + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pairing" + assert result["errors"] == {"base": "auth_missing"} + with patch( + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote.open", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("rest_api_failing") async def test_user_not_successful(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" with patch( @@ -331,6 +498,7 @@ async def test_user_not_successful(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT +@pytest.mark.usefixtures("rest_api_failing") async def test_user_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow by user but no connection found.""" with patch( @@ -347,75 +515,60 @@ async def test_user_not_successful_2(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote", "rest_api_failing") async def test_ssdp(hass: HomeAssistant) -> None: """Test starting a flow from discovery.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", - return_value=MOCK_DEVICE_INFO, - ): - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" - # entry was added - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "create_entry" - assert result["title"] == "fake_name (fake_model)" - assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_name" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result["data"][CONF_MODEL] == "fake_model" - assert result["result"].unique_id == "123" + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake_model" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MODEL] == "fake_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote", "rest_api_failing") async def test_ssdp_noprefix(hass: HomeAssistant) -> None: """Test starting a flow from discovery without prefixes.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSBridge.async_device_info", - return_value=MOCK_DEVICE_INFO_2, - ): - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_SSDP_DATA_NOPREFIX, - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" - - with patch( - "homeassistant.components.samsungtv.bridge.Remote.__enter__", - return_value=True, - ): - - # entry was added - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "create_entry" - assert result["title"] == "fake2_name (fake2_model)" - assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "fake2_name" - assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" - assert result["data"][CONF_MODEL] == "fake2_model" - assert result["result"].unique_id == "345" + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_NOPREFIX, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake2_model" + assert result["data"][CONF_HOST] == "fake2_host" + assert result["data"][CONF_NAME] == "fake2_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" + assert result["data"][CONF_MODEL] == "fake2_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api_failing") async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: """Test starting a flow from discovery with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=AccessDenied("Boom"), ): - # confirm to add the entry result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -424,48 +577,51 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: assert result["step_id"] == "confirm" # missing authentication + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pairing" + assert result["errors"] == {"base": "auth_missing"} - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.async_try_connect", - return_value=RESULT_AUTH_MISSING, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" - ) - assert result["type"] == "abort" - assert result["reason"] == RESULT_AUTH_MISSING + with patch("homeassistant.components.samsungtv.bridge.Remote"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "fake_model" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MODEL] == "fake_model" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -@pytest.mark.usefixtures("remote", "remotews") +@pytest.mark.usefixtures("remotews", "rest_api_failing") async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: """Test starting a flow from discovery for not supported device.""" - - # confirm to add the entry - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "form" - assert result["step_id"] == "confirm" - with patch( "homeassistant.components.samsungtv.bridge.SamsungTVLegacyBridge.async_try_connect", return_value=RESULT_NOT_SUPPORTED, ): - # device not supported - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input="whatever" + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == RESULT_NOT_SUPPORTED -@pytest.mark.usefixtures("remote", "remotews") -async def test_ssdp_websocket_success_populates_mac_address( +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( hass: HomeAssistant, ) -> None: """Test starting a flow from ssdp for a supported device populates the mac.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) assert result["type"] == "form" assert result["step_id"] == "confirm" @@ -480,13 +636,89 @@ async def test_ssdp_websocket_success_populates_mac_address( assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "82GXARRS" + assert ( + result["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] + == "https://fake_host:12345/test" + ) assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_ssdp_websocket_not_supported( +@pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only") +async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_location( + hass: HomeAssistant, +) -> None: + """Test starting a flow from ssdp for a supported device populates the mac.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + with patch( + "homeassistant.components.samsungtv.config_flow.SamsungTVEncryptedWSAsyncAuthenticator", + autospec=True, + ) as authenticator_mock: + authenticator_mock.return_value.try_pin.side_effect = [ + None, + "037739871315caef138547b03e348b72", + ] + authenticator_mock.return_value.get_session_id_and_close.return_value = "1" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["step_id"] == "encrypted_pairing" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"pin": "invalid"} + ) + assert result3["step_id"] == "encrypted_pairing" + assert result3["errors"] == {"base": "invalid_pin"} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], user_input={"pin": "1234"} + ) + + assert result4["type"] == "create_entry" + assert result4["title"] == "Living Room (82GXARRS)" + assert result4["data"][CONF_HOST] == "fake_host" + assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" + assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result4["data"][CONF_MODEL] == "82GXARRS" + assert ( + result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] + == "https://fake_host:12345/test" + ) + assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" + assert result4["data"][CONF_SESSION_ID] == "1" + assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("rest_api_non_ssl_only") +async def test_ssdp_encrypted_websocket_not_supported( + hass: HomeAssistant, +) -> None: + """Test starting a flow from ssdp for an unsupported device populates the mac.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", + side_effect=WebSocketException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == RESULT_NOT_SUPPORTED + + +async def test_ssdp_websocket_cannot_connect( hass: HomeAssistant, rest_api: Mock ) -> None: - """Test starting a flow from discovery for not supported device.""" + """Test starting a flow from discovery and we cannot connect.""" rest_api.rest_device_info.return_value = None with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -504,7 +736,7 @@ async def test_ssdp_websocket_not_supported( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "abort" - assert result["reason"] == RESULT_NOT_SUPPORTED + assert result["reason"] == RESULT_CANNOT_CONNECT @pytest.mark.usefixtures("remote") @@ -521,6 +753,7 @@ async def test_ssdp_model_not_supported(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_NOT_SUPPORTED +@pytest.mark.usefixtures("remoteencws_failing") async def test_ssdp_not_successful(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( @@ -549,6 +782,7 @@ async def test_ssdp_not_successful(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT +@pytest.mark.usefixtures("remoteencws_failing") async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: """Test starting a flow from discovery but no device found.""" with patch( @@ -577,7 +811,7 @@ async def test_ssdp_not_successful_2(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_CANNOT_CONNECT -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote", "remoteencws_failing") async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: """Test starting a flow from discovery twice.""" with patch( @@ -600,7 +834,7 @@ async def test_ssdp_already_in_progress(hass: HomeAssistant) -> None: assert result["reason"] == RESULT_ALREADY_IN_PROGRESS -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remotews", "remoteencws_failing") async def test_ssdp_already_configured(hass: HomeAssistant) -> None: """Test starting a flow from discovery when already configured.""" with patch( @@ -615,8 +849,8 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: assert result["type"] == "create_entry" entry = result["result"] assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER - assert entry.data[CONF_MODEL] is None - assert entry.unique_id is None + assert entry.data[CONF_MODEL] == "fake_model" + assert entry.unique_id == "123" # failed as already configured result2 = await hass.config_entries.flow.async_init( @@ -677,7 +911,7 @@ async def test_import_legacy_without_name(hass: HomeAssistant, rest_api: Mock) - assert entries[0].data[CONF_PORT] == LEGACY_PORT -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_import_websocket(hass: HomeAssistant): """Test importing from yaml with hostname.""" result = await hass.config_entries.flow.async_init( @@ -716,7 +950,7 @@ async def test_import_websocket_encrypted(hass: HomeAssistant): assert result["result"].unique_id is None -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_import_websocket_without_port(hass: HomeAssistant): """Test importing from yaml with hostname by no port.""" result = await hass.config_entries.flow.async_init( @@ -755,7 +989,7 @@ async def test_import_unknown_host(hass: HomeAssistant): assert result["reason"] == RESULT_UNKNOWN_HOST -@pytest.mark.usefixtures("remote", "remotews") +@pytest.mark.usefixtures("remotews", "rest_api_non_ssl_only", "remoteencws_failing") async def test_dhcp_wireless(hass: HomeAssistant) -> None: """Test starting a flow from dhcp.""" # confirm to add the entry @@ -782,7 +1016,7 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote", "remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from dhcp.""" # Even though it is named "wifiMac", it matches the mac of the wired connection @@ -811,7 +1045,7 @@ async def test_dhcp_wired(hass: HomeAssistant, rest_api: Mock) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remote", "remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_zeroconf(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf.""" result = await hass.config_entries.flow.async_init( @@ -837,7 +1071,7 @@ async def test_zeroconf(hass: HomeAssistant) -> None: assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "remoteencws_failing") async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> None: """Test starting a flow from zeroconf where the device is actually a soundbar.""" rest_api.rest_device_info.return_value = { @@ -874,7 +1108,7 @@ async def test_zeroconf_no_device_info(hass: HomeAssistant, rest_api: Mock) -> N assert result["reason"] == "not_supported" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf and dhcp.""" result = await hass.config_entries.flow.async_init( @@ -896,6 +1130,7 @@ async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant) -> None: assert result2["reason"] == "already_in_progress" +@pytest.mark.usefixtures("remoteencws_failing") async def test_autodetect_websocket(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( @@ -941,6 +1176,7 @@ async def test_autodetect_websocket(hass: HomeAssistant) -> None: assert entries[0].data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" +@pytest.mark.usefixtures("remoteencws_failing") async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: """Test for send key with autodetection of protocol.""" mac_address.return_value = "gg:ee:tt:mm:aa:cc" @@ -987,21 +1223,36 @@ async def test_websocket_no_mac(hass: HomeAssistant, mac_address: Mock) -> None: assert entries[0].data[CONF_MAC] == "gg:ee:tt:mm:aa:cc" +@pytest.mark.usefixtures("rest_api_failing") async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[AccessDenied("Boom")], + side_effect=AccessDenied("Boom"), ) as remote: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == "abort" - assert result["reason"] == RESULT_AUTH_MISSING - assert remote.call_count == 1 - assert remote.call_args_list == [call(AUTODETECT_LEGACY)] + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pairing" + assert result["errors"] == {"base": "auth_missing"} + + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_LEGACY), + call(AUTODETECT_LEGACY), + ] + with patch("homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == RESULT_CANNOT_CONNECT +@pytest.mark.usefixtures("rest_api_failing") async def test_autodetect_not_supported(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" with patch( @@ -1017,7 +1268,7 @@ async def test_autodetect_not_supported(hass: HomeAssistant) -> None: assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -@pytest.mark.usefixtures("remote") +@pytest.mark.usefixtures("remote", "rest_api_failing") async def test_autodetect_legacy(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" result = await hass.config_entries.flow.async_init( @@ -1032,18 +1283,13 @@ async def test_autodetect_legacy(hass: HomeAssistant) -> None: async def test_autodetect_none(hass: HomeAssistant) -> None: """Test for send key with autodetection of protocol.""" - mock_remotews = Mock() - mock_remotews.__aenter__ = AsyncMock(return_value=mock_remotews) - mock_remotews.__aexit__ = AsyncMock() - mock_remotews.open = Mock(side_effect=OSError("Boom")) - with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ) as remote, patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWSAsyncRemote", - return_value=mock_remotews, - ) as remotews: + "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest.rest_device_info", + side_effect=ResponseError, + ) as rest_device_info: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) @@ -1053,56 +1299,43 @@ async def test_autodetect_none(hass: HomeAssistant) -> None: assert remote.call_args_list == [ call(AUTODETECT_LEGACY), ] - assert remotews.call_count == 2 - assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_SSL), - call(**AUTODETECT_WEBSOCKET_PLAIN), - ] + assert rest_device_info.call_count == 2 -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_old_entry(hass: HomeAssistant) -> None: """Test update of old entry.""" - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - remote().rest_device_info.return_value = { - "device": { - "modelName": "fake_model2", - "name": "[TV] Fake Name", - "udn": "uuid:fake_serial", - } - } + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry.add_to_hass(hass) - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) - entry.add_to_hass(hass) + config_entries_domain = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries_domain) == 1 + assert entry is config_entries_domain[0] + assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" + assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP + assert not entry.unique_id - config_entries_domain = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries_domain) == 1 - assert entry is config_entries_domain[0] - assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" - assert entry.data[CONF_IP_ADDRESS] == EXISTING_IP - assert not entry.unique_id + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() - assert await async_setup_component(hass, DOMAIN, {}) is True - await hass.async_block_till_done() + # failed as already configured + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_ALREADY_CONFIGURED - # failed as already configured - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA - ) - assert result["type"] == "abort" - assert result["reason"] == RESULT_ALREADY_CONFIGURED + config_entries_domain = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries_domain) == 1 + entry2 = config_entries_domain[0] - config_entries_domain = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries_domain) == 1 - entry2 = config_entries_domain[0] - - # check updated device info - assert entry2.data.get(CONF_ID) is not None - assert entry2.data.get(CONF_IP_ADDRESS) is not None - assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + # check updated device info + assert entry2.data.get(CONF_ID) is not None + assert entry2.data.get(CONF_IP_ADDRESS) is not None + assert entry2.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_dhcp( hass: HomeAssistant, ) -> None: @@ -1131,7 +1364,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_zeroconf( hass: HomeAssistant, ) -> None: @@ -1159,11 +1392,11 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") -async def test_update_missing_mac_unique_id_added_from_ssdp( +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( hass: HomeAssistant, ) -> None: - """Test missing mac and unique id added via ssdp.""" + """Test missing mac, ssdp_location, and unique id added via ssdp.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) entry.add_to_hass(hass) with patch( @@ -1185,10 +1418,131 @@ async def test_update_missing_mac_unique_id_added_from_ssdp( assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Wrong st + assert CONF_SSDP_RENDERING_CONTROL_LOCATION not in entry.data assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test missing mac and unique id with outdated ssdp_location with the wrong st added via ssdp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + **MOCK_OLD_ENTRY, + CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test", + }, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Wrong ST, ssdp location should not change + assert ( + entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://1.2.3.4:555/test" + ) + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_updated_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + **MOCK_OLD_ENTRY, + CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test", + }, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Correct ST, ssdp location should change + assert ( + entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] + == "https://fake_host:12345/test" + ) + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_ssdp_location_rendering_st_updated_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test with outdated ssdp_location with the correct st added via ssdp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Correct ST, ssdp location should be added + assert ( + entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] + == "https://fake_host:12345/test" + ) + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews", "rest_api") async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( hass: HomeAssistant, ) -> None: @@ -1292,6 +1646,79 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( assert entry.unique_id is None +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_ssdp_location_unique_id_added_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test missing ssdp_location, and unique id added via ssdp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Wrong st + assert CONF_SSDP_RENDERING_CONTROL_LOCATION not in entry.data + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_control_st( + hass: HomeAssistant, +) -> None: + """Test missing ssdp_location, and unique id added via ssdp with rendering control st.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Correct st + assert ( + entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] + == "https://fake_host:12345/test" + ) + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + @pytest.mark.usefixtures("remote") async def test_form_reauth_legacy(hass: HomeAssistant) -> None: """Test reauthenticate legacy.""" @@ -1314,7 +1741,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: assert result2["reason"] == "reauth_successful" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_form_reauth_websocket(hass: HomeAssistant) -> None: """Test reauthenticate websocket.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) @@ -1339,6 +1766,7 @@ async def test_form_reauth_websocket(hass: HomeAssistant) -> None: assert entry.state == config_entries.ConfigEntryState.LOADED +@pytest.mark.usefixtures("rest_api") async def test_form_reauth_websocket_cannot_connect( hass: HomeAssistant, remotews: Mock ) -> None: @@ -1399,7 +1827,7 @@ async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: assert result2["reason"] == "not_supported" -@pytest.mark.usefixtures("remoteencws") +@pytest.mark.usefixtures("remoteencws", "rest_api") async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: """Test reauth flow for encrypted TVs.""" encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS} @@ -1429,13 +1857,13 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm_encrypted" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} # First time on reauth_confirm_encrypted # creates the authenticator, start pairing and requests PIN result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=None + result["flow_id"], user_input={} ) assert result["type"] == "form" assert result["step_id"] == "reauth_confirm_encrypted" @@ -1471,7 +1899,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: assert entry.data[CONF_SESSION_ID] == "1" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( hass: HomeAssistant, ) -> None: @@ -1504,7 +1932,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( hass: HomeAssistant, ) -> None: @@ -1537,7 +1965,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_from_dhcp( hass: HomeAssistant, ) -> None: @@ -1571,7 +1999,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( hass: HomeAssistant, ) -> None: diff --git a/tests/components/samsungtv/test_diagnostics.py b/tests/components/samsungtv/test_diagnostics.py index 88fb98a66db..65f51268081 100644 --- a/tests/components/samsungtv/test_diagnostics.py +++ b/tests/components/samsungtv/test_diagnostics.py @@ -19,7 +19,7 @@ from .test_media_player import MOCK_ENTRY_WS_WITH_MAC from tests.components.diagnostics import get_diagnostics_for_config_entry -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_entry_diagnostics( hass: HomeAssistant, hass_client: ClientSession ) -> None: diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 6fec07fab84..239840f8c8b 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -56,7 +56,12 @@ REMOTE_CALL = { } -@pytest.mark.usefixtures("remotews") +@pytest.fixture(name="autouse_rest_api", autouse=True) +def autouse_rest_api(rest_api) -> Mock: + """Enable auto use of the rest api fixture for these tests.""" + + +@pytest.mark.usefixtures("remotews", "remoteencws_failing") async def test_setup(hass: HomeAssistant) -> None: """Test Samsung TV integration is setup.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -98,7 +103,7 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant) assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "remoteencws_failing") async def test_setup_from_yaml_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -127,7 +132,7 @@ async def test_setup_duplicate_config( assert "duplicate host entries found" in caplog.text -@pytest.mark.usefixtures("remote", "remotews") +@pytest.mark.usefixtures("remote", "remotews", "remoteencws_failing") async def test_setup_duplicate_entries(hass: HomeAssistant) -> None: """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -138,7 +143,7 @@ async def test_setup_duplicate_entries(hass: HomeAssistant) -> None: assert len(hass.states.async_all("media_player")) == 1 -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "remoteencws_failing") async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index a0b712e575b..346d04e9a65 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -141,6 +141,11 @@ MOCK_CONFIG_NOTURNON = { } +@pytest.fixture(name="autouse_rest_api", autouse=True) +def autouse_rest_api(rest_api) -> Mock: + """Enable auto use of the rest api fixture for these tests.""" + + @pytest.fixture(name="delay") def delay_fixture(): """Patch the delay script function.""" From 42a5e2d4fe8ab9b4bca46074a45a759c47020c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 27 Mar 2022 22:39:46 +0200 Subject: [PATCH 0734/1054] Add Airzone binary sensor platform (#68140) Co-authored-by: J. Nick Koston --- homeassistant/components/airzone/__init__.py | 2 +- .../components/airzone/binary_sensor.py | 114 ++++++++++++++++++ .../components/airzone/test_binary_sensor.py | 56 +++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/airzone/binary_sensor.py create mode 100644 tests/components/airzone/test_binary_sensor.py diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 5d5350d7a29..0555b888fd8 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER from .coordinator import AirzoneUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py new file mode 100644 index 00000000000..1d0c76906e8 --- /dev/null +++ b/homeassistant/components/airzone/binary_sensor.py @@ -0,0 +1,114 @@ +"""Support for the Airzone sensors.""" +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Final + +from aioairzone.const import ( + AZD_AIR_DEMAND, + AZD_ERRORS, + AZD_FLOOR_DEMAND, + AZD_NAME, + AZD_PROBLEMS, + AZD_ZONES, +) + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import AirzoneEntity +from .const import DOMAIN +from .coordinator import AirzoneUpdateCoordinator + + +@dataclass +class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription): + """A class that describes airzone binary sensor entities.""" + + attributes: dict[str, str] | None = None + + +BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( + AirzoneBinarySensorEntityDescription( + device_class=DEVICE_CLASS_RUNNING, + key=AZD_AIR_DEMAND, + name="Air Demand", + ), + AirzoneBinarySensorEntityDescription( + device_class=DEVICE_CLASS_RUNNING, + key=AZD_FLOOR_DEMAND, + name="Floor Demand", + ), + AirzoneBinarySensorEntityDescription( + attributes={ + "errors": AZD_ERRORS, + }, + device_class=DEVICE_CLASS_PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + key=AZD_PROBLEMS, + name="Problem", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone binary sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + binary_sensors = [] + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): + for description in BINARY_SENSOR_TYPES: + if description.key in zone_data: + binary_sensors.append( + AirzoneBinarySensor( + coordinator, + description, + entry, + system_zone_id, + zone_data, + ) + ) + + async_add_entities(binary_sensors) + + +class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): + """Define an Airzone sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneBinarySensorEntityDescription, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + self._attr_name = f"{zone_data[AZD_NAME]} {description.name}" + self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}" + self.attributes = description.attributes + self.entity_description = description + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return state attributes.""" + if not self.attributes: + return None + return {key: self.get_zone_value(val) for key, val in self.attributes.items()} + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.get_zone_value(self.entity_description.key) diff --git a/tests/components/airzone/test_binary_sensor.py b/tests/components/airzone/test_binary_sensor.py new file mode 100644 index 00000000000..ee3a8324ea4 --- /dev/null +++ b/tests/components/airzone/test_binary_sensor.py @@ -0,0 +1,56 @@ +"""The sensor tests for the Airzone platform.""" + +from homeassistant.const import STATE_OFF, STATE_ON + +from .util import async_init_integration + + +async def test_airzone_create_binary_sensors(hass): + """Test creation of binary sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.despacho_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.despacho_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.despacho_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_1_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_1_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_1_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_2_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_2_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_2_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_ppal_air_demand") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.dorm_ppal_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.dorm_ppal_problem") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.salon_air_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.salon_floor_demand") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.salon_problem") + assert state.state == STATE_OFF From b5496441ae54bbd894aa3c9cab4f91102a49adda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Mar 2022 12:12:32 -1000 Subject: [PATCH 0735/1054] Use new samsungtv exception to detect when reauth is needed (#68762) --- homeassistant/components/samsungtv/bridge.py | 13 ++++++++---- .../components/samsungtv/test_media_player.py | 20 ++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 4e53b0fd0a5..fd258f562b4 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -534,16 +534,21 @@ class SamsungTVWSBridge(SamsungTVBridge): ) try: await self._remote.start_listening(self._remote_event) - except ConnectionClosedError as err: - # This is only happening when the auth was switched to DENY - # A removed auth will lead to socket timeout because waiting - # for auth popup is just an open socket + except UnauthorizedError as err: LOGGER.info( "Failed to get remote for %s, re-authentication required: %s", self.host, err.__repr__(), ) self._notify_reauth_callback() + self._remote = None + except ConnectionClosedError as err: + LOGGER.info( + "Failed to get remote for %s: %s", + self.host, + err.__repr__(), + ) + self._remote = None except ConnectionFailure as err: LOGGER.warning( "Unexpected ConnectionFailure trying to get remote for %s, " diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 346d04e9a65..f40777ccff5 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -12,7 +12,7 @@ from samsungtvws.encrypted.remote import ( SamsungTVEncryptedCommand, SamsungTVEncryptedWSAsyncRemote, ) -from samsungtvws.exceptions import ConnectionFailure, HttpApiError +from samsungtvws.exceptions import ConnectionFailure, HttpApiError, UnauthorizedError from samsungtvws.remote import ChannelEmitCommand, SendRemoteKey from websockets.exceptions import ConnectionClosedError, WebSocketException @@ -478,6 +478,24 @@ async def test_update_ws_connection_closed( async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_update_ws_unauthorized_error( + hass: HomeAssistant, mock_now: datetime, remotews: Mock +) -> None: + """Testing update tv unauthorized failure exception.""" + await setup_samsungtv(hass, MOCK_CONFIGWS) + + with patch.object( + remotews, "start_listening", side_effect=UnauthorizedError + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert [ flow for flow in hass.config_entries.flow.async_progress() From c024033dae98f01380842dac35be743fbefa0a36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Mar 2022 00:27:24 +0200 Subject: [PATCH 0736/1054] Add Upnp volume control/status to SamsungTV (#68663) Co-authored-by: epenet Co-authored-by: J. Nick Koston --- .../components/samsungtv/manifest.json | 3 +- .../components/samsungtv/media_player.py | 112 ++++++++++++++++-- requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/samsungtv/__init__.py | 25 ++++ tests/components/samsungtv/conftest.py | 24 ++++ .../components/samsungtv/test_media_player.py | 90 +++++++++++++- 7 files changed, 240 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 44309ddf924..c27011d6bd1 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -6,7 +6,8 @@ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", - "wakeonlan==2.0.1" + "wakeonlan==2.0.1", + "async-upnp-client==0.27.0" ], "ssdp": [ { diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index edd929273b1..578408d98f3 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -2,9 +2,15 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine +import contextlib from datetime import datetime, timedelta from typing import Any +from async_upnp_client.aiohttp import AiohttpSessionRequester +from async_upnp_client.client import UpnpDevice, UpnpService +from async_upnp_client.client_factory import UpnpFactory +from async_upnp_client.exceptions import UpnpActionResponseError, UpnpConnectionError import voluptuous as vol from wakeonlan import send_magic_packet @@ -24,12 +30,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo @@ -42,9 +50,11 @@ from .const import ( CONF_MANUFACTURER, CONF_MODEL, CONF_ON_ACTION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_NAME, DOMAIN, LOGGER, + UPNP_SVC_RENDERINGCONTROL, ) SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} @@ -104,6 +114,9 @@ class SamsungTVDevice(MediaPlayerEntity): self._config_entry = config_entry self._host: str | None = config_entry.data[CONF_HOST] self._mac: str | None = config_entry.data.get(CONF_MAC) + self._ssdp_rendering_control_location = config_entry.data.get( + CONF_SSDP_RENDERING_CONTROL_LOCATION + ) self._on_script = on_script # Assume that the TV is in Play mode self._playing: bool = True @@ -121,6 +134,8 @@ class SamsungTVDevice(MediaPlayerEntity): if self._on_script or self._mac: # Add turn-on if on_script or mac is available self._attr_supported_features |= SUPPORT_TURN_ON + if self._ssdp_rendering_control_location: + self._attr_supported_features |= SUPPORT_VOLUME_SET self._attr_device_info = DeviceInfo( name=self.name, @@ -142,6 +157,8 @@ class SamsungTVDevice(MediaPlayerEntity): self._bridge.register_reauth_callback(self.access_denied) self._bridge.register_app_list_callback(self._app_list_callback) + self._upnp_device: UpnpDevice | None = None + def _update_sources(self) -> None: self._attr_source_list = list(SOURCES) if app_list := self._app_list: @@ -179,21 +196,77 @@ class SamsungTVDevice(MediaPlayerEntity): STATE_ON if await self._bridge.async_is_on() else STATE_OFF ) - if self._attr_state == STATE_ON and not self._app_list_event.is_set(): - await self._bridge.async_request_app_list() - if self._app_list_event.is_set(): - # The try+wait_for is a bit expensive so we should try not to - # enter it unless we have to (Python 3.11 will have zero cost try) - return - try: - await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY) - except asyncio.TimeoutError as err: - # No need to try again - self._app_list_event.set() - LOGGER.debug( - "Failed to load app list from %s: %s", self._host, err.__repr__() + if self._attr_state != STATE_ON: + return + + startup_tasks: list[Coroutine[Any, Any, None]] = [] + + if not self._app_list_event.is_set(): + startup_tasks.append(self._async_startup_app_list()) + + if not self._upnp_device and self._ssdp_rendering_control_location: + startup_tasks.append(self._async_startup_upnp()) + + if startup_tasks: + await asyncio.gather(*startup_tasks) + + if not (service := self._get_upnp_service()): + return + + get_volume, get_mute = await asyncio.gather( + service.action("GetVolume").async_call(InstanceID=0, Channel="Master"), + service.action("GetMute").async_call(InstanceID=0, Channel="Master"), + ) + LOGGER.debug("Upnp GetVolume on %s: %s", self._host, get_volume) + if (volume_level := get_volume.get("CurrentVolume")) is not None: + self._attr_volume_level = volume_level / 100 + LOGGER.debug("Upnp GetMute on %s: %s", self._host, get_mute) + if (is_muted := get_mute.get("CurrentMute")) is not None: + self._attr_is_volume_muted = is_muted + + async def _async_startup_app_list(self) -> None: + await self._bridge.async_request_app_list() + if self._app_list_event.is_set(): + # The try+wait_for is a bit expensive so we should try not to + # enter it unless we have to (Python 3.11 will have zero cost try) + return + try: + await asyncio.wait_for(self._app_list_event.wait(), APP_LIST_DELAY) + except asyncio.TimeoutError as err: + # No need to try again + self._app_list_event.set() + LOGGER.debug( + "Failed to load app list from %s: %s", self._host, err.__repr__() + ) + + async def _async_startup_upnp(self) -> None: + assert self._ssdp_rendering_control_location is not None + if self._upnp_device is None: + session = async_get_clientsession(self.hass) + upnp_requester = AiohttpSessionRequester(session) + upnp_factory = UpnpFactory(upnp_requester) + with contextlib.suppress(UpnpConnectionError): + self._upnp_device = await upnp_factory.async_create_device( + self._ssdp_rendering_control_location ) + def _get_upnp_service(self, log: bool = False) -> UpnpService | None: + if self._upnp_device is None: + if log: + LOGGER.info("Upnp services are not available on %s", self._host) + return None + + if service := self._upnp_device.services.get(UPNP_SVC_RENDERINGCONTROL): + return service + + if log: + LOGGER.info( + "Upnp service %s is not available on %s", + UPNP_SVC_RENDERINGCONTROL, + self._host, + ) + return None + async def _async_launch_app(self, app_id: str) -> None: """Send launch_app to the tv.""" if self._power_off_in_progress(): @@ -233,6 +306,19 @@ class SamsungTVDevice(MediaPlayerEntity): self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME await self._bridge.async_power_off() + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level on the media player.""" + if not (service := self._get_upnp_service(log=True)): + return + try: + await service.action("SetVolume").async_call( + InstanceID=0, Channel="Master", DesiredVolume=int(volume * 100) + ) + except UpnpActionResponseError as err: + LOGGER.warning( + "Unable to set volume level on %s: %s", self._host, err.__repr__() + ) + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._async_send_keys(["KEY_VOLUP"]) diff --git a/requirements_all.txt b/requirements_all.txt index 329671ed5d0..0edc85003d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -326,6 +326,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms +# homeassistant.components.samsungtv # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85ea66b0090..7001227f590 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -259,6 +259,7 @@ arcam-fmj==0.12.0 # homeassistant.components.dlna_dmr # homeassistant.components.dlna_dms +# homeassistant.components.samsungtv # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 1358e8e0bb5..045b8b6e6de 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1,6 +1,10 @@ """Tests for the samsungtv component.""" from __future__ import annotations +from unittest.mock import Mock + +from async_upnp_client.client import UpnpAction, UpnpService + from homeassistant.components.samsungtv.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -21,3 +25,24 @@ async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> Config await hass.async_block_till_done() return entry + + +def upnp_get_action_mock(device: Mock, service_type: str, action: str) -> Mock: + """Get or Add UpnpService/UpnpAction to UpnpDevice mock.""" + upnp_service: Mock | None + if (upnp_service := device.services.get(service_type)) is None: + upnp_service = Mock(UpnpService) + upnp_service.actions = {} + + def _get_action(action: str): + return upnp_service.actions.get(action) + + upnp_service.action.side_effect = _get_action + device.services[service_type] = upnp_service + + upnp_action: Mock | None + if (upnp_action := upnp_service.actions.get(action)) is None: + upnp_action = Mock(UpnpAction) + upnp_service.actions[action] = upnp_action + + return upnp_action diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index b602a3a9c52..a3809726b5b 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -6,6 +6,8 @@ from datetime import datetime from typing import Any from unittest.mock import AsyncMock, Mock, patch +from async_upnp_client.client import UpnpDevice +from async_upnp_client.exceptions import UpnpConnectionError import pytest from samsungctl import Remote from samsungtvws.async_remote import SamsungTVWSAsyncRemote @@ -38,6 +40,28 @@ def app_list_delay_fixture() -> None: yield +@pytest.fixture(name="upnp_factory", autouse=True) +def upnp_factory_fixture() -> Mock: + """Patch UpnpFactory.""" + with patch( + "homeassistant.components.samsungtv.media_player.UpnpFactory", + autospec=True, + ) as upnp_factory_class: + upnp_factory: Mock = upnp_factory_class.return_value + upnp_factory.async_create_device.side_effect = UpnpConnectionError + yield upnp_factory + + +@pytest.fixture(name="upnp_device") +async def upnp_device_fixture(upnp_factory: Mock) -> Mock: + """Patch async_upnp_client.""" + upnp_device = Mock(UpnpDevice) + upnp_device.services = {} + + with patch.object(upnp_factory, "async_create_device", side_effect=[upnp_device]): + yield upnp_device + + @pytest.fixture(name="remote") def remote_fixture() -> Mock: """Patch the samsungctl Remote.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index f40777ccff5..546bede6467 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch +from async_upnp_client.exceptions import UpnpActionResponseError import pytest from samsungctl import exceptions from samsungtvws.async_remote import SamsungTVWSAsyncRemote @@ -21,6 +22,7 @@ from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, MEDIA_TYPE_APP, @@ -33,13 +35,17 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.samsungtv.const import ( CONF_MODEL, CONF_ON_ACTION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN as SAMSUNGTV_DOMAIN, ENCRYPTED_WEBSOCKET_PORT, METHOD_ENCRYPTED_WEBSOCKET, METHOD_WEBSOCKET, TIMEOUT_WEBSOCKET, ) -from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV +from homeassistant.components.samsungtv.media_player import ( + SUPPORT_SAMSUNGTV, + UPNP_SVC_RENDERINGCONTROL, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -62,6 +68,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_OFF, STATE_ON, @@ -73,7 +80,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import setup_samsungtv_entry +from . import setup_samsungtv_entry, upnp_get_action_mock from .const import ( MOCK_ENTRYDATA_ENCRYPTED_WS, SAMPLE_DEVICE_INFO_FRAME, @@ -119,6 +126,7 @@ MOCK_ENTRY_WS = { CONF_NAME: "fake", CONF_PORT: 8001, CONF_TOKEN: "123456789", + CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://any", } @@ -1304,3 +1312,81 @@ async def test_websocket_unsupported_remote_control( assert entry.data[CONF_PORT] == ENCRYPTED_WEBSOCKET_PORT state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.usefixtures("remotews") +async def test_volume_control_upnp( + hass: HomeAssistant, upnp_device: Mock, caplog: pytest.LogCaptureFixture +) -> None: + """Test for Upnp volume control.""" + upnp_get_volume = upnp_get_action_mock( + upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetVolume" + ) + upnp_get_volume.async_call.return_value = {"CurrentVolume": 44} + + upnp_get_mute = upnp_get_action_mock( + upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetMute" + ) + upnp_get_mute.async_call.return_value = {"CurrentMute": False} + + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + upnp_get_volume.async_call.assert_called_once() + upnp_get_mute.async_call.assert_called_once() + + # Upnp action succeeds + upnp_set_volume = upnp_get_action_mock( + upnp_device, UPNP_SVC_RENDERINGCONTROL, "SetVolume" + ) + assert await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, + True, + ) + assert "Unable to set volume level on" not in caplog.text + + # Upnp action failed + upnp_set_volume.async_call.side_effect = UpnpActionResponseError( + status=500, error_code=501, error_desc="Action Failed" + ) + assert await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, + True, + ) + assert "Unable to set volume level on" in caplog.text + + +@pytest.mark.usefixtures("remotews") +async def test_upnp_not_available( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test for volume control when Upnp is not available.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + # Upnp action fails + assert await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, + True, + ) + assert "Upnp services are not available" in caplog.text + + +@pytest.mark.usefixtures("remotews", "upnp_device") +async def test_upnp_missing_service( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test for volume control when Upnp is not available.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + # Upnp action fails + assert await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, + True, + ) + assert f"Upnp service {UPNP_SVC_RENDERINGCONTROL} is not available" in caplog.text From 17ddbb498373b62700b055780c01a2fb3d163c6d Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 28 Mar 2022 02:57:15 +0200 Subject: [PATCH 0737/1054] Restore AndroidTV entity name from migration (#68756) --- homeassistant/components/androidtv/config_flow.py | 4 ++-- .../components/androidtv/media_player.py | 4 +++- tests/components/androidtv/test_media_player.py | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 3dbae1cf393..520c0eccbeb 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -8,7 +8,7 @@ from androidtv import state_detection_rules_validator import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -164,7 +164,7 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input.get(CONF_NAME) or host, + title=host, data=user_input, ) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 19cae59a1b4..db09dc8ec66 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST, + CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, @@ -128,7 +129,8 @@ async def async_setup_entry( aftv = hass.data[DOMAIN][entry.entry_id][ANDROID_DEV] device_class = aftv.DEVICE_CLASS device_type = "Android TV" if device_class == DEVICE_ANDROIDTV else "Fire TV" - device_name = f"{device_type} {entry.data[CONF_HOST]}" + # CONF_NAME may be present in entry.data for configuration imported from YAML + device_name = entry.data.get(CONF_NAME) or f"{device_type} {entry.data[CONF_HOST]}" device_args = [ aftv, diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 59f31085ad2..d5102a06887 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -53,6 +53,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF, @@ -86,6 +87,14 @@ CONFIG_ANDROIDTV_PYTHON_ADB = { } } +# Android TV device with Python ADB implementation imported from YAML +CONFIG_ANDROIDTV_PYTHON_ADB_YAML = { + DOMAIN: { + CONF_NAME: "ADB yaml import", + **CONFIG_ANDROIDTV_PYTHON_ADB[DOMAIN], + } +} + # Android TV device with ADB server CONFIG_ANDROIDTV_ADB_SERVER = { DOMAIN: { @@ -127,7 +136,10 @@ def _setup(config): patch_key = "server" host = config[DOMAIN][CONF_HOST] - if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + # CONF_NAME available for configuration imported from YAML + if conf_name := config[DOMAIN].get(CONF_NAME): + entity_id = slugify(conf_name) + elif config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": entity_id = slugify(f"Android TV {host}") else: entity_id = slugify(f"Fire TV {host}") @@ -147,6 +159,7 @@ def _setup(config): "config", [ CONFIG_ANDROIDTV_PYTHON_ADB, + CONFIG_ANDROIDTV_PYTHON_ADB_YAML, CONFIG_FIRETV_PYTHON_ADB, CONFIG_ANDROIDTV_ADB_SERVER, CONFIG_FIRETV_ADB_SERVER, From cfc8b5fee7a2b8851d83c7f32bb5f36cf23b4e58 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 28 Mar 2022 08:53:30 +0200 Subject: [PATCH 0738/1054] Fix notify discovery setup (#68451) * Fix notify discovery setup * add test * unsubscribe at reset * Add guard * move dispatcher to reload module * only unsubscribe if platform was setup * initialize dispatcher once and tests * test get_service too * add tests * fix test * use get_service for test invalid platform * Test built-in reload method * set to None after clearing dispatcher - tests * Pathing services file * Update tests/components/notify/test_init.py Co-authored-by: Martin Hjelmare * dispatcher is not set twice if integration loaded * empty discovery payload * Removed not needed services.yaml mock * Update tests/components/notify/test_init.py Co-authored-by: Martin Hjelmare * flake8 Co-authored-by: Martin Hjelmare --- homeassistant/components/notify/legacy.py | 9 +- homeassistant/helpers/discovery.py | 4 +- homeassistant/helpers/reload.py | 9 +- tests/components/notify/test_init.py | 237 +++++++++++++++++++++- 4 files changed, 252 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index af29a9fba99..50b02324827 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -29,11 +29,13 @@ from .const import ( CONF_FIELDS = "fields" NOTIFY_SERVICES = "notify_services" +NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: """Set up legacy notify services.""" hass.data.setdefault(NOTIFY_SERVICES, {}) + hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None) async def async_setup_platform( integration_name: str, @@ -114,7 +116,9 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: """Handle for discovered platform.""" await async_setup_platform(platform, discovery_info=info) - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + hass.data[NOTIFY_DISCOVERY_DISPATCHER] = discovery.async_listen_platform( + hass, DOMAIN, async_platform_discovered + ) @callback @@ -147,6 +151,9 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None: @bind_hass async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Unregister notify services for an integration.""" + if NOTIFY_DISCOVERY_DISPATCHER in hass.data: + hass.data[NOTIFY_DISCOVERY_DISPATCHER]() + hass.data[NOTIFY_DISCOVERY_DISPATCHER] = None if not _async_integration_has_notify_services(hass, integration_name): return diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 20819ac7504..93efd7e69c1 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -95,7 +95,7 @@ def async_listen_platform( hass: core.HomeAssistant, component: str, callback: Callable[[str, dict[str, Any] | None], Any], -) -> None: +) -> Callable[[], None]: """Register a platform loader listener. This method must be run in the event loop. @@ -112,7 +112,7 @@ def async_listen_platform( if task: await task - async_dispatcher_connect( + return async_dispatcher_connect( hass, SIGNAL_PLATFORM_DISCOVERED.format(service), discovery_platform_listener ) diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 42fcf1032bb..83698557eb6 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -21,6 +21,8 @@ from .typing import ConfigType _LOGGER = logging.getLogger(__name__) +PLATFORM_RESET_LOCK = "lock_async_reset_platform_{}" + async def async_reload_integration_platforms( hass: HomeAssistant, integration_name: str, integration_platforms: Iterable[str] @@ -79,8 +81,11 @@ async def _resetup_platform( if hasattr(component, "async_reset_platform"): # If the integration has its own way to reset # use this method. - await component.async_reset_platform(hass, integration_name) - await component.async_setup(hass, root_config) + async with hass.data.setdefault( + PLATFORM_RESET_LOCK.format(integration_platform), asyncio.Lock() + ): + await component.async_reset_platform(hass, integration_name) + await component.async_setup(hass, root_config) return # If it's an entity platform, we use the entity_platform diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 9dbbcf9b9b9..ae32884add7 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,8 +1,41 @@ """The tests for notify services that change targets.""" + +from unittest.mock import Mock, patch + +import yaml + +from homeassistant import config as hass_config from homeassistant.components import notify +from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.setup import async_setup_component +from tests.common import MockPlatform, mock_platform + + +class MockNotifyPlatform(MockPlatform): + """Help to set up test notify service.""" + + def __init__(self, async_get_service=None, get_service=None): + """Return the notify service.""" + super().__init__() + if get_service: + self.get_service = get_service + if async_get_service: + self.async_get_service = async_get_service + + +def mock_notify_platform( + hass, tmp_path, integration="notify", async_get_service=None, get_service=None +): + """Specialize the mock platform for notify.""" + loaded_platform = MockNotifyPlatform(async_get_service, get_service) + mock_platform(hass, f"{integration}.notify", loaded_platform) + + return loaded_platform + async def test_same_targets(hass: HomeAssistant): """Test not changing the targets in a notify service.""" @@ -73,10 +106,16 @@ async def test_remove_targets(hass: HomeAssistant): class NotificationService(notify.BaseNotificationService): """A test class for notification services.""" - def __init__(self, hass): + def __init__(self, hass, target_list={"a": 1, "b": 2}, name="notify"): """Initialize the service.""" + + async def _async_make_reloadable(hass): + """Initialize the reload service.""" + await async_setup_reload_service(hass, name, [notify.DOMAIN]) + self.hass = hass - self.target_list = {"a": 1, "b": 2} + self.target_list = target_list + hass.async_create_task(_async_make_reloadable(hass)) @property def targets(self): @@ -97,3 +136,197 @@ async def test_warn_template(hass, caplog): # We should only log it once assert caplog.text.count("Passing templates to notify service is deprecated") == 1 assert hass.states.get("persistent_notification.notification") is not None + + +async def test_invalid_platform(hass, caplog, tmp_path): + """Test service setup with an invalid platform.""" + mock_notify_platform(hass, tmp_path, "testnotify1") + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify1"}]} + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + caplog.clear() + # Setup the second testnotify2 platform dynamically + mock_notify_platform(hass, tmp_path, "testnotify2") + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify2"}]}, + ) + await hass.async_block_till_done() + assert "Invalid notify platform" in caplog.text + + +async def test_invalid_service(hass, caplog, tmp_path): + """Test service setup with an invalid service object or platform.""" + + def get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + return None + + mock_notify_platform(hass, tmp_path, "testnotify", get_service=get_service) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Failed to initialize notification service testnotify" in caplog.text + caplog.clear() + + await async_load_platform( + hass, + "notify", + "testnotifyinvalid", + {"notify": [{"platform": "testnotifyinvalid"}]}, + hass_config={"notify": [{"platform": "testnotifyinvalid"}]}, + ) + await hass.async_block_till_done() + assert "Unknown notification service specified" in caplog.text + + +async def test_platform_setup_with_error(hass, caplog, tmp_path): + """Test service setup with an invalid setup.""" + + async def async_get_service(hass, config, discovery_info=None): + """Return None for an invalid notify service.""" + raise Exception("Setup error") + + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert "Error setting up platform testnotify" in caplog.text + + +async def test_reload_with_notify_builtin_platform_reload(hass, caplog, tmp_path): + """Test reload using the notify platform reload method.""" + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + # platform with service + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Perform a reload using the notify module for testnotify (without services) + await notify.async_reload(hass, "testnotify") + + # Setup the platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + # Perform a reload using the notify module for testnotify (with services) + await notify.async_reload(hass, "testnotify") + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + + +async def test_setup_platform_and_reload(hass, caplog, tmp_path): + """Test service setup and reload.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + # Setup the testnotify platform + await async_setup_component( + hass, "notify", {"notify": [{"platform": "testnotify"}]} + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + get_service_called.reset_mock() + + # Setup the second testnotify2 platform dynamically + await async_load_platform( + hass, + "notify", + "testnotify2", + {}, + hass_config={"notify": [{"platform": "testnotify"}]}, + ) + await hass.async_block_till_done() + assert hass.services.has_service("testnotify2", SERVICE_RELOAD) + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {} + assert get_service_called.call_args[0][1] == {} + get_service_called.reset_mock() + + # Perform a reload + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({"notify": [{"platform": "testnotify"}]}) + new_yaml_config_file.write_text(new_yaml_config) + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + await hass.services.async_call( + "testnotify", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.services.async_call( + "testnotify2", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + # Check if the notify services from setup still exist + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert get_service_called.call_count == 1 + assert get_service_called.call_args[0][0] == {"platform": "testnotify"} + assert get_service_called.call_args[0][1] is None + + # Check if the dynamically notify services from setup were removed + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") From 1746677b61c99ec55bb4ab1a7bdb0eb7660134cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 09:28:32 +0200 Subject: [PATCH 0739/1054] Clarify duration selector (#68731) --- homeassistant/helpers/selector.py | 2 ++ tests/helpers/test_selector.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 764f8ed49af..9577381d92b 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -277,6 +277,8 @@ class DurationSelector(Selector): CONFIG_SCHEMA = vol.Schema( { + # Enable day field in frontend. A selection with `days` set is allowed + # even if `enable_day` is not set vol.Optional("enable_day"): cv.boolean, } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index f1d7c83f211..9e68af05487 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -406,12 +406,15 @@ def test_attribute_selector_schema(schema, valid_selections, invalid_selections) ( ( {}, - ({"seconds": 10},), + ( + {"seconds": 10}, + {"days": 10}, # Days is allowed also if `enable_day` is not set + ), (None, {}), ), ( {"enable_day": True}, - ({"seconds": 10},), + ({"seconds": 10}, {"days": 10}), (None, {}), ), ), From 66d892237d2577d74d03702e9e44b75109c2d785 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 09:39:54 +0200 Subject: [PATCH 0740/1054] Add config flow for min_max sensor (#67807) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/group/config_flow.py | 6 +- homeassistant/components/min_max/__init__.py | 22 ++- .../components/min_max/config_flow.py | 58 ++++++++ homeassistant/components/min_max/const.py | 6 + .../components/min_max/manifest.json | 8 +- homeassistant/components/min_max/sensor.py | 71 +++++++-- homeassistant/components/min_max/strings.json | 28 ++++ .../components/min_max/translations/en.json | 28 ++++ homeassistant/generated/config_flows.py | 1 + tests/components/min_max/test_config_flow.py | 135 ++++++++++++++++++ tests/components/min_max/test_init.py | 55 +++++++ tests/components/min_max/test_sensor.py | 2 +- 12 files changed, 402 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/min_max/config_flow.py create mode 100644 homeassistant/components/min_max/const.py create mode 100644 homeassistant/components/min_max/strings.json create mode 100644 homeassistant/components/min_max/translations/en.json create mode 100644 tests/components/min_max/test_config_flow.py create mode 100644 tests/components/min_max/test_init.py diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 23394c0ee59..ed10391cb1d 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -115,7 +115,11 @@ class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: - """Return config entry title.""" + """Return config entry title. + + The options parameter contains config entry options, which is the union of user + input from the config flow steps. + """ return cast(str, options["name"]) if "name" in options else "" @callback diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index 84b7c0a2cf5..db80473f90a 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -1,6 +1,26 @@ """The min_max component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant -DOMAIN = "min_max" PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Min/Max from a config entry.""" + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py new file mode 100644 index 00000000000..353a90cbf6c --- /dev/null +++ b/homeassistant/components/min_max/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Min/Max integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.const import CONF_TYPE +from homeassistant.helpers import selector +from homeassistant.helpers.helper_config_entry_flow import ( + HelperConfigFlowHandler, + HelperFlowFormStep, + HelperFlowMenuStep, +) + +from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN + +_STATISTIC_MEASURES = ["last", "max", "mean", "min", "median"] + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_IDS): selector.selector( + {"entity": {"domain": "sensor", "multiple": True}} + ), + vol.Required(CONF_TYPE): selector.selector( + {"select": {"options": _STATISTIC_MEASURES}} + ), + vol.Required(CONF_ROUND_DIGITS, default=2): selector.selector( + {"number": {"min": 0, "max": 6, "mode": "box"}} + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required("name"): selector.selector({"text": {}}), + } +).extend(OPTIONS_SCHEMA.schema) + +CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "user": HelperFlowFormStep(CONFIG_SCHEMA) +} + +OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { + "init": HelperFlowFormStep(OPTIONS_SCHEMA) +} + + +class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): + """Handle a config or options flow for Min/Max.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return cast(str, options["name"]) if "name" in options else "" diff --git a/homeassistant/components/min_max/const.py b/homeassistant/components/min_max/const.py new file mode 100644 index 00000000000..d738eff4774 --- /dev/null +++ b/homeassistant/components/min_max/const.py @@ -0,0 +1,6 @@ +"""Constants for the Min/Max integration.""" + +DOMAIN = "min_max" + +CONF_ENTITY_IDS = "entity_ids" +CONF_ROUND_DIGITS = "round_digits" diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index cf8c78d46ac..45098f943d1 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -1,8 +1,12 @@ { "domain": "min_max", + "integration_type": "helper", "name": "Min/Max", "documentation": "https://www.home-assistant.io/integrations/min_max", - "codeowners": ["@fabaff"], + "codeowners": [ + "@fabaff" + ], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "config_flow": true } diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index cb6463ba5c9..bfc6b99d150 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -6,6 +6,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -13,14 +14,15 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, PLATFORMS +from . import PLATFORMS +from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -46,9 +48,6 @@ ATTR_TO_PROPERTY = [ ATTR_LAST_ENTITY_ID, ] -CONF_ENTITY_IDS = "entity_ids" -CONF_ROUND_DIGITS = "round_digits" - ICON = "mdi:calculator" SENSOR_TYPES = { @@ -71,6 +70,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize min/max/mean config entry.""" + registry = er.async_get(hass) + entity_ids = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITY_IDS] + ) + sensor_type = config_entry.options[CONF_TYPE] + round_digits = int(config_entry.options[CONF_ROUND_DIGITS]) + + async_add_entities( + [ + MinMaxSensor( + entity_ids, + config_entry.title, + sensor_type, + round_digits, + config_entry.entry_id, + ) + ] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -85,7 +110,9 @@ async def async_setup_platform( await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - async_add_entities([MinMaxSensor(entity_ids, name, sensor_type, round_digits)]) + async_add_entities( + [MinMaxSensor(entity_ids, name, sensor_type, round_digits, None)] + ) def calc_min(sensor_values): @@ -148,8 +175,9 @@ def calc_median(sensor_values, round_digits): class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" - def __init__(self, entity_ids, name, sensor_type, round_digits): + def __init__(self, entity_ids, name, sensor_type, round_digits, unique_id): """Initialize the min/max sensor.""" + self._attr_unique_id = unique_id self._entity_ids = entity_ids self._sensor_type = sensor_type self._round_digits = round_digits @@ -173,6 +201,12 @@ class MinMaxSensor(SensorEntity): ) ) + # Replay current state of source entities + for entity_id in self._entity_ids: + state = self.hass.states.get(entity_id) + state_event = Event("", {"entity_id": entity_id, "new_state": state}) + self._async_min_max_sensor_state_listener(state_event, update_state=False) + self._calc_values() @property @@ -216,16 +250,24 @@ class MinMaxSensor(SensorEntity): return ICON @callback - def _async_min_max_sensor_state_listener(self, event): + def _async_min_max_sensor_state_listener(self, event, update_state=True): """Handle the sensor state changes.""" new_state = event.data.get("new_state") entity = event.data.get("entity_id") - if new_state.state is None or new_state.state in [ - STATE_UNKNOWN, - STATE_UNAVAILABLE, - ]: + if ( + new_state is None + or new_state.state is None + or new_state.state + in [ + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ] + ): self.states[entity] = STATE_UNKNOWN + if not update_state: + return + self._calc_values() self.async_write_ha_state() return @@ -252,6 +294,9 @@ class MinMaxSensor(SensorEntity): "Unable to store state. Only numerical states are supported" ) + if not update_state: + return + self._calc_values() self.async_write_ha_state() diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json new file mode 100644 index 00000000000..13f81365a25 --- /dev/null +++ b/homeassistant/components/min_max/strings.json @@ -0,0 +1,28 @@ +{ + "title": "Min / max / mean / median sensor", + "config": { + "step": { + "user": { + "description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median.", + "data": { + "entity_ids": "Input entities", + "name": "Name", + "round_digits": "Precision", + "type": "Statistic characteristic" + } + } + } + }, + "options": { + "step": { + "options": { + "description": "[%key:component::min_max::config::step::user::description%]", + "data": { + "entity_ids": "[%key:component::min_max::config::step::user::data::entity_ids%]", + "round_digits": "[%key:component::min_max::config::step::user::data::round_digits%]", + "type": "[%key:component::min_max::config::step::user::data::type%]" + } + } + } + } +} diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json new file mode 100644 index 00000000000..65cdce569a8 --- /dev/null +++ b/homeassistant/components/min_max/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Input entities", + "name": "Name", + "round_digits": "Precision", + "type": "Statistic characteristic" + }, + "description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "entity_ids": "Input entities", + "round_digits": "Precision", + "type": "Statistic characteristic" + }, + "description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median." + } + } + }, + "title": "Min / max / mean / median sensor" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a8a5f492e9f..c659f78805e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -403,6 +403,7 @@ FLOWS = { ], "helper": [ "derivative", + "min_max", "tod" ] } diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py new file mode 100644 index 00000000000..8e66f1a4711 --- /dev/null +++ b/tests/components/min_max/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the Min/Max config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.min_max.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_config_flow(hass: HomeAssistant, platform) -> None: + """Test the config flow.""" + input_sensors = ["sensor.input_one", "sensor.input_two"] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.min_max.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"name": "My min_max", "entity_ids": input_sensors, "type": "max"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My min_max" + assert result["data"] == {} + assert result["options"] == { + "entity_ids": input_sensors, + "name": "My min_max", + "round_digits": 2.0, + "type": "max", + } + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == { + "entity_ids": input_sensors, + "name": "My min_max", + "round_digits": 2.0, + "type": "max", + } + assert config_entry.title == "My min_max" + + +def get_suggested(schema, key): + """Get suggested value for key in voluptuous schema.""" + for k in schema.keys(): + if k == key: + if k.description is None or "suggested_value" not in k.description: + return None + return k.description["suggested_value"] + # Wanted key absent from schema + raise Exception + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_options(hass: HomeAssistant, platform) -> None: + """Test reconfiguring.""" + hass.states.async_set("sensor.input_one", "10") + hass.states.async_set("sensor.input_two", "20") + hass.states.async_set("sensor.input_three", "33.33") + + input_sensors1 = ["sensor.input_one", "sensor.input_two"] + input_sensors2 = ["sensor.input_one", "sensor.input_two", "sensor.input_three"] + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_ids": input_sensors1, + "name": "My min_max", + "round_digits": 0, + "type": "min", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + assert get_suggested(schema, "entity_ids") == input_sensors1 + assert get_suggested(schema, "round_digits") == 0 + assert get_suggested(schema, "type") == "min" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "entity_ids": input_sensors2, + "round_digits": 1, + "type": "mean", + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "entity_ids": input_sensors2, + "name": "My min_max", + "round_digits": 1, + "type": "mean", + } + assert config_entry.data == {} + assert config_entry.options == { + "entity_ids": input_sensors2, + "name": "My min_max", + "round_digits": 1, + "type": "mean", + } + assert config_entry.title == "My min_max" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 4 + + # Check the state of the entity has changed as expected + state = hass.states.get(f"{platform}.my_min_max") + assert state.state == "21.1" + assert state.attributes["count_sensors"] == 3 diff --git a/tests/components/min_max/test_init.py b/tests/components/min_max/test_init.py new file mode 100644 index 00000000000..4df543c2565 --- /dev/null +++ b/tests/components/min_max/test_init.py @@ -0,0 +1,55 @@ +"""Test the Min/Max integration.""" +import pytest + +from homeassistant.components.min_max.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("platform", ("sensor",)) +async def test_setup_and_remove_config_entry( + hass: HomeAssistant, + platform: str, +) -> None: + """Test setting up and removing a config entry.""" + hass.states.async_set("sensor.input_one", "10") + hass.states.async_set("sensor.input_two", "20") + + input_sensors = ["sensor.input_one", "sensor.input_two"] + + registry = er.async_get(hass) + min_max_entity_id = f"{platform}.my_min_max" + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + "entity_ids": input_sensors, + "name": "My min_max", + "round_digits": 2.0, + "type": "max", + }, + title="My min_max", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the entity is registered in the entity registry + assert registry.async_get(min_max_entity_id) is not None + + # Check the platform is setup correctly + state = hass.states.get(min_max_entity_id) + assert state.state == "20.0" + assert state.attributes["count_sensors"] == 2 + + # Remove the config entry + assert await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + + # Check the state and entity registry entry are removed + assert hass.states.get(min_max_entity_id) is None + assert registry.async_get(min_max_entity_id) is None diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 1712a9027ca..ae6a14893f8 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -3,7 +3,7 @@ import statistics from unittest.mock import patch from homeassistant import config as hass_config -from homeassistant.components.min_max import DOMAIN +from homeassistant.components.min_max.const import DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, From 23c47c2206d926493527577629263976a6c15d12 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 28 Mar 2022 09:48:25 +0200 Subject: [PATCH 0741/1054] Add state class to Tankerkoenig fuel price sensors (#68737) --- homeassistant/components/tankerkoenig/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 0c1220ac07c..c22d75a16c6 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, @@ -92,6 +92,8 @@ async def async_setup_platform( class FuelPriceSensor(CoordinatorEntity, SensorEntity): """Contains prices for fuel in a given station.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, fuel_type, station, coordinator, name, show_on_map): """Initialize the sensor.""" super().__init__(coordinator) From 6cec53bea145d8e007a204d2db82bc5ee8a23963 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Mar 2022 22:01:07 -1000 Subject: [PATCH 0742/1054] Add support for finding the samsungtv MainTvAgent service location (#68763) --- .../components/samsungtv/__init__.py | 80 +++++++++- homeassistant/components/samsungtv/bridge.py | 13 +- .../components/samsungtv/config_flow.py | 113 +++++++++----- homeassistant/components/samsungtv/const.py | 7 +- .../components/samsungtv/manifest.json | 4 + .../components/samsungtv/media_player.py | 6 +- homeassistant/generated/ssdp.py | 3 + tests/components/samsungtv/__init__.py | 15 +- tests/components/samsungtv/conftest.py | 16 ++ tests/components/samsungtv/const.py | 43 +++++- .../components/samsungtv/test_config_flow.py | 146 +++++++++++++++--- tests/components/samsungtv/test_init.py | 44 ++++++ .../components/samsungtv/test_media_player.py | 18 ++- 13 files changed, 416 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 4363e7d8c44..b141d78fb9a 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -5,11 +5,13 @@ from collections.abc import Mapping from functools import partial import socket from typing import Any +from urllib.parse import urlparse import getmac import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -24,6 +26,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info @@ -31,12 +34,17 @@ from .const import ( CONF_MODEL, CONF_ON_ACTION, CONF_SESSION_ID, + CONF_SSDP_MAIN_TV_AGENT_LOCATION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_NAME, DOMAIN, + ENTRY_RELOAD_COOLDOWN, LEGACY_PORT, LOGGER, METHOD_ENCRYPTED_WEBSOCKET, METHOD_LEGACY, + UPNP_SVC_MAIN_TV_AGENT, + UPNP_SVC_RENDERING_CONTROL, ) @@ -109,6 +117,60 @@ def _async_get_device_bridge( ) +class DebouncedEntryReloader: + """Reload only after the timer expires.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Init the debounced entry reloader.""" + self.hass = hass + self.entry = entry + self.token = self.entry.data.get(CONF_TOKEN) + self._debounced_reload = Debouncer( + hass, + LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + + async def async_call(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Start the countdown for a reload.""" + if (new_token := entry.data.get(CONF_TOKEN)) != self.token: + LOGGER.debug("Skipping reload as its a token update") + self.token = new_token + return # Token updates should not trigger a reload + LOGGER.debug("Calling debouncer to get a reload after cooldown") + await self._debounced_reload.async_call() + + @callback + def async_cancel(self) -> None: + """Cancel any pending reload.""" + self._debounced_reload.async_cancel() + + async def _async_reload_entry(self) -> None: + """Reload entry.""" + LOGGER.debug("Reloading entry %s", self.entry.title) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + +async def _async_update_ssdp_locations(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update ssdp locations from discovery cache.""" + updates = {} + for ssdp_st, key in ( + (UPNP_SVC_RENDERING_CONTROL, CONF_SSDP_RENDERING_CONTROL_LOCATION), + (UPNP_SVC_MAIN_TV_AGENT, CONF_SSDP_MAIN_TV_AGENT_LOCATION), + ): + for discovery_info in await ssdp.async_get_discovery_info_by_st(hass, ssdp_st): + location = discovery_info.ssdp_location + host = urlparse(location).hostname + if host == entry.data[CONF_HOST]: + updates[key] = location + break + + if updates: + hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Samsung TV platform.""" @@ -128,14 +190,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: bridge.register_update_config_entry_callback(_update_config_entry) - # Allow bridge to force the reload of the config_entry - @callback - def _reload_config_entry() -> None: - """Update config entry with the new token.""" - hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) - - bridge.register_reload_callback(_reload_config_entry) - async def stop_bridge(event: Event) -> None: """Stop SamsungTV bridge connection.""" LOGGER.debug("Stopping SamsungTVBridge %s", bridge.host) @@ -145,8 +199,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_bridge) ) + await _async_update_ssdp_locations(hass, entry) + + # We must not await after we setup the reload or there + # will be a race where the config flow will see the entry + # as not loaded and may reload it + debounced_reloader = DebouncedEntryReloader(hass, entry) + entry.async_on_unload(debounced_reloader.async_cancel) + entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call)) + hass.data[DOMAIN][entry.entry_id] = bridge hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index fd258f562b4..d093dc35c72 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -148,7 +148,6 @@ class SamsungTVBridge(ABC): self.token: str | None = None self.session_id: str | None = None self._reauth_callback: CALLBACK_TYPE | None = None - self._reload_callback: CALLBACK_TYPE | None = None self._update_config_entry: Callable[[Mapping[str, Any]], None] | None = None self._app_list_callback: Callable[[dict[str, str]], None] | None = None @@ -156,10 +155,6 @@ class SamsungTVBridge(ABC): """Register a callback function.""" self._reauth_callback = func - def register_reload_callback(self, func: CALLBACK_TYPE) -> None: - """Register a callback function.""" - self._reload_callback = func - def register_update_config_entry_callback( self, func: Callable[[Mapping[str, Any]], None] ) -> None: @@ -211,11 +206,6 @@ class SamsungTVBridge(ABC): if self._reauth_callback is not None: self._reauth_callback() - def _notify_reload_callback(self) -> None: - """Notify reload callback.""" - if self._reload_callback is not None: - self._reload_callback() - def _notify_update_config_entry(self, updates: Mapping[str, Any]) -> None: """Notify update config callback.""" if self._update_config_entry is not None: @@ -597,7 +587,7 @@ class SamsungTVWSBridge(SamsungTVBridge): ) == "unrecognized method value : ms.remote.control": LOGGER.error( "Your TV seems to be unsupported by SamsungTVWSBridge" - " and needs a PIN: '%s'. Reloading", + " and needs a PIN: '%s'. Updating config entry", message, ) self._notify_update_config_entry( @@ -606,7 +596,6 @@ class SamsungTVWSBridge(SamsungTVBridge): CONF_PORT: ENCRYPTED_WEBSOCKET_PORT, } ) - self._notify_reload_callback() async def async_power_off(self) -> None: """Send power off command to remote.""" diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 0b87e38b00a..2acc0d1c7d5 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -30,6 +30,7 @@ from .const import ( CONF_MANUFACTURER, CONF_MODEL, CONF_SESSION_ID, + CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, @@ -46,7 +47,8 @@ from .const import ( RESULT_SUCCESS, RESULT_UNKNOWN_HOST, SUCCESSFUL_RESULTS, - UPNP_SVC_RENDERINGCONTROL, + UPNP_SVC_MAIN_TV_AGENT, + UPNP_SVC_RENDERING_CONTROL, WEBSOCKET_PORTS, ) @@ -58,7 +60,9 @@ def _strip_uuid(udn: str) -> str: def _entry_is_complete( - entry: config_entries.ConfigEntry, ssdp_rendering_control_location: str | None + entry: config_entries.ConfigEntry, + ssdp_rendering_control_location: str | None, + ssdp_main_tv_agent_location: str | None, ) -> bool: """Return True if the config entry information is complete. @@ -72,6 +76,10 @@ def _entry_is_complete( not ssdp_rendering_control_location or entry.data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) ) + and ( + not ssdp_main_tv_agent_location + or entry.data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION) + ) ) @@ -88,6 +96,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._udn: str | None = None self._upnp_udn: str | None = None self._ssdp_rendering_control_location: str | None = None + self._ssdp_main_tv_agent_location: str | None = None self._manufacturer: str | None = None self._model: str | None = None self._connect_result: str | None = None @@ -111,6 +120,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: self._name, CONF_PORT: self._bridge.port, CONF_SSDP_RENDERING_CONTROL_LOCATION: self._ssdp_rendering_control_location, + CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location, } def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: @@ -141,7 +151,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(self._udn, raise_on_progress=False) if ( entry := self._async_update_existing_matching_entry() - ) and _entry_is_complete(entry, self._ssdp_rendering_control_location): + ) and _entry_is_complete( + entry, + self._ssdp_rendering_control_location, + self._ssdp_main_tv_agent_location, + ): raise data_entry_flow.AbortFlow("already_configured") # Now that we have updated the config entry, we can raise # if another one is progressing @@ -157,7 +171,11 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): updates[ CONF_SSDP_RENDERING_CONTROL_LOCATION ] = self._ssdp_rendering_control_location - self._abort_if_unique_id_configured(updates=updates) + if self._ssdp_main_tv_agent_location: + updates[ + CONF_SSDP_MAIN_TV_AGENT_LOCATION + ] = self._ssdp_main_tv_agent_location + self._abort_if_unique_id_configured(updates=updates, reload_on_update=False) async def _async_create_bridge(self) -> None: """Create the bridge.""" @@ -342,36 +360,54 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Returns the existing entry if it was updated. """ entry, is_unique_match = self._async_get_existing_matching_entry() - if entry: - entry_kw_args: dict = {} - if self.unique_id and ( - entry.unique_id is None - or (is_unique_match and self.unique_id != entry.unique_id) - ): - entry_kw_args["unique_id"] = self.unique_id - data = entry.data - update_ssdp_rendering_control_location = ( - self._ssdp_rendering_control_location - and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) - != self._ssdp_rendering_control_location + if not entry: + return None + entry_kw_args: dict = {} + if ( + self.unique_id + and entry.unique_id is None + or (is_unique_match and self.unique_id != entry.unique_id) + ): + entry_kw_args["unique_id"] = self.unique_id + data: dict[str, Any] = dict(entry.data) + update_ssdp_rendering_control_location = ( + self._ssdp_rendering_control_location + and data.get(CONF_SSDP_RENDERING_CONTROL_LOCATION) + != self._ssdp_rendering_control_location + ) + update_ssdp_main_tv_agent_location = ( + self._ssdp_main_tv_agent_location + and data.get(CONF_SSDP_MAIN_TV_AGENT_LOCATION) + != self._ssdp_main_tv_agent_location + ) + update_mac = self._mac and not data.get(CONF_MAC) + if ( + update_ssdp_rendering_control_location + or update_ssdp_main_tv_agent_location + or update_mac + ): + if update_ssdp_rendering_control_location: + data[ + CONF_SSDP_RENDERING_CONTROL_LOCATION + ] = self._ssdp_rendering_control_location + if update_ssdp_main_tv_agent_location: + data[ + CONF_SSDP_MAIN_TV_AGENT_LOCATION + ] = self._ssdp_main_tv_agent_location + if update_mac: + data[CONF_MAC] = self._mac + entry_kw_args["data"] = data + if not entry_kw_args: + return None + LOGGER.debug("Updating existing config entry with %s", entry_kw_args) + self.hass.config_entries.async_update_entry(entry, **entry_kw_args) + if entry.state != config_entries.ConfigEntryState.LOADED: + # If its loaded it already has a reload listener in place + # and we do not want to trigger multiple reloads + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) ) - update_mac = self._mac and not data.get(CONF_MAC) - if update_ssdp_rendering_control_location or update_mac: - entry_kw_args["data"] = {**entry.data} - if update_ssdp_rendering_control_location: - entry_kw_args["data"][ - CONF_SSDP_RENDERING_CONTROL_LOCATION - ] = self._ssdp_rendering_control_location - if update_mac: - entry_kw_args["data"][CONF_MAC] = self._mac - if entry_kw_args: - LOGGER.debug("Updating existing config entry with %s", entry_kw_args) - self.hass.config_entries.async_update_entry(entry, **entry_kw_args) - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - return entry - return None + return entry async def _async_start_discovery_with_mac_address(self) -> None: """Start discovery.""" @@ -402,10 +438,17 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" - if discovery_info.ssdp_st == UPNP_SVC_RENDERINGCONTROL: + if discovery_info.ssdp_st == UPNP_SVC_RENDERING_CONTROL: self._ssdp_rendering_control_location = discovery_info.ssdp_location LOGGER.debug( - "Set SSDP location to: %s", self._ssdp_rendering_control_location + "Set SSDP RenderingControl location to: %s", + self._ssdp_rendering_control_location, + ) + elif discovery_info.ssdp_st == UPNP_SVC_MAIN_TV_AGENT: + self._ssdp_main_tv_agent_location = discovery_info.ssdp_location + LOGGER.debug( + "Set SSDP MainTvAgent location to: %s", + self._ssdp_main_tv_agent_location, ) self._udn = self._upnp_udn = _strip_uuid( discovery_info.upnp[ssdp.ATTR_UPNP_UDN] diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index ad3300cd1e4..cabd85901f6 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -16,6 +16,7 @@ CONF_DESCRIPTION = "description" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location" +CONF_SSDP_MAIN_TV_AGENT_LOCATION = "ssdp_main_tv_agent_location" CONF_ON_ACTION = "turn_on_action" CONF_SESSION_ID = "session_id" @@ -41,4 +42,8 @@ WEBSOCKET_PORTS = (WEBSOCKET_SSL_PORT, WEBSOCKET_NO_SSL_PORT) SUCCESSFUL_RESULTS = {RESULT_AUTH_MISSING, RESULT_SUCCESS} -UPNP_SVC_RENDERINGCONTROL = "urn:schemas-upnp-org:service:RenderingControl:1" +UPNP_SVC_RENDERING_CONTROL = "urn:schemas-upnp-org:service:RenderingControl:1" +UPNP_SVC_MAIN_TV_AGENT = "urn:samsung.com:service:MainTVAgent2:1" + +# Time to wait before reloading entry upon device config change +ENTRY_RELOAD_COOLDOWN = 5 diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index c27011d6bd1..acc5ea3a66a 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -13,6 +13,9 @@ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" }, + { + "st": "urn:samsung.com:service:MainTVAgent2:1" + }, { "manufacturer": "Samsung", "st": "urn:schemas-upnp-org:service:RenderingControl:1" @@ -25,6 +28,7 @@ "zeroconf": [ {"type":"_airplay._tcp.local.","properties":{"manufacturer":"samsung*"}} ], + "dependencies": ["ssdp"], "dhcp": [ {"registered_devices": true}, { diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 578408d98f3..c20674efdfd 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -54,7 +54,7 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, - UPNP_SVC_RENDERINGCONTROL, + UPNP_SVC_RENDERING_CONTROL, ) SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} @@ -256,13 +256,13 @@ class SamsungTVDevice(MediaPlayerEntity): LOGGER.info("Upnp services are not available on %s", self._host) return None - if service := self._upnp_device.services.get(UPNP_SVC_RENDERINGCONTROL): + if service := self._upnp_device.services.get(UPNP_SVC_RENDERING_CONTROL): return service if log: LOGGER.info( "Upnp service %s is not available on %s", - UPNP_SVC_RENDERINGCONTROL, + UPNP_SVC_RENDERING_CONTROL, self._host, ) return None diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 10ea82949b5..1c0876cd791 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -226,6 +226,9 @@ SSDP = { { "st": "urn:samsung.com:device:RemoteControlReceiver:1" }, + { + "st": "urn:samsung.com:service:MainTVAgent2:1" + }, { "manufacturer": "Samsung", "st": "urn:schemas-upnp-org:service:RenderingControl:1" diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 045b8b6e6de..516aa5d8c95 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1,17 +1,28 @@ """Tests for the samsungtv component.""" from __future__ import annotations +from datetime import timedelta from unittest.mock import Mock from async_upnp_client.client import UpnpAction, UpnpService -from homeassistant.components.samsungtv.const import DOMAIN +from homeassistant.components.samsungtv.const import DOMAIN, ENTRY_RELOAD_COOLDOWN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def async_wait_config_entry_reload(hass: HomeAssistant) -> None: + """Wait for the config entry to reload.""" + await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=ENTRY_RELOAD_COOLDOWN) + ) + await hass.async_block_till_done() async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> ConfigEntry: diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index a3809726b5b..e4ef0666423 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -23,6 +23,22 @@ import homeassistant.util.dt as dt_util from .const import SAMPLE_DEVICE_INFO_WIFI +@pytest.fixture(autouse=True) +async def silent_ssdp_scanner(hass): + """Start SSDP component and get Scanner, prevent actual SSDP traffic.""" + with patch( + "homeassistant.components.ssdp.Scanner._async_start_ssdp_listeners" + ), patch("homeassistant.components.ssdp.Scanner._async_stop_ssdp_listeners"), patch( + "homeassistant.components.ssdp.Scanner.async_scan" + ): + yield + + +@pytest.fixture(autouse=True) +def samsungtv_mock_get_source_ip(mock_get_source_ip): + """Mock network util's async_get_source_ip.""" + + @pytest.fixture(autouse=True) def fake_host_fixture() -> None: """Patch gethostbyname.""" diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 64c3c6add8e..28e6e6a7120 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -1,7 +1,18 @@ """Constants for the samsungtv tests.""" from samsungtvws.event import ED_INSTALLED_APP_EVENT -from homeassistant.components.samsungtv.const import CONF_SESSION_ID +from homeassistant.components import ssdp +from homeassistant.components.samsungtv.const import ( + CONF_MODEL, + CONF_SESSION_ID, + METHOD_WEBSOCKET, +) +from homeassistant.components.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_UDN, +) from homeassistant.const import ( CONF_HOST, CONF_IP_ADDRESS, @@ -25,6 +36,36 @@ MOCK_ENTRYDATA_ENCRYPTED_WS = { CONF_TOKEN: "037739871315caef138547b03e348b72", CONF_SESSION_ID: "2", } +MOCK_ENTRYDATA_WS = { + CONF_HOST: "fake_host", + CONF_METHOD: METHOD_WEBSOCKET, + CONF_PORT: 8002, + CONF_MODEL: "any", + CONF_NAME: "any", +} + +MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1", + ssdp_location="https://fake_host:12345/test", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake_model", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", + }, +) +MOCK_SSDP_DATA_MAIN_TV_AGENT_ST = ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="urn:samsung.com:service:MainTVAgent2:1", + ssdp_location="https://fake_host:12345/tv_agent", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake_model", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", + }, +) SAMPLE_DEVICE_INFO_WIFI = { "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 1620b46ee23..ba66cbc67ae 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -24,6 +24,7 @@ from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, CONF_MODEL, CONF_SESSION_ID, + CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_MANUFACTURER, DOMAIN, @@ -66,6 +67,9 @@ from . import setup_samsungtv_entry from .const import ( MOCK_CONFIG_ENCRYPTED_WS, MOCK_ENTRYDATA_ENCRYPTED_WS, + MOCK_ENTRYDATA_WS, + MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, + MOCK_SSDP_DATA_RENDERING_CONTROL_ST, SAMPLE_DEVICE_INFO_FRAME, ) @@ -100,17 +104,6 @@ MOCK_SSDP_DATA = ssdp.SsdpServiceInfo( }, ) -MOCK_SSDP_DATA_RENDERING_CONTROL_ST = ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="urn:schemas-upnp-org:service:RenderingControl:1", - ssdp_location="https://fake_host:12345/test", - upnp={ - ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", - ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", - ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", - }, -) MOCK_SSDP_DATA_NOPREFIX = ssdp.SsdpServiceInfo( ssdp_usn="mock_usn", ssdp_st="mock_st", @@ -164,13 +157,6 @@ MOCK_LEGACY_ENTRY = { CONF_METHOD: "legacy", CONF_PORT: None, } -MOCK_WS_ENTRY = { - CONF_HOST: "fake_host", - CONF_METHOD: METHOD_WEBSOCKET, - CONF_PORT: 8002, - CONF_MODEL: "any", - CONF_NAME: "any", -} MOCK_DEVICE_INFO = { "device": { "type": "Samsung SmartTV", @@ -643,6 +629,36 @@ async def test_ssdp_websocket_success_populates_mac_address_and_ssdp_location( assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_ssdp_websocket_success_populates_mac_address_and_main_tv_ssdp_location( + hass: HomeAssistant, +) -> None: + """Test starting a flow from ssdp for a supported device populates the mac.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert ( + result["data"][CONF_SSDP_MAIN_TV_AGENT_LOCATION] + == "https://fake_host:12345/tv_agent" + ) + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + @pytest.mark.usefixtures("remoteencws", "rest_api_non_ssl_only") async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_location( hass: HomeAssistant, @@ -1504,6 +1520,52 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + **MOCK_OLD_ENTRY, + CONF_SSDP_RENDERING_CONTROL_LOCATION: "https://1.2.3.4:555/test", + CONF_SSDP_MAIN_TV_AGENT_LOCATION: "https://1.2.3.4:555/test", + }, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Main TV Agent ST, ssdp location should change + assert ( + entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] + == "https://fake_host:12345/tv_agent" + ) + # Rendering control should not be affected + assert ( + entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://1.2.3.4:555/test" + ) + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_rendering_st_updated_from_ssdp( hass: HomeAssistant, @@ -1542,6 +1604,44 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +@pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") +async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( + hass: HomeAssistant, +) -> None: + """Test with outdated ssdp_location with the correct st added via ssdp.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_OLD_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + unique_id="be9554b9-c9fb-41f4-8920-22da015376a4", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" + # Correct ST for MainTV, ssdp location should be added + assert ( + entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] + == "https://fake_host:12345/tv_agent" + ) + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + @pytest.mark.usefixtures("remotews", "rest_api") async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( hass: HomeAssistant, @@ -1744,7 +1844,7 @@ async def test_form_reauth_legacy(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api") async def test_form_reauth_websocket(hass: HomeAssistant) -> None: """Test reauthenticate websocket.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) entry.add_to_hass(hass) assert entry.state == config_entries.ConfigEntryState.NOT_LOADED @@ -1771,7 +1871,7 @@ async def test_form_reauth_websocket_cannot_connect( hass: HomeAssistant, remotews: Mock ) -> None: """Test reauthenticate websocket when we cannot connect on the first attempt.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1803,7 +1903,7 @@ async def test_form_reauth_websocket_cannot_connect( async def test_form_reauth_websocket_not_supported(hass: HomeAssistant) -> None: """Test reauthenticate websocket when the device is not supported.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRYDATA_WS) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -1972,7 +2072,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( """Test that DHCP updates the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_WS_ENTRY, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, + data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:ww:ii:ff:ii"}, source=config_entries.SOURCE_SSDP, unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) @@ -2006,7 +2106,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( """Test that DHCP does not update the wrong udn from ssdp via host match.""" entry = MockConfigEntry( domain=DOMAIN, - data={**MOCK_WS_ENTRY, CONF_MAC: "aa:bb:ss:ss:dd:pp"}, + data={**MOCK_ENTRYDATA_WS, CONF_MAC: "aa:bb:ss:ss:dd:pp"}, source=config_entries.SOURCE_SSDP, unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", ) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 239840f8c8b..9d3289495a6 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -7,8 +7,12 @@ import pytest from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, + CONF_SSDP_MAIN_TV_AGENT_LOCATION, + CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN as SAMSUNGTV_DOMAIN, METHOD_WEBSOCKET, + UPNP_SVC_MAIN_TV_AGENT, + UPNP_SVC_RENDERING_CONTROL, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.config_entries import ConfigEntryState @@ -24,6 +28,14 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from .const import ( + MOCK_ENTRYDATA_WS, + MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, + MOCK_SSDP_DATA_RENDERING_CONTROL_ST, +) + +from tests.common import MockConfigEntry + ENTITY_ID = f"{DOMAIN}.fake_name" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ @@ -156,3 +168,35 @@ async def test_setup_h_j_model( state = hass.states.get(ENTITY_ID) assert state assert "H and J series use an encrypted protocol" in caplog.text + + +@pytest.mark.usefixtures("remote", "remotews", "remoteencws_failing") +async def test_setup_updates_from_ssdp(hass: HomeAssistant) -> None: + """Test setting up the entry fetches data from ssdp cache.""" + entry = MockConfigEntry(domain="samsungtv", data=MOCK_ENTRYDATA_WS) + entry.add_to_hass(hass) + + async def _mock_async_get_discovery_info_by_st(hass: HomeAssistant, mock_st: str): + if mock_st == UPNP_SVC_RENDERING_CONTROL: + return [MOCK_SSDP_DATA_RENDERING_CONTROL_ST] + if mock_st == UPNP_SVC_MAIN_TV_AGENT: + return [MOCK_SSDP_DATA_MAIN_TV_AGENT_ST] + raise ValueError(f"Unknown st {mock_st}") + + with patch( + "homeassistant.components.samsungtv.ssdp.async_get_discovery_info_by_st", + _mock_async_get_discovery_info_by_st, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get("media_player.any") + assert ( + entry.data[CONF_SSDP_MAIN_TV_AGENT_LOCATION] + == "https://fake_host:12345/tv_agent" + ) + assert ( + entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] + == "https://fake_host:12345/test" + ) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 546bede6467..fa8cd871de9 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -44,7 +44,7 @@ from homeassistant.components.samsungtv.const import ( ) from homeassistant.components.samsungtv.media_player import ( SUPPORT_SAMSUNGTV, - UPNP_SVC_RENDERINGCONTROL, + UPNP_SVC_RENDERING_CONTROL, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -80,7 +80,11 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import setup_samsungtv_entry, upnp_get_action_mock +from . import ( + async_wait_config_entry_reload, + setup_samsungtv_entry, + upnp_get_action_mock, +) from .const import ( MOCK_ENTRYDATA_ENCRYPTED_WS, SAMPLE_DEVICE_INFO_FRAME, @@ -1301,8 +1305,8 @@ async def test_websocket_unsupported_remote_control( "'unrecognized method value : ms.remote.control'" in caplog.text ) + await async_wait_config_entry_reload(hass) # ensure reauth triggered, and method/port updated - await hass.async_block_till_done() assert [ flow for flow in hass.config_entries.flow.async_progress() @@ -1320,12 +1324,12 @@ async def test_volume_control_upnp( ) -> None: """Test for Upnp volume control.""" upnp_get_volume = upnp_get_action_mock( - upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetVolume" + upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetVolume" ) upnp_get_volume.async_call.return_value = {"CurrentVolume": 44} upnp_get_mute = upnp_get_action_mock( - upnp_device, UPNP_SVC_RENDERINGCONTROL, "GetMute" + upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetMute" ) upnp_get_mute.async_call.return_value = {"CurrentMute": False} @@ -1335,7 +1339,7 @@ async def test_volume_control_upnp( # Upnp action succeeds upnp_set_volume = upnp_get_action_mock( - upnp_device, UPNP_SVC_RENDERINGCONTROL, "SetVolume" + upnp_device, UPNP_SVC_RENDERING_CONTROL, "SetVolume" ) assert await hass.services.async_call( DOMAIN, @@ -1389,4 +1393,4 @@ async def test_upnp_missing_service( {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, ) - assert f"Upnp service {UPNP_SVC_RENDERINGCONTROL} is not available" in caplog.text + assert f"Upnp service {UPNP_SVC_RENDERING_CONTROL} is not available" in caplog.text From 6f567afc0e295170a5bac7a60d09f31f3d68c7e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Mar 2022 10:33:43 +0200 Subject: [PATCH 0743/1054] Mock out all default onboarding integrations in test (#68776) Co-authored-by: Erik Montnemery --- tests/components/onboarding/test_views.py | 64 +++++++++++------------ 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 3044f53cc38..025459e73b7 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -14,12 +14,6 @@ from homeassistant.setup import async_setup_component from . import mock_storage from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, register_auth_provider -from tests.components.met.conftest import mock_weather # noqa: F401 - - -@pytest.fixture(autouse=True) -def always_mock_weather(mock_weather): # noqa: F811 - """Mock the Met weather provider.""" @pytest.fixture(autouse=True) @@ -90,6 +84,21 @@ async def mock_supervisor_fixture(hass, aioclient_mock): yield +@pytest.fixture +def mock_default_integrations(): + """Mock the default integrations set up during onboarding.""" + with patch( + "homeassistant.components.rpi_power.config_flow.new_under_voltage" + ), patch( + "homeassistant.components.rpi_power.binary_sensor.new_under_voltage" + ), patch( + "homeassistant.components.met.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.radio_browser.async_setup_entry", return_value=True + ): + yield + + async def test_onboarding_progress(hass, hass_storage, hass_client_no_auth): """Test fetching progress.""" mock_storage(hass_storage, {"done": ["hello"]}) @@ -364,7 +373,9 @@ async def test_onboarding_integration_requires_auth( assert resp.status == 401 -async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): +async def test_onboarding_core_sets_up_met( + hass, hass_storage, hass_client, mock_default_integrations +): """Test finishing the core step.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -372,19 +383,17 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): await hass.async_block_till_done() client = await hass_client() - with patch( - "homeassistant.components.met.async_setup_entry", return_value=True - ) as mock_setup: - resp = await client.post("/api/onboarding/core_config") + resp = await client.post("/api/onboarding/core_config") assert resp.status == 200 await hass.async_block_till_done() assert len(hass.config_entries.async_entries("met")) == 1 - assert len(mock_setup.mock_calls) == 1 -async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_client): +async def test_onboarding_core_sets_up_radio_browser( + hass, hass_storage, hass_client, mock_default_integrations +): """Test finishing the core step set up the radio browser.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -392,21 +401,16 @@ async def test_onboarding_core_sets_up_radio_browser(hass, hass_storage, hass_cl await hass.async_block_till_done() client = await hass_client() - - with patch( - "homeassistant.components.radio_browser.async_setup_entry", return_value=True - ) as mock_setup: - resp = await client.post("/api/onboarding/core_config") + resp = await client.post("/api/onboarding/core_config") assert resp.status == 200 await hass.async_block_till_done() assert len(hass.config_entries.async_entries("radio_browser")) == 1 - assert len(mock_setup.mock_calls) == 1 async def test_onboarding_core_sets_up_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, rpi + hass, hass_storage, hass_client, aioclient_mock, rpi, mock_default_integrations ): """Test that the core step sets up rpi_power on RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -416,21 +420,18 @@ async def test_onboarding_core_sets_up_rpi_power( client = await hass_client() - with patch( - "homeassistant.components.rpi_power.config_flow.new_under_voltage" - ), patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"): - resp = await client.post("/api/onboarding/core_config") + resp = await client.post("/api/onboarding/core_config") - assert resp.status == 200 + assert resp.status == 200 - await hass.async_block_till_done() + await hass.async_block_till_done() rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") assert rpi_power_state async def test_onboarding_core_no_rpi_power( - hass, hass_storage, hass_client, aioclient_mock, no_rpi + hass, hass_storage, hass_client, aioclient_mock, no_rpi, mock_default_integrations ): """Test that the core step do not set up rpi_power on non RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) @@ -440,14 +441,11 @@ async def test_onboarding_core_no_rpi_power( client = await hass_client() - with patch( - "homeassistant.components.rpi_power.config_flow.new_under_voltage" - ), patch("homeassistant.components.rpi_power.binary_sensor.new_under_voltage"): - resp = await client.post("/api/onboarding/core_config") + resp = await client.post("/api/onboarding/core_config") - assert resp.status == 200 + assert resp.status == 200 - await hass.async_block_till_done() + await hass.async_block_till_done() rpi_power_state = hass.states.get("binary_sensor.rpi_power_status") assert not rpi_power_state From 95d6848726a6680c18d97c03d60ca1c6e824c55a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 10:35:08 +0200 Subject: [PATCH 0744/1054] Mark switch_as_x as a helper integration (#68779) --- homeassistant/components/switch_as_x/manifest.json | 1 + homeassistant/generated/config_flows.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch_as_x/manifest.json b/homeassistant/components/switch_as_x/manifest.json index 358b50bfd1b..921bc09b974 100644 --- a/homeassistant/components/switch_as_x/manifest.json +++ b/homeassistant/components/switch_as_x/manifest.json @@ -1,5 +1,6 @@ { "domain": "switch_as_x", + "integration_type": "helper", "name": "Switch as X", "documentation": "https://www.home-assistant.io/integrations/switch_as_x", "codeowners": [ diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c659f78805e..9c8a3909436 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -328,7 +328,6 @@ FLOWS = { "subaru", "sun", "surepetcare", - "switch_as_x", "switchbot", "switcher_kis", "syncthing", @@ -404,6 +403,7 @@ FLOWS = { "helper": [ "derivative", "min_max", + "switch_as_x", "tod" ] } From 48187cebad2ef6eb18093a59a8a10d6f830edaea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 10:35:52 +0200 Subject: [PATCH 0745/1054] Mark integration as a helper integration (#68778) --- homeassistant/components/integration/manifest.json | 1 + homeassistant/generated/config_flows.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index a36dc51555f..809d1d5b0f5 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -1,5 +1,6 @@ { "domain": "integration", + "integration_type": "helper", "name": "Integration - Riemann sum integral", "documentation": "https://www.home-assistant.io/integrations/integration", "codeowners": [ diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9c8a3909436..ba1ec324d42 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -160,7 +160,6 @@ FLOWS = { "icloud", "ifttt", "insteon", - "integration", "intellifire", "ios", "iotawatt", @@ -402,6 +401,7 @@ FLOWS = { ], "helper": [ "derivative", + "integration", "min_max", "switch_as_x", "tod" From 67d3e84448abeaaed9d21e4f490fb4fd04e77ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 28 Mar 2022 10:51:59 +0200 Subject: [PATCH 0746/1054] Locally patch AirzoneLocalApi in tests (#68770) --- tests/components/airzone/test_config_flow.py | 6 +++--- tests/components/airzone/test_init.py | 2 +- tests/components/airzone/util.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index aeea960c554..a6612d6de9c 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -21,7 +21,7 @@ async def test_form(hass): "homeassistant.components.airzone.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): result = await hass.config_entries.flow.async_init( @@ -57,7 +57,7 @@ async def test_form_duplicated_id(hass): entry.add_to_hass(hass) with patch( - "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): result = await hass.config_entries.flow.async_init( @@ -72,7 +72,7 @@ async def test_connection_error(hass): """Test connection to host error.""" with patch( - "aioairzone.localapi_device.AirzoneLocalApi.validate_airzone", + "homeassistant.components.airzone.AirzoneLocalApi.validate_airzone", side_effect=ClientConnectorError(MagicMock(), MagicMock()), ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py index a529907a246..30e3ce37d6f 100644 --- a/tests/components/airzone/test_init.py +++ b/tests/components/airzone/test_init.py @@ -19,7 +19,7 @@ async def test_unload_entry(hass): config_entry.add_to_hass(hass) with patch( - "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 745daac9269..2f7afb068b3 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -157,7 +157,7 @@ async def async_init_integration( entry.add_to_hass(hass) with patch( - "aioairzone.localapi_device.AirzoneLocalApi.get_hvac", + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", return_value=HVAC_MOCK, ): await hass.config_entries.async_setup(entry.entry_id) From aa7cb087a9870c730af6e954353efecff6612493 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 27 Mar 2022 23:05:50 -1000 Subject: [PATCH 0747/1054] Fix ignoring elkm1 discovery (#68750) --- homeassistant/components/elkm1/config_flow.py | 3 +++ tests/components/elkm1/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index d68fce268a2..05ae860d273 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -166,6 +166,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == host: return self.async_abort(reason="already_in_progress") + # Handled ignored case since _async_current_entries + # is called with include_ignore=False + self._abort_if_unique_id_configured() if not device.port: if discovered_device := await async_discover_device(self.hass, host): self._discovered_device = discovered_device diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 183ab90086c..a1ec9eeff8e 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -27,6 +27,27 @@ ELK_DISCOVERY_INFO = asdict(ELK_DISCOVERY) MODULE = "homeassistant.components.elkm1" +async def test_discovery_ignored_entry(hass): + """Test we abort on ignored entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="aa:bb:cc:dd:ee:ff", + source=config_entries.SOURCE_IGNORE, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=ELK_DISCOVERY_INFO, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + async def test_form_user_with_secure_elk_no_discovery(hass): """Test we can setup a secure elk.""" From 9d14201b13be4f5a5cc5e5f52bba56bfd8fa9694 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 28 Mar 2022 05:15:48 -0400 Subject: [PATCH 0748/1054] Don't create two zwave_js.lock entities for a single device (#68651) --- .../components/zwave_js/discovery.py | 19 +- tests/components/zwave_js/conftest.py | 14 + .../fixtures/lock_home_connect_620_state.json | 11476 ++++++++++++++++ tests/components/zwave_js/test_lock.py | 5 + 4 files changed, 11508 insertions(+), 6 deletions(-) create mode 100644 tests/components/zwave_js/fixtures/lock_home_connect_620_state.json diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index e11b01dab2b..e94f9645444 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -209,6 +209,12 @@ def get_config_parameter_discovery_schema( ) +DOOR_LOCK_CURRENT_MODE_SCHEMA = ZWaveValueDiscoverySchema( + command_class={CommandClass.DOOR_LOCK}, + property={CURRENT_MODE_PROPERTY}, + type={"number"}, +) + SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema( command_class={CommandClass.SWITCH_MULTILEVEL}, property={CURRENT_VALUE_PROPERTY}, @@ -501,16 +507,17 @@ DISCOVERY_SCHEMAS = [ ), # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks + # Door Lock CC + ZWaveDiscoverySchema(platform="lock", primary_value=DOOR_LOCK_CURRENT_MODE_SCHEMA), + # Only discover the Lock CC if the Door Lock CC isn't also present on the node ZWaveDiscoverySchema( platform="lock", primary_value=ZWaveValueDiscoverySchema( - command_class={ - CommandClass.LOCK, - CommandClass.DOOR_LOCK, - }, - property={CURRENT_MODE_PROPERTY, LOCKED_PROPERTY}, - type={"number", "boolean"}, + command_class={CommandClass.LOCK}, + property={LOCKED_PROPERTY}, + type={"boolean"}, ), + absent_values=[DOOR_LOCK_CURRENT_MODE_SCHEMA], ), # door lock door status ZWaveDiscoverySchema( diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index b5ca9f5e611..7cf7ebd7ea2 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -515,6 +515,12 @@ def light_express_controls_ezmultipli_state_fixture(): return json.loads(load_fixture("zwave_js/express_controls_ezmultipli_state.json")) +@pytest.fixture(name="lock_home_connect_620_state", scope="session") +def lock_home_connect_620_state_fixture(): + """Load the Home Connect 620 lock node state fixture data.""" + return json.loads(load_fixture("zwave_js/lock_home_connect_620_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" @@ -1031,3 +1037,11 @@ def express_controls_ezmultipli_fixture(client, express_controls_ezmultipli_stat node = Node(client, copy.deepcopy(express_controls_ezmultipli_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="lock_home_connect_620") +def lock_home_connect_620_fixture(client, lock_home_connect_620_state): + """Mock a Home Connect 620 lock node.""" + node = Node(client, copy.deepcopy(lock_home_connect_620_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/lock_home_connect_620_state.json b/tests/components/zwave_js/fixtures/lock_home_connect_620_state.json new file mode 100644 index 00000000000..286559ef50d --- /dev/null +++ b/tests/components/zwave_js/fixtures/lock_home_connect_620_state.json @@ -0,0 +1,11476 @@ +{ + "nodeId": 14, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "manufacturerId": 144, + "productId": 9128, + "productType": 2065, + "firmwareVersion": "7.20", + "zwavePlusVersion": 2, + "name": "Shed Door Lock", + "deviceConfig": { + "filename": "/usr/src/app/store/.config-db/devices/0x0090/hc620.json", + "isEmbedded": true, + "manufacturer": "Kwikset", + "manufacturerId": 144, + "label": "HC620", + "description": "Home Connect 620 Connected Smart Lock", + "devices": [ + { + "productType": 2065, + "productId": 936 + }, + { + "productType": 2065, + "productId": 5032 + }, + { + "productType": 2065, + "productId": 9128 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Press button \"A\" on the lock interior one time", + "exclusion": "Press button \"A\" one time", + "reset": "1. Remove battery pack.\n2. Press and HOLD \"Program\" button while reinserting the battery pack. Keep holding the button for 30 seconds until the lock beeps and the status LED flashes red", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=product_documents/4279/5069372%20Weiser%20GED1800ss.pdf" + } + }, + "label": "HC620", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 14, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 1, + "label": "Door Lock" + }, + "mandatorySupportedCCs": [32, 118], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 76, + "name": "Door Lock Logging", + "version": 1, + "isSecure": true + }, + { + "id": 85, + "name": "Transport Service", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 3, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": true + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 98, + "name": "Door Lock", + "version": 4, + "isSecure": true + }, + { + "id": 99, + "name": "User Code", + "version": 2, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 4, + "isSecure": true + }, + { + "id": 113, + "name": "Notification", + "version": 8, + "isSecure": true + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": true + }, + { + "id": 115, + "name": "Powerlevel", + "version": 1, + "isSecure": true + }, + { + "id": 118, + "name": "Lock", + "version": 1, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 5, + "isSecure": true + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": true + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": true + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": true + }, + { + "id": 135, + "name": "Indicator", + "version": 3, + "isSecure": true + }, + { + "id": 139, + "name": "Time Parameters", + "version": 1, + "isSecure": true + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": true + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 159, + "name": "Security 2", + "version": 1, + "isSecure": true + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "255": "Secured" + } + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "duration", + "propertyName": "duration", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Remaining duration until target lock mode" + }, + "value": { + "value": 0, + "unit": "seconds" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [true, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "2": "Timed", + "3": "unknown (0x03)" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "autoRelockTime", + "propertyName": "autoRelockTime", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration in seconds until lock returns to secure state", + "min": 0, + "max": 65535 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "holdAndReleaseTime", + "propertyName": "holdAndReleaseTime", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration in seconds the latch stays retracted", + "min": 0, + "max": 65535 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "twistAssist", + "propertyName": "twistAssist", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Twist Assist enabled" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "blockToBlock", + "propertyName": "blockToBlock", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Block-to-block functionality enabled" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "keypadMode", + "propertyName": "keypadMode", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "masterCode", + "propertyName": "masterCode", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "Master Code", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 27, + "propertyName": "userIdStatus", + "propertyKeyName": "27", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (27)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 27, + "propertyName": "userCode", + "propertyKeyName": "27", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (27)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 31, + "propertyName": "userIdStatus", + "propertyKeyName": "31", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (31)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 31, + "propertyName": "userCode", + "propertyKeyName": "31", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (31)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 32, + "propertyName": "userIdStatus", + "propertyKeyName": "32", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (32)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 32, + "propertyName": "userCode", + "propertyKeyName": "32", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (32)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 33, + "propertyName": "userIdStatus", + "propertyKeyName": "33", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (33)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 33, + "propertyName": "userCode", + "propertyKeyName": "33", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (33)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 34, + "propertyName": "userIdStatus", + "propertyKeyName": "34", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (34)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 34, + "propertyName": "userCode", + "propertyKeyName": "34", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (34)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 35, + "propertyName": "userIdStatus", + "propertyKeyName": "35", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (35)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 35, + "propertyName": "userCode", + "propertyKeyName": "35", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (35)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 36, + "propertyName": "userIdStatus", + "propertyKeyName": "36", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (36)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 36, + "propertyName": "userCode", + "propertyKeyName": "36", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (36)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 37, + "propertyName": "userIdStatus", + "propertyKeyName": "37", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (37)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 37, + "propertyName": "userCode", + "propertyKeyName": "37", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (37)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 38, + "propertyName": "userIdStatus", + "propertyKeyName": "38", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (38)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 38, + "propertyName": "userCode", + "propertyKeyName": "38", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (38)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 39, + "propertyName": "userIdStatus", + "propertyKeyName": "39", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (39)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 39, + "propertyName": "userCode", + "propertyKeyName": "39", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (39)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 40, + "propertyName": "userIdStatus", + "propertyKeyName": "40", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (40)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 40, + "propertyName": "userCode", + "propertyKeyName": "40", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (40)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 41, + "propertyName": "userIdStatus", + "propertyKeyName": "41", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (41)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 41, + "propertyName": "userCode", + "propertyKeyName": "41", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (41)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 42, + "propertyName": "userIdStatus", + "propertyKeyName": "42", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (42)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 42, + "propertyName": "userCode", + "propertyKeyName": "42", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (42)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 43, + "propertyName": "userIdStatus", + "propertyKeyName": "43", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (43)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 43, + "propertyName": "userCode", + "propertyKeyName": "43", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (43)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 44, + "propertyName": "userIdStatus", + "propertyKeyName": "44", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (44)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 44, + "propertyName": "userCode", + "propertyKeyName": "44", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (44)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 45, + "propertyName": "userIdStatus", + "propertyKeyName": "45", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (45)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 45, + "propertyName": "userCode", + "propertyKeyName": "45", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (45)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 46, + "propertyName": "userIdStatus", + "propertyKeyName": "46", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (46)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 46, + "propertyName": "userCode", + "propertyKeyName": "46", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (46)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 47, + "propertyName": "userIdStatus", + "propertyKeyName": "47", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (47)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 47, + "propertyName": "userCode", + "propertyKeyName": "47", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (47)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 48, + "propertyName": "userIdStatus", + "propertyKeyName": "48", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (48)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 48, + "propertyName": "userCode", + "propertyKeyName": "48", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (48)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 49, + "propertyName": "userIdStatus", + "propertyKeyName": "49", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (49)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 49, + "propertyName": "userCode", + "propertyKeyName": "49", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (49)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 50, + "propertyName": "userIdStatus", + "propertyKeyName": "50", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (50)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 50, + "propertyName": "userCode", + "propertyKeyName": "50", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (50)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 51, + "propertyName": "userIdStatus", + "propertyKeyName": "51", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (51)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 51, + "propertyName": "userCode", + "propertyKeyName": "51", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (51)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 52, + "propertyName": "userIdStatus", + "propertyKeyName": "52", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (52)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 52, + "propertyName": "userCode", + "propertyKeyName": "52", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (52)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 53, + "propertyName": "userIdStatus", + "propertyKeyName": "53", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (53)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 53, + "propertyName": "userCode", + "propertyKeyName": "53", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (53)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 54, + "propertyName": "userIdStatus", + "propertyKeyName": "54", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (54)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 54, + "propertyName": "userCode", + "propertyKeyName": "54", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (54)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 55, + "propertyName": "userIdStatus", + "propertyKeyName": "55", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (55)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 55, + "propertyName": "userCode", + "propertyKeyName": "55", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (55)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 56, + "propertyName": "userIdStatus", + "propertyKeyName": "56", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (56)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 56, + "propertyName": "userCode", + "propertyKeyName": "56", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (56)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 57, + "propertyName": "userIdStatus", + "propertyKeyName": "57", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (57)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 57, + "propertyName": "userCode", + "propertyKeyName": "57", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (57)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 58, + "propertyName": "userIdStatus", + "propertyKeyName": "58", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (58)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 58, + "propertyName": "userCode", + "propertyKeyName": "58", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (58)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 59, + "propertyName": "userIdStatus", + "propertyKeyName": "59", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (59)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 59, + "propertyName": "userCode", + "propertyKeyName": "59", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (59)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 60, + "propertyName": "userIdStatus", + "propertyKeyName": "60", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (60)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 60, + "propertyName": "userCode", + "propertyKeyName": "60", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (60)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 61, + "propertyName": "userIdStatus", + "propertyKeyName": "61", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (61)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 61, + "propertyName": "userCode", + "propertyKeyName": "61", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (61)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 62, + "propertyName": "userIdStatus", + "propertyKeyName": "62", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (62)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 62, + "propertyName": "userCode", + "propertyKeyName": "62", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (62)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 63, + "propertyName": "userIdStatus", + "propertyKeyName": "63", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (63)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 63, + "propertyName": "userCode", + "propertyKeyName": "63", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (63)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 64, + "propertyName": "userIdStatus", + "propertyKeyName": "64", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (64)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 64, + "propertyName": "userCode", + "propertyKeyName": "64", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (64)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 65, + "propertyName": "userIdStatus", + "propertyKeyName": "65", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (65)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 65, + "propertyName": "userCode", + "propertyKeyName": "65", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (65)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 66, + "propertyName": "userIdStatus", + "propertyKeyName": "66", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (66)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 66, + "propertyName": "userCode", + "propertyKeyName": "66", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (66)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 67, + "propertyName": "userIdStatus", + "propertyKeyName": "67", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (67)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 67, + "propertyName": "userCode", + "propertyKeyName": "67", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (67)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 68, + "propertyName": "userIdStatus", + "propertyKeyName": "68", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (68)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 68, + "propertyName": "userCode", + "propertyKeyName": "68", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (68)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 69, + "propertyName": "userIdStatus", + "propertyKeyName": "69", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (69)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 69, + "propertyName": "userCode", + "propertyKeyName": "69", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (69)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 70, + "propertyName": "userIdStatus", + "propertyKeyName": "70", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (70)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 70, + "propertyName": "userCode", + "propertyKeyName": "70", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (70)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 71, + "propertyName": "userIdStatus", + "propertyKeyName": "71", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (71)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 71, + "propertyName": "userCode", + "propertyKeyName": "71", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (71)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 72, + "propertyName": "userIdStatus", + "propertyKeyName": "72", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (72)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 72, + "propertyName": "userCode", + "propertyKeyName": "72", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (72)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 73, + "propertyName": "userIdStatus", + "propertyKeyName": "73", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (73)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 73, + "propertyName": "userCode", + "propertyKeyName": "73", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (73)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 74, + "propertyName": "userIdStatus", + "propertyKeyName": "74", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (74)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 74, + "propertyName": "userCode", + "propertyKeyName": "74", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (74)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 75, + "propertyName": "userIdStatus", + "propertyKeyName": "75", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (75)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 75, + "propertyName": "userCode", + "propertyKeyName": "75", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (75)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 76, + "propertyName": "userIdStatus", + "propertyKeyName": "76", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (76)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 76, + "propertyName": "userCode", + "propertyKeyName": "76", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (76)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 77, + "propertyName": "userIdStatus", + "propertyKeyName": "77", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (77)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 77, + "propertyName": "userCode", + "propertyKeyName": "77", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (77)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 78, + "propertyName": "userIdStatus", + "propertyKeyName": "78", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (78)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 78, + "propertyName": "userCode", + "propertyKeyName": "78", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (78)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 79, + "propertyName": "userIdStatus", + "propertyKeyName": "79", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (79)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 79, + "propertyName": "userCode", + "propertyKeyName": "79", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (79)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 80, + "propertyName": "userIdStatus", + "propertyKeyName": "80", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (80)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 80, + "propertyName": "userCode", + "propertyKeyName": "80", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (80)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 81, + "propertyName": "userIdStatus", + "propertyKeyName": "81", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (81)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 81, + "propertyName": "userCode", + "propertyKeyName": "81", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (81)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 82, + "propertyName": "userIdStatus", + "propertyKeyName": "82", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (82)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 82, + "propertyName": "userCode", + "propertyKeyName": "82", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (82)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 83, + "propertyName": "userIdStatus", + "propertyKeyName": "83", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (83)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 83, + "propertyName": "userCode", + "propertyKeyName": "83", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (83)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 84, + "propertyName": "userIdStatus", + "propertyKeyName": "84", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (84)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 84, + "propertyName": "userCode", + "propertyKeyName": "84", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (84)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 85, + "propertyName": "userIdStatus", + "propertyKeyName": "85", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (85)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 85, + "propertyName": "userCode", + "propertyKeyName": "85", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (85)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 86, + "propertyName": "userIdStatus", + "propertyKeyName": "86", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (86)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 86, + "propertyName": "userCode", + "propertyKeyName": "86", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (86)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 87, + "propertyName": "userIdStatus", + "propertyKeyName": "87", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (87)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 87, + "propertyName": "userCode", + "propertyKeyName": "87", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (87)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 88, + "propertyName": "userIdStatus", + "propertyKeyName": "88", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (88)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 88, + "propertyName": "userCode", + "propertyKeyName": "88", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (88)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 89, + "propertyName": "userIdStatus", + "propertyKeyName": "89", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (89)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 89, + "propertyName": "userCode", + "propertyKeyName": "89", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (89)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 90, + "propertyName": "userIdStatus", + "propertyKeyName": "90", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (90)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 90, + "propertyName": "userCode", + "propertyKeyName": "90", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (90)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 91, + "propertyName": "userIdStatus", + "propertyKeyName": "91", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (91)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 91, + "propertyName": "userCode", + "propertyKeyName": "91", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (91)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 92, + "propertyName": "userIdStatus", + "propertyKeyName": "92", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (92)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 92, + "propertyName": "userCode", + "propertyKeyName": "92", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (92)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 93, + "propertyName": "userIdStatus", + "propertyKeyName": "93", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (93)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 93, + "propertyName": "userCode", + "propertyKeyName": "93", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (93)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 94, + "propertyName": "userIdStatus", + "propertyKeyName": "94", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (94)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 94, + "propertyName": "userCode", + "propertyKeyName": "94", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (94)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 95, + "propertyName": "userIdStatus", + "propertyKeyName": "95", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (95)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 95, + "propertyName": "userCode", + "propertyKeyName": "95", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (95)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 96, + "propertyName": "userIdStatus", + "propertyKeyName": "96", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (96)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 96, + "propertyName": "userCode", + "propertyKeyName": "96", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (96)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 97, + "propertyName": "userIdStatus", + "propertyKeyName": "97", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (97)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 97, + "propertyName": "userCode", + "propertyKeyName": "97", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (97)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 98, + "propertyName": "userIdStatus", + "propertyKeyName": "98", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (98)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 98, + "propertyName": "userCode", + "propertyKeyName": "98", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (98)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 99, + "propertyName": "userIdStatus", + "propertyKeyName": "99", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (99)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 99, + "propertyName": "userCode", + "propertyKeyName": "99", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (99)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 100, + "propertyName": "userIdStatus", + "propertyKeyName": "100", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (100)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 100, + "propertyName": "userCode", + "propertyKeyName": "100", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (100)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 101, + "propertyName": "userIdStatus", + "propertyKeyName": "101", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (101)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 101, + "propertyName": "userCode", + "propertyKeyName": "101", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (101)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 102, + "propertyName": "userIdStatus", + "propertyKeyName": "102", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (102)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 102, + "propertyName": "userCode", + "propertyKeyName": "102", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (102)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 103, + "propertyName": "userIdStatus", + "propertyKeyName": "103", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (103)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 103, + "propertyName": "userCode", + "propertyKeyName": "103", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (103)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 104, + "propertyName": "userIdStatus", + "propertyKeyName": "104", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (104)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 104, + "propertyName": "userCode", + "propertyKeyName": "104", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (104)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 105, + "propertyName": "userIdStatus", + "propertyKeyName": "105", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (105)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 105, + "propertyName": "userCode", + "propertyKeyName": "105", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (105)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 106, + "propertyName": "userIdStatus", + "propertyKeyName": "106", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (106)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 106, + "propertyName": "userCode", + "propertyKeyName": "106", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (106)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 107, + "propertyName": "userIdStatus", + "propertyKeyName": "107", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (107)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 107, + "propertyName": "userCode", + "propertyKeyName": "107", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (107)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 108, + "propertyName": "userIdStatus", + "propertyKeyName": "108", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (108)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 108, + "propertyName": "userCode", + "propertyKeyName": "108", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (108)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 109, + "propertyName": "userIdStatus", + "propertyKeyName": "109", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (109)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 109, + "propertyName": "userCode", + "propertyKeyName": "109", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (109)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 110, + "propertyName": "userIdStatus", + "propertyKeyName": "110", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (110)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 110, + "propertyName": "userCode", + "propertyKeyName": "110", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (110)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 111, + "propertyName": "userIdStatus", + "propertyKeyName": "111", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (111)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 111, + "propertyName": "userCode", + "propertyKeyName": "111", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (111)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 112, + "propertyName": "userIdStatus", + "propertyKeyName": "112", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (112)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 112, + "propertyName": "userCode", + "propertyKeyName": "112", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (112)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 113, + "propertyName": "userIdStatus", + "propertyKeyName": "113", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (113)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 113, + "propertyName": "userCode", + "propertyKeyName": "113", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (113)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 114, + "propertyName": "userIdStatus", + "propertyKeyName": "114", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (114)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 114, + "propertyName": "userCode", + "propertyKeyName": "114", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (114)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 115, + "propertyName": "userIdStatus", + "propertyKeyName": "115", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (115)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 115, + "propertyName": "userCode", + "propertyKeyName": "115", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (115)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 116, + "propertyName": "userIdStatus", + "propertyKeyName": "116", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (116)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 116, + "propertyName": "userCode", + "propertyKeyName": "116", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (116)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 117, + "propertyName": "userIdStatus", + "propertyKeyName": "117", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (117)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 117, + "propertyName": "userCode", + "propertyKeyName": "117", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (117)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 118, + "propertyName": "userIdStatus", + "propertyKeyName": "118", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (118)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 118, + "propertyName": "userCode", + "propertyKeyName": "118", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (118)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 119, + "propertyName": "userIdStatus", + "propertyKeyName": "119", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (119)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 119, + "propertyName": "userCode", + "propertyKeyName": "119", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (119)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 120, + "propertyName": "userIdStatus", + "propertyKeyName": "120", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (120)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 120, + "propertyName": "userCode", + "propertyKeyName": "120", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (120)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 121, + "propertyName": "userIdStatus", + "propertyKeyName": "121", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (121)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 121, + "propertyName": "userCode", + "propertyKeyName": "121", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (121)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 122, + "propertyName": "userIdStatus", + "propertyKeyName": "122", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (122)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 122, + "propertyName": "userCode", + "propertyKeyName": "122", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (122)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 123, + "propertyName": "userIdStatus", + "propertyKeyName": "123", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (123)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 123, + "propertyName": "userCode", + "propertyKeyName": "123", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (123)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 124, + "propertyName": "userIdStatus", + "propertyKeyName": "124", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (124)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 124, + "propertyName": "userCode", + "propertyKeyName": "124", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (124)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 125, + "propertyName": "userIdStatus", + "propertyKeyName": "125", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (125)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 125, + "propertyName": "userCode", + "propertyKeyName": "125", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (125)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 126, + "propertyName": "userIdStatus", + "propertyKeyName": "126", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (126)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 126, + "propertyName": "userCode", + "propertyKeyName": "126", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (126)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 127, + "propertyName": "userIdStatus", + "propertyKeyName": "127", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (127)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 127, + "propertyName": "userCode", + "propertyKeyName": "127", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (127)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 128, + "propertyName": "userIdStatus", + "propertyKeyName": "128", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (128)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 128, + "propertyName": "userCode", + "propertyKeyName": "128", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (128)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 129, + "propertyName": "userIdStatus", + "propertyKeyName": "129", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (129)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 129, + "propertyName": "userCode", + "propertyKeyName": "129", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (129)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 130, + "propertyName": "userIdStatus", + "propertyKeyName": "130", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (130)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 130, + "propertyName": "userCode", + "propertyKeyName": "130", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (130)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 131, + "propertyName": "userIdStatus", + "propertyKeyName": "131", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (131)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 131, + "propertyName": "userCode", + "propertyKeyName": "131", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (131)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 132, + "propertyName": "userIdStatus", + "propertyKeyName": "132", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (132)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 132, + "propertyName": "userCode", + "propertyKeyName": "132", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (132)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 133, + "propertyName": "userIdStatus", + "propertyKeyName": "133", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (133)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 133, + "propertyName": "userCode", + "propertyKeyName": "133", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (133)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 134, + "propertyName": "userIdStatus", + "propertyKeyName": "134", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (134)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 134, + "propertyName": "userCode", + "propertyKeyName": "134", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (134)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 135, + "propertyName": "userIdStatus", + "propertyKeyName": "135", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (135)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 135, + "propertyName": "userCode", + "propertyKeyName": "135", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (135)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 136, + "propertyName": "userIdStatus", + "propertyKeyName": "136", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (136)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 136, + "propertyName": "userCode", + "propertyKeyName": "136", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (136)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 137, + "propertyName": "userIdStatus", + "propertyKeyName": "137", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (137)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 137, + "propertyName": "userCode", + "propertyKeyName": "137", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (137)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 138, + "propertyName": "userIdStatus", + "propertyKeyName": "138", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (138)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 138, + "propertyName": "userCode", + "propertyKeyName": "138", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (138)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 139, + "propertyName": "userIdStatus", + "propertyKeyName": "139", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (139)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 139, + "propertyName": "userCode", + "propertyKeyName": "139", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (139)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 140, + "propertyName": "userIdStatus", + "propertyKeyName": "140", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (140)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 140, + "propertyName": "userCode", + "propertyKeyName": "140", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (140)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 141, + "propertyName": "userIdStatus", + "propertyKeyName": "141", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (141)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 141, + "propertyName": "userCode", + "propertyKeyName": "141", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (141)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 142, + "propertyName": "userIdStatus", + "propertyKeyName": "142", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (142)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 142, + "propertyName": "userCode", + "propertyKeyName": "142", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (142)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 143, + "propertyName": "userIdStatus", + "propertyKeyName": "143", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (143)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 143, + "propertyName": "userCode", + "propertyKeyName": "143", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (143)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 144, + "propertyName": "userIdStatus", + "propertyKeyName": "144", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (144)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 144, + "propertyName": "userCode", + "propertyKeyName": "144", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (144)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 145, + "propertyName": "userIdStatus", + "propertyKeyName": "145", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (145)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 145, + "propertyName": "userCode", + "propertyKeyName": "145", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (145)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 146, + "propertyName": "userIdStatus", + "propertyKeyName": "146", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (146)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 146, + "propertyName": "userCode", + "propertyKeyName": "146", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (146)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 147, + "propertyName": "userIdStatus", + "propertyKeyName": "147", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (147)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 147, + "propertyName": "userCode", + "propertyKeyName": "147", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (147)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 148, + "propertyName": "userIdStatus", + "propertyKeyName": "148", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (148)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 148, + "propertyName": "userCode", + "propertyKeyName": "148", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (148)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 149, + "propertyName": "userIdStatus", + "propertyKeyName": "149", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (149)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 149, + "propertyName": "userCode", + "propertyKeyName": "149", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (149)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 150, + "propertyName": "userIdStatus", + "propertyKeyName": "150", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (150)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 150, + "propertyName": "userCode", + "propertyKeyName": "150", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (150)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 151, + "propertyName": "userIdStatus", + "propertyKeyName": "151", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (151)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 151, + "propertyName": "userCode", + "propertyKeyName": "151", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (151)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 152, + "propertyName": "userIdStatus", + "propertyKeyName": "152", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (152)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 152, + "propertyName": "userCode", + "propertyKeyName": "152", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (152)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 153, + "propertyName": "userIdStatus", + "propertyKeyName": "153", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (153)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 153, + "propertyName": "userCode", + "propertyKeyName": "153", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (153)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 154, + "propertyName": "userIdStatus", + "propertyKeyName": "154", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (154)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 154, + "propertyName": "userCode", + "propertyKeyName": "154", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (154)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 155, + "propertyName": "userIdStatus", + "propertyKeyName": "155", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (155)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 155, + "propertyName": "userCode", + "propertyKeyName": "155", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (155)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 156, + "propertyName": "userIdStatus", + "propertyKeyName": "156", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (156)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 156, + "propertyName": "userCode", + "propertyKeyName": "156", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (156)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 157, + "propertyName": "userIdStatus", + "propertyKeyName": "157", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (157)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 157, + "propertyName": "userCode", + "propertyKeyName": "157", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (157)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 158, + "propertyName": "userIdStatus", + "propertyKeyName": "158", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (158)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 158, + "propertyName": "userCode", + "propertyKeyName": "158", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (158)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 159, + "propertyName": "userIdStatus", + "propertyKeyName": "159", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (159)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 159, + "propertyName": "userCode", + "propertyKeyName": "159", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (159)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 160, + "propertyName": "userIdStatus", + "propertyKeyName": "160", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (160)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 160, + "propertyName": "userCode", + "propertyKeyName": "160", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (160)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 161, + "propertyName": "userIdStatus", + "propertyKeyName": "161", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (161)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 161, + "propertyName": "userCode", + "propertyKeyName": "161", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (161)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 162, + "propertyName": "userIdStatus", + "propertyKeyName": "162", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (162)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 162, + "propertyName": "userCode", + "propertyKeyName": "162", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (162)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 163, + "propertyName": "userIdStatus", + "propertyKeyName": "163", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (163)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 163, + "propertyName": "userCode", + "propertyKeyName": "163", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (163)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 164, + "propertyName": "userIdStatus", + "propertyKeyName": "164", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (164)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 164, + "propertyName": "userCode", + "propertyKeyName": "164", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (164)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 165, + "propertyName": "userIdStatus", + "propertyKeyName": "165", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (165)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 165, + "propertyName": "userCode", + "propertyKeyName": "165", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (165)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 166, + "propertyName": "userIdStatus", + "propertyKeyName": "166", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (166)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 166, + "propertyName": "userCode", + "propertyKeyName": "166", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (166)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 167, + "propertyName": "userIdStatus", + "propertyKeyName": "167", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (167)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 167, + "propertyName": "userCode", + "propertyKeyName": "167", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (167)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 168, + "propertyName": "userIdStatus", + "propertyKeyName": "168", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (168)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 168, + "propertyName": "userCode", + "propertyKeyName": "168", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (168)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 169, + "propertyName": "userIdStatus", + "propertyKeyName": "169", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (169)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 169, + "propertyName": "userCode", + "propertyKeyName": "169", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (169)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 170, + "propertyName": "userIdStatus", + "propertyKeyName": "170", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (170)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 170, + "propertyName": "userCode", + "propertyKeyName": "170", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (170)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 171, + "propertyName": "userIdStatus", + "propertyKeyName": "171", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (171)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 171, + "propertyName": "userCode", + "propertyKeyName": "171", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (171)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 172, + "propertyName": "userIdStatus", + "propertyKeyName": "172", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (172)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 172, + "propertyName": "userCode", + "propertyKeyName": "172", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (172)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 173, + "propertyName": "userIdStatus", + "propertyKeyName": "173", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (173)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 173, + "propertyName": "userCode", + "propertyKeyName": "173", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (173)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 174, + "propertyName": "userIdStatus", + "propertyKeyName": "174", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (174)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 174, + "propertyName": "userCode", + "propertyKeyName": "174", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (174)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 175, + "propertyName": "userIdStatus", + "propertyKeyName": "175", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (175)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 175, + "propertyName": "userCode", + "propertyKeyName": "175", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (175)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 176, + "propertyName": "userIdStatus", + "propertyKeyName": "176", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (176)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 176, + "propertyName": "userCode", + "propertyKeyName": "176", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (176)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 177, + "propertyName": "userIdStatus", + "propertyKeyName": "177", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (177)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 177, + "propertyName": "userCode", + "propertyKeyName": "177", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (177)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 178, + "propertyName": "userIdStatus", + "propertyKeyName": "178", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (178)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 178, + "propertyName": "userCode", + "propertyKeyName": "178", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (178)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 179, + "propertyName": "userIdStatus", + "propertyKeyName": "179", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (179)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 179, + "propertyName": "userCode", + "propertyKeyName": "179", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (179)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 180, + "propertyName": "userIdStatus", + "propertyKeyName": "180", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (180)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 180, + "propertyName": "userCode", + "propertyKeyName": "180", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (180)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 181, + "propertyName": "userIdStatus", + "propertyKeyName": "181", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (181)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 181, + "propertyName": "userCode", + "propertyKeyName": "181", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (181)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 182, + "propertyName": "userIdStatus", + "propertyKeyName": "182", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (182)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 182, + "propertyName": "userCode", + "propertyKeyName": "182", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (182)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 183, + "propertyName": "userIdStatus", + "propertyKeyName": "183", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (183)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 183, + "propertyName": "userCode", + "propertyKeyName": "183", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (183)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 184, + "propertyName": "userIdStatus", + "propertyKeyName": "184", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (184)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 184, + "propertyName": "userCode", + "propertyKeyName": "184", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (184)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 185, + "propertyName": "userIdStatus", + "propertyKeyName": "185", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (185)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 185, + "propertyName": "userCode", + "propertyKeyName": "185", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (185)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 186, + "propertyName": "userIdStatus", + "propertyKeyName": "186", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (186)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 186, + "propertyName": "userCode", + "propertyKeyName": "186", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (186)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 187, + "propertyName": "userIdStatus", + "propertyKeyName": "187", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (187)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 187, + "propertyName": "userCode", + "propertyKeyName": "187", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (187)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 188, + "propertyName": "userIdStatus", + "propertyKeyName": "188", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (188)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 188, + "propertyName": "userCode", + "propertyKeyName": "188", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (188)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 189, + "propertyName": "userIdStatus", + "propertyKeyName": "189", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (189)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 189, + "propertyName": "userCode", + "propertyKeyName": "189", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (189)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 190, + "propertyName": "userIdStatus", + "propertyKeyName": "190", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (190)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 190, + "propertyName": "userCode", + "propertyKeyName": "190", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (190)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 191, + "propertyName": "userIdStatus", + "propertyKeyName": "191", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (191)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 191, + "propertyName": "userCode", + "propertyKeyName": "191", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (191)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 192, + "propertyName": "userIdStatus", + "propertyKeyName": "192", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (192)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 192, + "propertyName": "userCode", + "propertyKeyName": "192", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (192)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 193, + "propertyName": "userIdStatus", + "propertyKeyName": "193", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (193)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 193, + "propertyName": "userCode", + "propertyKeyName": "193", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (193)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 194, + "propertyName": "userIdStatus", + "propertyKeyName": "194", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (194)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 194, + "propertyName": "userCode", + "propertyKeyName": "194", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (194)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 195, + "propertyName": "userIdStatus", + "propertyKeyName": "195", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (195)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 195, + "propertyName": "userCode", + "propertyKeyName": "195", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (195)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 196, + "propertyName": "userIdStatus", + "propertyKeyName": "196", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (196)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 196, + "propertyName": "userCode", + "propertyKeyName": "196", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (196)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 197, + "propertyName": "userIdStatus", + "propertyKeyName": "197", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (197)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 197, + "propertyName": "userCode", + "propertyKeyName": "197", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (197)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 198, + "propertyName": "userIdStatus", + "propertyKeyName": "198", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (198)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 198, + "propertyName": "userCode", + "propertyKeyName": "198", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (198)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 199, + "propertyName": "userIdStatus", + "propertyKeyName": "199", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (199)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 199, + "propertyName": "userCode", + "propertyKeyName": "199", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (199)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 200, + "propertyName": "userIdStatus", + "propertyKeyName": "200", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (200)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 200, + "propertyName": "userCode", + "propertyKeyName": "200", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (200)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 201, + "propertyName": "userIdStatus", + "propertyKeyName": "201", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (201)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 201, + "propertyName": "userCode", + "propertyKeyName": "201", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (201)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 202, + "propertyName": "userIdStatus", + "propertyKeyName": "202", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (202)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 202, + "propertyName": "userCode", + "propertyKeyName": "202", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (202)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 203, + "propertyName": "userIdStatus", + "propertyKeyName": "203", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (203)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 203, + "propertyName": "userCode", + "propertyKeyName": "203", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (203)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 204, + "propertyName": "userIdStatus", + "propertyKeyName": "204", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (204)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 204, + "propertyName": "userCode", + "propertyKeyName": "204", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (204)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 205, + "propertyName": "userIdStatus", + "propertyKeyName": "205", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (205)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 205, + "propertyName": "userCode", + "propertyKeyName": "205", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (205)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 206, + "propertyName": "userIdStatus", + "propertyKeyName": "206", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (206)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 206, + "propertyName": "userCode", + "propertyKeyName": "206", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (206)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 207, + "propertyName": "userIdStatus", + "propertyKeyName": "207", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (207)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 207, + "propertyName": "userCode", + "propertyKeyName": "207", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (207)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 208, + "propertyName": "userIdStatus", + "propertyKeyName": "208", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (208)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 208, + "propertyName": "userCode", + "propertyKeyName": "208", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (208)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 209, + "propertyName": "userIdStatus", + "propertyKeyName": "209", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (209)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 209, + "propertyName": "userCode", + "propertyKeyName": "209", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (209)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 210, + "propertyName": "userIdStatus", + "propertyKeyName": "210", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (210)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 210, + "propertyName": "userCode", + "propertyKeyName": "210", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (210)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 211, + "propertyName": "userIdStatus", + "propertyKeyName": "211", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (211)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 211, + "propertyName": "userCode", + "propertyKeyName": "211", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (211)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 212, + "propertyName": "userIdStatus", + "propertyKeyName": "212", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (212)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 212, + "propertyName": "userCode", + "propertyKeyName": "212", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (212)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 213, + "propertyName": "userIdStatus", + "propertyKeyName": "213", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (213)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 213, + "propertyName": "userCode", + "propertyKeyName": "213", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (213)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 214, + "propertyName": "userIdStatus", + "propertyKeyName": "214", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (214)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 214, + "propertyName": "userCode", + "propertyKeyName": "214", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (214)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 215, + "propertyName": "userIdStatus", + "propertyKeyName": "215", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (215)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 215, + "propertyName": "userCode", + "propertyKeyName": "215", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (215)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 216, + "propertyName": "userIdStatus", + "propertyKeyName": "216", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (216)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 216, + "propertyName": "userCode", + "propertyKeyName": "216", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (216)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 217, + "propertyName": "userIdStatus", + "propertyKeyName": "217", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (217)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 217, + "propertyName": "userCode", + "propertyKeyName": "217", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (217)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 218, + "propertyName": "userIdStatus", + "propertyKeyName": "218", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (218)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 218, + "propertyName": "userCode", + "propertyKeyName": "218", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (218)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 219, + "propertyName": "userIdStatus", + "propertyKeyName": "219", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (219)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 219, + "propertyName": "userCode", + "propertyKeyName": "219", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (219)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 220, + "propertyName": "userIdStatus", + "propertyKeyName": "220", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (220)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 220, + "propertyName": "userCode", + "propertyKeyName": "220", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (220)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 221, + "propertyName": "userIdStatus", + "propertyKeyName": "221", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (221)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 221, + "propertyName": "userCode", + "propertyKeyName": "221", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (221)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 222, + "propertyName": "userIdStatus", + "propertyKeyName": "222", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (222)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 222, + "propertyName": "userCode", + "propertyKeyName": "222", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (222)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 223, + "propertyName": "userIdStatus", + "propertyKeyName": "223", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (223)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 223, + "propertyName": "userCode", + "propertyKeyName": "223", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (223)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 224, + "propertyName": "userIdStatus", + "propertyKeyName": "224", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (224)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 224, + "propertyName": "userCode", + "propertyKeyName": "224", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (224)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 225, + "propertyName": "userIdStatus", + "propertyKeyName": "225", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (225)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 225, + "propertyName": "userCode", + "propertyKeyName": "225", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (225)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 226, + "propertyName": "userIdStatus", + "propertyKeyName": "226", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (226)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 226, + "propertyName": "userCode", + "propertyKeyName": "226", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (226)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 227, + "propertyName": "userIdStatus", + "propertyKeyName": "227", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (227)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 227, + "propertyName": "userCode", + "propertyKeyName": "227", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (227)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 228, + "propertyName": "userIdStatus", + "propertyKeyName": "228", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (228)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 228, + "propertyName": "userCode", + "propertyKeyName": "228", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (228)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 229, + "propertyName": "userIdStatus", + "propertyKeyName": "229", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (229)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 229, + "propertyName": "userCode", + "propertyKeyName": "229", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (229)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 230, + "propertyName": "userIdStatus", + "propertyKeyName": "230", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (230)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 230, + "propertyName": "userCode", + "propertyKeyName": "230", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (230)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 231, + "propertyName": "userIdStatus", + "propertyKeyName": "231", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (231)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 231, + "propertyName": "userCode", + "propertyKeyName": "231", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (231)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 232, + "propertyName": "userIdStatus", + "propertyKeyName": "232", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (232)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 232, + "propertyName": "userCode", + "propertyKeyName": "232", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (232)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 233, + "propertyName": "userIdStatus", + "propertyKeyName": "233", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (233)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 233, + "propertyName": "userCode", + "propertyKeyName": "233", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (233)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 234, + "propertyName": "userIdStatus", + "propertyKeyName": "234", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (234)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 234, + "propertyName": "userCode", + "propertyKeyName": "234", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (234)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 235, + "propertyName": "userIdStatus", + "propertyKeyName": "235", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (235)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 235, + "propertyName": "userCode", + "propertyKeyName": "235", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (235)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 236, + "propertyName": "userIdStatus", + "propertyKeyName": "236", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (236)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 236, + "propertyName": "userCode", + "propertyKeyName": "236", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (236)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 237, + "propertyName": "userIdStatus", + "propertyKeyName": "237", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (237)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 237, + "propertyName": "userCode", + "propertyKeyName": "237", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (237)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 238, + "propertyName": "userIdStatus", + "propertyKeyName": "238", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (238)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 238, + "propertyName": "userCode", + "propertyKeyName": "238", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (238)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 239, + "propertyName": "userIdStatus", + "propertyKeyName": "239", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (239)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 239, + "propertyName": "userCode", + "propertyKeyName": "239", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (239)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 240, + "propertyName": "userIdStatus", + "propertyKeyName": "240", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (240)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 240, + "propertyName": "userCode", + "propertyKeyName": "240", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (240)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 241, + "propertyName": "userIdStatus", + "propertyKeyName": "241", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (241)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 241, + "propertyName": "userCode", + "propertyKeyName": "241", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (241)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 242, + "propertyName": "userIdStatus", + "propertyKeyName": "242", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (242)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 242, + "propertyName": "userCode", + "propertyKeyName": "242", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (242)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 243, + "propertyName": "userIdStatus", + "propertyKeyName": "243", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (243)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 243, + "propertyName": "userCode", + "propertyKeyName": "243", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (243)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 244, + "propertyName": "userIdStatus", + "propertyKeyName": "244", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (244)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 244, + "propertyName": "userCode", + "propertyKeyName": "244", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (244)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 245, + "propertyName": "userIdStatus", + "propertyKeyName": "245", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (245)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 245, + "propertyName": "userCode", + "propertyKeyName": "245", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (245)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 246, + "propertyName": "userIdStatus", + "propertyKeyName": "246", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (246)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 246, + "propertyName": "userCode", + "propertyKeyName": "246", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (246)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 247, + "propertyName": "userIdStatus", + "propertyKeyName": "247", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (247)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 247, + "propertyName": "userCode", + "propertyKeyName": "247", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (247)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 248, + "propertyName": "userIdStatus", + "propertyKeyName": "248", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (248)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 248, + "propertyName": "userCode", + "propertyKeyName": "248", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (248)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 249, + "propertyName": "userIdStatus", + "propertyKeyName": "249", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (249)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 249, + "propertyName": "userCode", + "propertyKeyName": "249", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (249)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 250, + "propertyName": "userIdStatus", + "propertyKeyName": "250", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (250)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + "3": "Messaging", + "4": "PassageMode" + } + } + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 250, + "propertyName": "userCode", + "propertyKeyName": "250", + "ccVersion": 2, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (250)", + "minLength": 4, + "maxLength": 10 + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Lock Direction", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "Indicates the direction of the lock. Set to 1 (Right handed lock) to initiate direction detection.", + "label": "Lock Direction", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "Unknown latch position", + "1": "Right handed lock", + "2": "Left handed lock" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 46, + "propertyName": "Motor Resistance", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Motor Resistance", + "default": 0, + "min": 0, + "max": 255, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 750900 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Status LED", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Status LED", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Buzzer", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Buzzer", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "User Program Button", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "User Program Button", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Secure Screen", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "description": "Control the secure screen functionality (on touch locks only)", + "label": "Secure Screen", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyName": "Reset to Factory Default Settings", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Reset to Factory Default Settings", + "default": 0, + "min": 0, + "max": 2, + "states": { + "0": "Normal Operation", + "1": "Reset to Factory Defaults", + "2": "Perform a modified factory reset" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Power status", + "propertyName": "Power Management", + "propertyKeyName": "Power status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Power status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "1": "Power has been applied" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Cover status", + "ccSpecific": { + "notificationType": 7 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "3": "Tampering, product cover removed" + } + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery maintenance status", + "ccSpecific": { + "notificationType": 8 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "10": "Replace battery soon", + "11": "Replace battery now" + } + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 144 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 2065 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 9128 + }, + { + "endpoint": 0, + "commandClass": 118, + "commandClassName": "Lock", + "property": "locked", + "propertyName": "locked", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "description": "Whether the lock is locked", + "label": "Locked" + }, + "value": true + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + } + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "7.13" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["7.20", "69.32"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "SDK version" + }, + "value": "7.13.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API version" + }, + "value": "10.13.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave application framework API build number" + }, + "value": 373 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API version" + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Serial API build number" + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "7.13.8" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol build number" + }, + "value": 373 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application version" + }, + "value": "7.20.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Application build number" + }, + "value": 43707 + }, + { + "endpoint": 0, + "commandClass": 139, + "commandClassName": "Time Parameters", + "property": "dateAndTime", + "propertyName": "dateAndTime", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 7, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 1, + "label": "Door Lock" + }, + "mandatorySupportedCCs": [32, 118], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0090:0x0811:0x23a8:7.20", + "statistics": { + "commandsTX": 1, + "commandsRX": 0, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 + }, + "highestSecurityClass": 2, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py index 9a0735d3dc6..2bf4cff8b5b 100644 --- a/tests/components/zwave_js/test_lock.py +++ b/tests/components/zwave_js/test_lock.py @@ -222,3 +222,8 @@ async def test_door_lock(hass, client, lock_schlage_be469, integration): assert node.status == NodeStatus.DEAD assert hass.states.get(SCHLAGE_BE469_LOCK_ENTITY).state == STATE_UNAVAILABLE + + +async def test_only_one_lock(hass, client, lock_home_connect_620, integration): + """Test node with both Door Lock and Lock CC values only gets one lock entity.""" + assert len(hass.states.async_entity_ids("lock")) == 1 From c2eaa76dde6de99bce012871434a58be278f6f96 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 28 Mar 2022 12:28:58 +0300 Subject: [PATCH 0749/1054] Bump aiowebostv to 0.2.0 (#68773) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index a51d2427b2c..30473551577 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,7 +3,7 @@ "name": "LG webOS Smart TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiowebostv==0.1.3", "sqlalchemy==1.4.32"], + "requirements": ["aiowebostv==0.2.0", "sqlalchemy==1.4.32"], "codeowners": ["@bendavid", "@thecode"], "ssdp": [{"st": "urn:lge-com:service:webos-second-screen:1"}], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 0edc85003d1..74998ec6cc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -259,7 +259,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.3 +aiowebostv==0.2.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7001227f590..b4d52499b2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.webostv -aiowebostv==0.1.3 +aiowebostv==0.2.0 # homeassistant.components.yandex_transport aioymaps==1.2.2 From a597c11ea2486f02f5420831b63dc2a710e152c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 12:26:52 +0200 Subject: [PATCH 0750/1054] Mark threshold as a helper integration (#68780) --- homeassistant/components/threshold/manifest.json | 1 + homeassistant/generated/config_flows.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index 84656ad4360..48a44c04a1d 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -1,5 +1,6 @@ { "domain": "threshold", + "integration_type": "helper", "name": "Threshold", "documentation": "https://www.home-assistant.io/integrations/threshold", "codeowners": ["@fabaff"], diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ba1ec324d42..d5b604b6bd2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -338,7 +338,6 @@ FLOWS = { "tasmota", "tellduslive", "tesla_wall_connector", - "threshold", "tibber", "tile", "tolo", @@ -404,6 +403,7 @@ FLOWS = { "integration", "min_max", "switch_as_x", + "threshold", "tod" ] } From 01980f0445a63e3978f1aebbbbd75933f3cbfa08 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 28 Mar 2022 12:27:26 +0200 Subject: [PATCH 0751/1054] Add switch groups (#68528) --- homeassistant/components/group/__init__.py | 1 + homeassistant/components/group/config_flow.py | 16 +- homeassistant/components/group/strings.json | 19 +- homeassistant/components/group/switch.py | 173 +++++++++ .../components/group/translations/en.json | 19 +- .../group/fixtures/configuration.yaml | 12 + tests/components/group/test_config_flow.py | 10 +- tests/components/group/test_switch.py | 359 ++++++++++++++++++ 8 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/group/switch.py create mode 100644 tests/components/group/test_switch.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 5d7b12bb595..8395c208ec1 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -67,6 +67,7 @@ PLATFORMS = [ Platform.LIGHT, Platform.MEDIA_PLAYER, Platform.NOTIFY, + Platform.SWITCH, ] REG_KEY = f"{DOMAIN}_registry" diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index ed10391cb1d..e2e31742460 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -55,11 +55,19 @@ LIGHT_OPTIONS_SCHEMA = basic_group_options_schema("light").extend( } ) +SWITCH_OPTIONS_SCHEMA = basic_group_options_schema("switch").extend( + { + vol.Required( + CONF_ALL, default=False, description={"advanced": True} + ): selector.selector({"boolean": {}}), + } +) + BINARY_SENSOR_CONFIG_SCHEMA = vol.Schema( {vol.Required("name"): selector.selector({"text": {}})} ).extend(BINARY_SENSOR_OPTIONS_SCHEMA.schema) -GROUP_TYPES = ["binary_sensor", "cover", "fan", "light", "media_player"] +GROUP_TYPES = ["binary_sensor", "cover", "fan", "light", "media_player", "switch"] @callback @@ -94,6 +102,9 @@ CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { "media_player": HelperFlowFormStep( basic_group_config_schema("media_player"), set_group_type("media_player") ), + "switch": HelperFlowFormStep( + basic_group_config_schema("switch"), set_group_type("switch") + ), } @@ -104,11 +115,12 @@ OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = { "fan": HelperFlowFormStep(basic_group_options_schema("fan")), "light": HelperFlowFormStep(LIGHT_OPTIONS_SCHEMA), "media_player": HelperFlowFormStep(basic_group_options_schema("media_player")), + "switch": HelperFlowFormStep(SWITCH_OPTIONS_SCHEMA), } class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN): - """Handle a config or options flow for Switch Light.""" + """Handle a config or options flow for groups.""" config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index cc42e150447..383489f37de 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -10,7 +10,8 @@ "cover": "Cover group", "fan": "Fan group", "light": "Light group", - "media_player": "Media player group" + "media_player": "Media player group", + "switch": "Switch group" } }, "binary_sensor": { @@ -54,6 +55,14 @@ "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } + }, + "switch": { + "title": "[%key:component::group::config::step::user::title%]", + "data": { + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]", + "name": "[%key:component::group::config::step::binary_sensor::data::name%]" + } } } }, @@ -92,6 +101,14 @@ "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } + }, + "switch": { + "description": "[%key:component::group::config::step::binary_sensor::description%]", + "data": { + "all": "[%key:component::group::config::step::binary_sensor::data::all%]", + "entities": "[%key:component::group::config::step::binary_sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" + } } } }, diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py new file mode 100644 index 00000000000..cbee6902f02 --- /dev/null +++ b/homeassistant/components/group/switch.py @@ -0,0 +1,173 @@ +"""This platform allows several switches to be grouped into one switch.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import GroupEntity + +DEFAULT_NAME = "Switch Group" +CONF_ALL = "all" + +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_ALL, default=False): cv.boolean, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Switch Group platform.""" + async_add_entities( + [ + SwitchGroup( + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + config.get(CONF_ALL, False), + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Switch Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + async_add_entities( + [ + SwitchGroup( + config_entry.entry_id, + config_entry.title, + entities, + config_entry.options.get(CONF_ALL), + ) + ] + ) + + +class SwitchGroup(GroupEntity, SwitchEntity): + """Representation of a switch group.""" + + _attr_available = False + _attr_should_poll = False + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + mode: bool | None, + ) -> None: + """Initialize a switch group.""" + self._entity_ids = entity_ids + + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + self.mode = any + if mode: + self.mode = all + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + + await super().async_added_to_hass() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to all switches in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + _LOGGER.debug("Forwarded turn_on command: %s", data) + + await self.hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + data, + blocking=True, + context=self._context, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to all switches in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + await self.hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + data, + blocking=True, + context=self._context, + ) + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the switch group state.""" + states = [ + state.state + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ] + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + + if not valid_state: + # Set as unknown if any / all member is unknown or unavailable + self._attr_is_on = None + else: + # Set as ON if any / all member is ON + self._attr_is_on = self.mode(list(map(lambda x: x == STATE_ON, states))) + + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index bb2b7ec8825..322f8e2fa10 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -43,6 +43,14 @@ }, "title": "New Group" }, + "switch": { + "data": { + "entities": "Members", + "hide_members": "Hide members", + "name": "Name" + }, + "title": "New Group" + }, "user": { "description": "Select group type", "menu_options": { @@ -50,7 +58,8 @@ "cover": "Cover group", "fan": "Fan group", "light": "Light group", - "media_player": "Media player group" + "media_player": "Media player group", + "switch": "Switch group" }, "title": "New Group" } @@ -91,6 +100,14 @@ "entities": "Members", "hide_members": "Hide members" } + }, + "switch": { + "data": { + "all": "All entities", + "entities": "Members", + "hide_members": "Hide members" + }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." } } }, diff --git a/tests/components/group/fixtures/configuration.yaml b/tests/components/group/fixtures/configuration.yaml index 0a5c9e18bd1..1e88cd6e217 100644 --- a/tests/components/group/fixtures/configuration.yaml +++ b/tests/components/group/fixtures/configuration.yaml @@ -10,6 +10,18 @@ light: - light.outside_patio_lights - light.outside_patio_lights_2 +switch: + - platform: group + name: Master Switches G + entities: + - switch.master_switch + - switch.master_switch_2 + - platform: group + name: Outside Switches G + entities: + - switch.outside_switch + - switch.outside_switch_2 + notify: - platform: group name: new_group_notify diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 67fdec1820f..72684a0fc26 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -25,6 +25,7 @@ from tests.common import MockConfigEntry ("fan", "on", "on", {}, {}, {}, {}), ("light", "on", "on", {}, {}, {}, {}), ("media_player", "on", "on", {}, {}, {}, {}), + ("switch", "on", "on", {}, {}, {}, {}), ), ) async def test_config_flow( @@ -108,6 +109,7 @@ async def test_config_flow( ("fan", {}), ("light", {}), ("media_player", {}), + ("switch", {}), ), ) async def test_config_flow_hides_members( @@ -178,6 +180,7 @@ def get_suggested(schema, key): ("fan", "on", {}), ("light", "on", {"all": False}), ("media_player", "on", {}), + ("switch", "on", {"all": False}), ), ) async def test_options( @@ -273,9 +276,13 @@ async def test_options( ("light", {"all": True}, {"all": True}, False), ("light", {"all": False}, {"all": False}, True), ("light", {"all": True}, {"all": False}, True), + ("switch", {"all": False}, {"all": False}, False), + ("switch", {"all": True}, {"all": True}, False), + ("switch", {"all": False}, {"all": False}, True), + ("switch", {"all": True}, {"all": False}, True), ), ) -async def test_light_all_options( +async def test_all_options( hass: HomeAssistant, group_type, extra_options, extra_options_after, advanced ) -> None: """Test reconfiguring.""" @@ -345,6 +352,7 @@ async def test_light_all_options( ("fan", {}), ("light", {}), ("media_player", {}), + ("switch", {}), ), ) async def test_options_flow_hides_members( diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py new file mode 100644 index 00000000000..5df2542d101 --- /dev/null +++ b/tests/components/group/test_switch.py @@ -0,0 +1,359 @@ +"""The tests for the Group Switch platform.""" +from unittest.mock import patch + +import async_timeout + +from homeassistant import config as hass_config +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import get_fixture_path + + +async def test_default_state(hass): + """Test switch group default state.""" + hass.states.async_set("switch.tv", "on") + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + "platform": DOMAIN, + "entities": ["switch.tv", "switch.soundbar"], + "name": "Multimedia Group", + "unique_id": "unique_identifier", + "all": "false", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("switch.multimedia_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.tv", "switch.soundbar"] + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("switch.multimedia_group") + assert entry + assert entry.unique_id == "unique_identifier" + + +async def test_state_reporting(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + "platform": DOMAIN, + "entities": ["switch.test1", "switch.test2"], + "all": "false", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + +async def test_state_reporting_all(hass): + """Test the state reporting.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + "platform": DOMAIN, + "entities": ["switch.test1", "switch.test2"], + "all": "true", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + +async def test_service_calls(hass, enable_custom_integrations): + """Test service calls.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "switch.ac", + "switch.decorative_lights", + ], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("switch.switch_group") + assert group_state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "switch.switch_group"}, + blocking=True, + ) + assert hass.states.get("switch.ac").state == STATE_OFF + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.switch_group"}, + blocking=True, + ) + + assert hass.states.get("switch.ac").state == STATE_ON + assert hass.states.get("switch.decorative_lights").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.switch_group"}, + blocking=True, + ) + + assert hass.states.get("switch.ac").state == STATE_OFF + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + + +async def test_reload(hass): + """Test the ability to reload switches.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "switch.ac", + "switch.decorative_lights", + ], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.switch_group") is None + assert hass.states.get("switch.master_switches_g") is not None + assert hass.states.get("switch.outside_switches_g") is not None + + +async def test_reload_with_platform_not_setup(hass): + """Test the ability to reload switches.""" + hass.states.async_set("switch.something", STATE_ON) + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + ] + }, + ) + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "switch.something", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.switch_group") is None + assert hass.states.get("switch.master_switches_g") is not None + assert hass.states.get("switch.outside_switches_g") is not None + + +async def test_reload_with_base_integration_platform_not_setup(hass): + """Test the ability to reload switches.""" + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "switch.something", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + hass.states.async_set("switch.master_switch", STATE_ON) + hass.states.async_set("switch.master_switch_2", STATE_OFF) + + hass.states.async_set("switch.outside_switch", STATE_OFF) + hass.states.async_set("switch.outside_switch_2", STATE_OFF) + + yaml_path = get_fixture_path("configuration.yaml", "group") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("switch.switch_group") is None + assert hass.states.get("switch.master_switches_g") is not None + assert hass.states.get("switch.outside_switches_g") is not None + assert hass.states.get("switch.master_switches_g").state == STATE_ON + assert hass.states.get("switch.outside_switches_g").state == STATE_OFF + + +async def test_nested_group(hass): + """Test nested switch group.""" + await async_setup_component( + hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": ["switch.some_group"], + "name": "Nested Group", + "all": "false", + }, + { + "platform": DOMAIN, + "entities": ["switch.ac", "switch.decorative_lights"], + "name": "Some Group", + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("switch.some_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == [ + "switch.ac", + "switch.decorative_lights", + ] + + state = hass.states.get("switch.nested_group") + assert state is not None + assert state.state == STATE_ON + assert state.attributes.get(ATTR_ENTITY_ID) == ["switch.some_group"] + + # Test controlling the nested group + async with async_timeout.timeout(0.5): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: "switch.nested_group"}, + blocking=True, + ) + assert hass.states.get("switch.ac").state == STATE_OFF + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("switch.some_group").state == STATE_OFF + assert hass.states.get("switch.nested_group").state == STATE_OFF From cc156c767d5e2a981e493f9d58d4c90b60506d08 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 12:28:15 +0200 Subject: [PATCH 0752/1054] Revert light.switch to 2022.3 (#68772) --- homeassistant/components/switch/light.py | 79 +++++++++++++++++++++-- tests/components/switch/test_light.py | 82 ++++++++++++++++++++---- 2 files changed, 146 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index ad75d229d51..ac548652953 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,15 +1,29 @@ """Light support for switch entities.""" from __future__ import annotations +from typing import Any + import voluptuous as vol -from homeassistant.components.light import PLATFORM_SCHEMA -from homeassistant.components.switch_as_x import LightSwitch -from homeassistant.const import CONF_ENTITY_ID, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.components.light import ( + COLOR_MODE_ONOFF, + PLATFORM_SCHEMA, + LightEntity, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + CONF_NAME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN as SWITCH_DOMAIN @@ -44,3 +58,60 @@ async def async_setup_platform( ) ] ) + + +class LightSwitch(LightEntity): + """Represents a Switch as a Light.""" + + _attr_color_mode = COLOR_MODE_ONOFF + _attr_should_poll = False + _attr_supported_color_modes = {COLOR_MODE_ONOFF} + + def __init__(self, name: str, switch_entity_id: str, unique_id: str | None) -> None: + """Initialize Light Switch.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._switch_entity_id = switch_entity_id + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to the switch in this light switch.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self._switch_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + self._attr_available = True + self._attr_is_on = state.state == STATE_ON + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._switch_entity_id], async_state_changed_listener + ) + ) + # Call once on adding + async_state_changed_listener() diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index e75e0e6d313..62fef242e9f 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,10 +1,18 @@ """The tests for the Light Switch platform.""" +from homeassistant.components.light import ( + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_ONOFF, +) from homeassistant.setup import async_setup_component +from tests.components.light import common +from tests.components.switch import common as switch_common + async def test_default_state(hass): - """Test light switch yaml config.""" + """Test light switch default state.""" await async_setup_component( hass, "light", @@ -18,21 +26,73 @@ async def test_default_state(hass): ) await hass.async_block_till_done() - assert hass.states.get("light.christmas_tree_lights") + state = hass.states.get("light.christmas_tree_lights") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 0 + assert state.attributes.get("brightness") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("effect_list") is None + assert state.attributes.get("effect") is None + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_ONOFF] + assert state.attributes.get(ATTR_COLOR_MODE) is None -async def test_default_state_no_name(hass): - """Test light switch default name.""" +async def test_light_service_calls(hass): + """Test service calls to light.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) await async_setup_component( hass, "light", - { - "light": { - "platform": "switch", - "entity_id": "switch.test", - } - }, + {"light": [{"platform": "switch", "entity_id": "switch.decorative_lights"}]}, ) await hass.async_block_till_done() - assert hass.states.get("light.light_switch") + assert hass.states.get("light.light_switch").state == "on" + + await common.async_toggle(hass, "light.light_switch") + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.light_switch").state == "off" + + await common.async_turn_on(hass, "light.light_switch") + + assert hass.states.get("switch.decorative_lights").state == "on" + assert hass.states.get("light.light_switch").state == "on" + assert ( + hass.states.get("light.light_switch").attributes.get(ATTR_COLOR_MODE) + == COLOR_MODE_ONOFF + ) + + await common.async_turn_off(hass, "light.light_switch") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.light_switch").state == "off" + + +async def test_switch_service_calls(hass): + """Test service calls to switch.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await async_setup_component( + hass, + "light", + {"light": [{"platform": "switch", "entity_id": "switch.decorative_lights"}]}, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.light_switch").state == "on" + + await switch_common.async_turn_off(hass, "switch.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.light_switch").state == "off" + + await switch_common.async_turn_on(hass, "switch.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "on" + assert hass.states.get("light.light_switch").state == "on" From 2ec1e06c756714ae9782db159dde0ffb5597c4a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Mar 2022 12:36:11 +0200 Subject: [PATCH 0753/1054] Mark group as a helper integration (#68775) --- homeassistant/components/group/manifest.json | 1 + homeassistant/generated/config_flows.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 9c97f318da3..136bc34dee9 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,5 +1,6 @@ { "domain": "group", + "integration_type": "helper", "name": "Group", "documentation": "https://www.home-assistant.io/integrations/group", "codeowners": [ diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d5b604b6bd2..1795f0c74b4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -132,7 +132,6 @@ FLOWS = { "google_travel_time", "gpslogger", "gree", - "group", "growatt_server", "guardian", "habitica", @@ -400,6 +399,7 @@ FLOWS = { ], "helper": [ "derivative", + "group", "integration", "min_max", "switch_as_x", From 3230ee88b6312dcabc160ed8afb3f67b0c1f8bc8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Mar 2022 12:36:45 +0200 Subject: [PATCH 0754/1054] Add OUI to SamsungTV (#68771) Co-authored-by: epenet --- homeassistant/components/samsungtv/manifest.json | 9 +++++---- homeassistant/generated/dhcp.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index acc5ea3a66a..c189a20b241 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -34,11 +34,12 @@ { "hostname": "tizen*" }, - {"macaddress": "8CC8CD*"}, - {"macaddress": "606BBD*"}, - {"macaddress": "F47B5E*"}, {"macaddress": "4844F7*"}, - {"macaddress": "8CEA48*"} + {"macaddress": "606BBD*"}, + {"macaddress": "641CB0*"}, + {"macaddress": "8CC8CD*"}, + {"macaddress": "8CEA48*"}, + {"macaddress": "F47B5E*"} ], "codeowners": [ "@chemelli74", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 91e6550919d..48760218f3c 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -76,11 +76,12 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'roomba', 'hostname': 'roomba-*', 'macaddress': 'DCF505*'}, {'domain': 'samsungtv', 'registered_devices': True}, {'domain': 'samsungtv', 'hostname': 'tizen*'}, - {'domain': 'samsungtv', 'macaddress': '8CC8CD*'}, - {'domain': 'samsungtv', 'macaddress': '606BBD*'}, - {'domain': 'samsungtv', 'macaddress': 'F47B5E*'}, {'domain': 'samsungtv', 'macaddress': '4844F7*'}, + {'domain': 'samsungtv', 'macaddress': '606BBD*'}, + {'domain': 'samsungtv', 'macaddress': '641CB0*'}, + {'domain': 'samsungtv', 'macaddress': '8CC8CD*'}, {'domain': 'samsungtv', 'macaddress': '8CEA48*'}, + {'domain': 'samsungtv', 'macaddress': 'F47B5E*'}, {'domain': 'screenlogic', 'registered_devices': True}, {'domain': 'screenlogic', 'hostname': 'pentair*', 'macaddress': '00C033*'}, {'domain': 'sense', 'hostname': 'sense-*', 'macaddress': '009D6B*'}, From a6d0a4ec645b54b6aec1e4deaf6b0c462f0749d1 Mon Sep 17 00:00:00 2001 From: rforro Date: Mon, 28 Mar 2022 15:56:37 +0200 Subject: [PATCH 0755/1054] Update climate.py (#68786) add TRV presets to all Zonnsmart-like models --- homeassistant/components/zha/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 4d91b84ec53..66ea06f0d2b 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -776,7 +776,10 @@ class StelproFanHeater(Thermostat): @STRICT_MATCH( channel_names=CHANNEL_THERMOSTAT, manufacturers={ - "_TZE200_hue3yfsn", + "_TZE200_e9ba97vf", # TV01-ZG + "_TZE200_husqqvux", # TSL-TRV-TV01ZG + "_TZE200_hue3yfsn", # TV02-ZG + "_TZE200_kly8gjlz", # TV05-ZG }, ) class ZONNSMARTThermostat(Thermostat): From 443315bcdc219c077f4e66c3bba6a25469fe160a Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 28 Mar 2022 08:22:56 -0700 Subject: [PATCH 0756/1054] Bump dependency (pyoverkiz to 1.3.12) in Overkiz integration (#68788) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 5e8fe27e21e..9ee52a113c7 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": [ - "pyoverkiz==1.3.9" + "pyoverkiz==1.3.12" ], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 74998ec6cc5..0ff5a4d328a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1699,7 +1699,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.9 +pyoverkiz==1.3.12 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b4d52499b2c..50a8b9e110e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.3.9 +pyoverkiz==1.3.12 # homeassistant.components.openweathermap pyowm==3.2.0 From c763d23cbb01d7f7e374dd422fce253f05d6497c Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 28 Mar 2022 08:23:25 -0700 Subject: [PATCH 0757/1054] Fix reauth message in Overkiz integration (#68787) --- homeassistant/components/overkiz/config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index f35941d6773..520a61f27fd 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -161,6 +161,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_get_entry(self.context["entry_id"]), ) + self.context["title_placeholders"] = { + "gateway_id": self._config_entry.unique_id + } + self._default_user = self._config_entry.data[CONF_USERNAME] self._default_hub = self._config_entry.data[CONF_HUB] From 33371bdd20104d2e792190c43670e43310c7669c Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 28 Mar 2022 17:23:44 +0200 Subject: [PATCH 0758/1054] Change fibaro disconnect log level to debug (#68783) --- homeassistant/components/fibaro/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 00f8c4da92d..fffc30580d8 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -439,7 +439,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - _LOGGER.info("Shutting down Fibaro connection") + _LOGGER.debug("Shutting down Fibaro connection") unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN][entry.entry_id][FIBARO_CONTROLLER].disable_state_handler() From 066128a53cf5301e9d8fe069334251d38502d41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 28 Mar 2022 18:24:13 +0300 Subject: [PATCH 0759/1054] Remove leftovers of Huawei LTE YAML config support (#68728) --- .../components/huawei_lte/__init__.py | 66 ++----------------- .../components/huawei_lte/config_flow.py | 6 -- 2 files changed, 6 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 6f1eb6c7a60..ca15731641d 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -19,11 +19,10 @@ from huawei_lte_api.exceptions import ( ResponseErrorNotSupportedException, ) from requests.exceptions import Timeout -from url_normalize import url_normalize import voluptuous as vol from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_HW_VERSION, ATTR_MODEL, @@ -300,50 +299,13 @@ class HuaweiLteData(NamedTuple): """Shared state.""" hass_config: ConfigType - # Our YAML config, keyed by router URL - config: dict[str, dict[str, Any]] routers: dict[str, Router] -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huawei LTE component from config entry.""" url = entry.data[CONF_URL] - # Override settings from YAML config, but only if they're changed in it - # Old values are stored as *_from_yaml in the config entry - if yaml_config := hass.data[DOMAIN].config.get(url): - # Config values - new_data = {} - for key in CONF_USERNAME, CONF_PASSWORD: - if key in yaml_config: - value = yaml_config[key] - if value != entry.data.get(f"{key}_from_yaml"): - new_data[f"{key}_from_yaml"] = value - new_data[key] = value - # Options - new_options = {} - yaml_recipient = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_RECIPIENT) - if yaml_recipient is not None and yaml_recipient != entry.options.get( - f"{CONF_RECIPIENT}_from_yaml" - ): - new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient - new_options[CONF_RECIPIENT] = yaml_recipient - yaml_notify_name = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_NAME) - if yaml_notify_name is not None and yaml_notify_name != entry.options.get( - f"{CONF_NAME}_from_yaml" - ): - new_options[f"{CONF_NAME}_from_yaml"] = yaml_notify_name - new_options[CONF_NAME] = yaml_notify_name - # Update entry if overrides were found - if new_data or new_options: - hass.config_entries.async_update_entry( - entry, - data={**entry.data, **new_data}, - options={**entry.options, **new_options}, - ) - def get_connection() -> Connection: """Set up a connection.""" if entry.options.get(CONF_UNAUTHENTICATED_MODE): @@ -512,14 +474,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # https://github.com/quandyfactory/dicttoxml/issues/60 logging.getLogger("dicttoxml").setLevel(logging.WARNING) - # Arrange our YAML config to dict with normalized URLs as keys - domain_config: dict[str, dict[str, Any]] = {} if DOMAIN not in hass.data: - hass.data[DOMAIN] = HuaweiLteData( - hass_config=config, config=domain_config, routers={} - ) - for router_config in config.get(DOMAIN, []): - domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + hass.data[DOMAIN] = HuaweiLteData(hass_config=config, routers={}) def service_handler(service: ServiceCall) -> None: """ @@ -580,19 +536,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SERVICE_SCHEMA, ) - for url, router_config in domain_config.items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_URL: url, - CONF_USERNAME: router_config.get(CONF_USERNAME), - CONF_PASSWORD: router_config.get(CONF_PASSWORD), - }, - ) - ) - return True @@ -612,6 +555,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> data[CONF_MAC] = [] hass.config_entries.async_update_entry(config_entry, data=data) _LOGGER.info("Migrated config entry to version %d", config_entry.version) + # There can be no longer needed *_from_yaml data and options things left behind + # from pre-2022.4ish; they can be removed while at it when/if we eventually bump and + # migrate to version > 3 for some other reason. return True diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index e8a02db4f1c..f4ea7cd86f7 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -89,12 +89,6 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle import initiated config flow.""" - return await self.async_step_user(user_input) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: From 24212ab59841b6c71a3e77e1b75bbed0f8427e1e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 28 Mar 2022 17:42:25 +0200 Subject: [PATCH 0760/1054] Add diagnostics platform for AccuWeather integration (#68752) --- .../components/accuweather/diagnostics.py | 28 +++++++++++++++++++ .../accuweather/test_diagnostics.py | 26 +++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 homeassistant/components/accuweather/diagnostics.py create mode 100644 tests/components/accuweather/test_diagnostics.py diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py new file mode 100644 index 00000000000..e4305eb747a --- /dev/null +++ b/homeassistant/components/accuweather/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for AccuWeather.""" +from __future__ import annotations + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.core import HomeAssistant + +from . import AccuWeatherDataUpdateCoordinator +from .const import DOMAIN + +TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + diagnostics_data = { + "config_entry_data": async_redact_data(dict(config_entry.data), TO_REDACT), + "coordinator_data": coordinator.data, + } + + return diagnostics_data diff --git a/tests/components/accuweather/test_diagnostics.py b/tests/components/accuweather/test_diagnostics.py new file mode 100644 index 00000000000..1936de5fad7 --- /dev/null +++ b/tests/components/accuweather/test_diagnostics.py @@ -0,0 +1,26 @@ +"""Test AccuWeather diagnostics.""" +import json + +from tests.common import load_fixture +from tests.components.accuweather import init_integration +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + entry = await init_integration(hass) + + coordinator_data = json.loads( + load_fixture("current_conditions_data.json", "accuweather") + ) + coordinator_data["forecast"] = {} + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["config_entry_data"] == { + "api_key": "**REDACTED**", + "latitude": "**REDACTED**", + "longitude": "**REDACTED**", + "name": "Home", + } + assert result["coordinator_data"] == coordinator_data From 79080f5e2f47e66c412c5f6ae5b6c63f31db3036 Mon Sep 17 00:00:00 2001 From: Will Marler Date: Mon, 28 Mar 2022 10:03:34 -0600 Subject: [PATCH 0761/1054] Adjust "default_config" comment in default config (#68679) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 17a1c0fbfa1..d5f9eb91b62 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -86,7 +86,7 @@ INTEGRATION_LOAD_EXCEPTIONS = ( ) DEFAULT_CONFIG = f""" -# Configure a default setup of Home Assistant (frontend, api, etc) +# Loads default set of integrations. Do not remove. default_config: # Text to speech From 609c6ef5d4fad2cd6e393d7075cf59c26c730cc2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 28 Mar 2022 09:05:02 -0700 Subject: [PATCH 0762/1054] Add test coverage for google calendar event response format (#68767) Add test coverage for the calendar event response format as I am about to do some refactoring and simplification. Notably, each calendar implementation uses a slightly different API response, and this is codifying that. --- tests/components/google/test_calendar.py | 45 ++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index baa09d0b88f..53e7308bc6f 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -6,6 +6,7 @@ import datetime from http import HTTPStatus from typing import Any from unittest.mock import patch +import urllib import httplib2 import pytest @@ -74,12 +75,21 @@ def upcoming() -> dict[str, Any]: } +def upcoming_date() -> dict[str, Any]: + """Create a test event with an arbitrary start/end date fetched from the api url.""" + now = dt_util.now() + return { + "start": {"date": now.date().isoformat()}, + "end": {"date": now.date().isoformat()}, + } + + def upcoming_event_url() -> str: """Return a calendar API to return events created by upcoming().""" now = dt_util.now() start = (now - datetime.timedelta(minutes=60)).isoformat() end = (now + datetime.timedelta(minutes=60)).isoformat() - return f"/api/calendars/{TEST_ENTITY}?start={start}&end={end}" + return f"/api/calendars/{TEST_ENTITY}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" async def test_all_day_event( @@ -365,10 +375,12 @@ async def test_http_event_api_failure( assert events == [] +@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") async def test_http_api_event( hass, hass_client, mock_events_list_items, component_setup ): """Test querying the API and fetching events from the server.""" + hass.config.set_time_zone("Asia/Baghdad") event = { **TEST_EVENT, **upcoming(), @@ -381,8 +393,35 @@ async def test_http_api_event( assert response.status == HTTPStatus.OK events = await response.json() assert len(events) == 1 - assert "summary" in events[0] - assert events[0]["summary"] == event["summary"] + assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == { + "summary": TEST_EVENT["summary"], + "start": {"dateTime": "2022-03-27T15:05:00+03:00"}, + "end": {"dateTime": "2022-03-27T15:10:00+03:00"}, + } + + +@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") +async def test_http_api_all_day_event( + hass, hass_client, mock_events_list_items, component_setup +): + """Test querying the API and fetching events from the server.""" + event = { + **TEST_EVENT, + **upcoming_date(), + } + mock_events_list_items([event]) + assert await component_setup() + + client = await hass_client() + response = await client.get(upcoming_event_url()) + assert response.status == HTTPStatus.OK + events = await response.json() + assert len(events) == 1 + assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == { + "summary": TEST_EVENT["summary"], + "start": {"date": "2022-03-27"}, + "end": {"date": "2022-03-27"}, + } @pytest.mark.parametrize( From 68a63599992248a15996dcbed1b8554fa3ce1b0e Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 28 Mar 2022 18:22:00 +0200 Subject: [PATCH 0763/1054] Address late motion blinds review comments (#68793) --- homeassistant/components/motion_blinds/cover.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index a2cbaabedd0..07024a9d1fc 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -266,11 +266,6 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._previous_positions = [] self._requesting_position = False - @callback - def async_scheduled_update_request_callback(self, now): - """Request a state update from the blind at a scheduled point in time using async_scheduled_update_request.""" - self.hass.loop.create_task(self.async_scheduled_update_request()) - def request_position_till_stop(self): """Request the position of the blind every UPDATE_INTERVAL_MOVING seconds until it stops moving.""" self._previous_positions = [] @@ -280,7 +275,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._requesting_position = True track_point_in_time( self.hass, - self.async_scheduled_update_request_callback, + self.async_scheduled_update_request, dt_util.utcnow() + timedelta(seconds=UPDATE_INTERVAL_MOVING), ) From e80933d6c73d1b3a0a31ce0b25caffdd2556932d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Mar 2022 10:32:15 -0700 Subject: [PATCH 0764/1054] Force helpers to have a mandatory description (#68796) --- .../components/derivative/strings.json | 1 + .../components/switch_as_x/strings.json | 9 ++++---- script/hassfest/translations.py | 23 ++++++++++++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index b225653cafa..b9ef15d652d 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -3,6 +3,7 @@ "step": { "user": { "title": "New Derivative sensor", + "description": "Create a sensor that estimates the derivative of a sensor.", "data": { "name": "Name", "round": "Precision", diff --git a/homeassistant/components/switch_as_x/strings.json b/homeassistant/components/switch_as_x/strings.json index cc50d1922cf..05863b0c5fe 100644 --- a/homeassistant/components/switch_as_x/strings.json +++ b/homeassistant/components/switch_as_x/strings.json @@ -2,11 +2,12 @@ "title": "Switch as X", "config": { "step": { - "init": { - "title": "Make a switch a ...", + "user": { + "title": "Change switch device type", + "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.", "data": { - "entity_id": "Switch entity", - "target_domain": "Type" + "entity_id": "Switch", + "target_domain": "New Type" } } } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 5e64c70210c..38392832261 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -100,6 +100,7 @@ def gen_data_entry_schema( integration: Integration, flow_title: int, require_step_title: bool, + mandatory_description: str | None = None, ): """Generate a data entry schema.""" step_title_class = vol.Required if require_step_title else vol.Optional @@ -138,7 +139,24 @@ def gen_data_entry_schema( return value - return vol.All(vol.Schema(schema), data_description_validator) + validators = [vol.Schema(schema), data_description_validator] + + if mandatory_description is not None: + + def validate_description_set(value): + """Validate description is set.""" + steps = value["step"] + if mandatory_description not in steps: + raise vol.Invalid(f"{mandatory_description} needs to be defined") + + if "description" not in steps[mandatory_description]: + raise vol.Invalid(f"Step {mandatory_description} needs a description") + + return value + + validators.append(validate_description_set) + + return vol.All(*validators) def gen_strings_schema(config: Config, integration: Integration): @@ -151,6 +169,9 @@ def gen_strings_schema(config: Config, integration: Integration): integration=integration, flow_title=REMOVED, require_step_title=False, + mandatory_description=( + "user" if integration.integration_type == "helper" else None + ), ), vol.Optional("options"): gen_data_entry_schema( config=config, From 5eee600fa8afe532c8717cb18d50f4d5d769b456 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Mar 2022 10:41:39 -0700 Subject: [PATCH 0765/1054] Ban brand name translations as step titles (#68801) --- .../components/accuweather/strings.json | 6 +- homeassistant/components/aemet/strings.json | 53 ++++++++-------- homeassistant/components/airly/strings.json | 3 +- homeassistant/components/airnow/strings.json | 3 +- .../components/amberelectric/strings.json | 4 +- .../components/androidtv/strings.json | 3 - .../components/braviatv/strings.json | 3 +- homeassistant/components/brother/strings.json | 3 +- .../components/denonavr/strings.json | 7 +-- .../components/doorbird/strings.json | 5 +- homeassistant/components/dunehd/strings.json | 3 +- homeassistant/components/freebox/strings.json | 1 - homeassistant/components/gios/strings.json | 1 - .../components/goalzero/strings.json | 4 +- .../components/honeywell/strings.json | 2 - homeassistant/components/insteon/strings.json | 12 +--- homeassistant/components/iqvia/strings.json | 1 - .../components/kaleidescape/strings.json | 6 +- .../components/meteo_france/strings.json | 2 - .../components/meteoclimatic/strings.json | 7 ++- .../components/modem_callerid/strings.json | 40 ++++++------ .../components/motion_blinds/strings.json | 7 --- .../components/nfandroidtv/strings.json | 3 +- .../components/opentherm_gw/strings.json | 2 - .../components/openweathermap/strings.json | 61 +++++++++---------- homeassistant/components/peco/strings.json | 3 +- homeassistant/components/plex/strings.json | 2 - .../components/poolsense/strings.json | 2 - homeassistant/components/ps4/strings.json | 11 ++-- .../pvpc_hourly_pricing/strings.json | 4 -- homeassistant/components/roku/strings.json | 1 - homeassistant/components/sentry/strings.json | 4 +- .../components/synology_dsm/strings.json | 2 - homeassistant/components/tasmota/strings.json | 2 - homeassistant/components/tibber/strings.json | 3 +- .../components/totalconnect/strings.json | 1 - .../components/twentemilieu/strings.json | 5 +- homeassistant/components/twinkly/strings.json | 2 - .../components/vallox/config_flow.py | 9 --- homeassistant/components/vallox/strings.json | 2 - homeassistant/components/vera/strings.json | 5 +- homeassistant/components/vilfo/strings.json | 2 - homeassistant/components/wilight/strings.json | 3 +- .../components/xiaomi_aqara/strings.json | 8 +-- .../components/xiaomi_miio/strings.json | 15 ++--- script/hassfest/translations.py | 37 ++++++++--- 46 files changed, 152 insertions(+), 213 deletions(-) diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index c4305a0a7a5..432cc095c7b 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "AccuWeather", - "description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.", "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", @@ -12,6 +10,9 @@ } } }, + "create_entry": { + "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options." + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", @@ -24,7 +25,6 @@ "options": { "step": { "user": { - "title": "AccuWeather Options", "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", "data": { "forecast": "Weather forecast" diff --git a/homeassistant/components/aemet/strings.json b/homeassistant/components/aemet/strings.json index 360f7c680ea..75c810978ad 100644 --- a/homeassistant/components/aemet/strings.json +++ b/homeassistant/components/aemet/strings.json @@ -1,31 +1,30 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" - }, - "step": { - "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]", - "name": "Name of the integration" - }, - "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" }, - "options": { - "step": { - "init": { - "data": { - "station_updates": "Gather data from AEMET weather stations" - } - } - } + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "name": "Name of the integration" + }, + "description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario" + } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Gather data from AEMET weather stations" + } + } + } + } } diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index c6b6f1e6a41..77f965b64c2 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Airly", - "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "description": "To generate API key go to https://developer.airly.eu/register", "data": { "name": "[%key:common::config_flow::data::name%]", "api_key": "[%key:common::config_flow::data::api_key%]", diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json index 9fc5bd3bccc..0e86c4531dc 100644 --- a/homeassistant/components/airnow/strings.json +++ b/homeassistant/components/airnow/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "AirNow", - "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "description": "To generate API key go to https://docs.airnowapi.org/account/request/", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "[%key:common::config_flow::data::latitude%]", diff --git a/homeassistant/components/amberelectric/strings.json b/homeassistant/components/amberelectric/strings.json index cdbff2022b3..61d2c061955 100644 --- a/homeassistant/components/amberelectric/strings.json +++ b/homeassistant/components/amberelectric/strings.json @@ -6,7 +6,6 @@ "api_token": "API Token", "site_id": "Site ID" }, - "title": "Amber Electric", "description": "Go to {api_url} to generate an API key" }, "site": { @@ -14,9 +13,8 @@ "site_nmi": "Site NMI", "site_name": "Site Name" }, - "title": "Amber Electric", "description": "Select the NMI of the site you would like to add" } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/androidtv/strings.json b/homeassistant/components/androidtv/strings.json index 1fd0231379f..7a46228bd4e 100644 --- a/homeassistant/components/androidtv/strings.json +++ b/homeassistant/components/androidtv/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Android TV", - "description": "Set required parameters to connect to your Android TV device", "data": { "host": "[%key:common::config_flow::data::host%]", "adbkey": "Path to your ADB key file (leave empty to auto generate)", @@ -29,7 +27,6 @@ "options": { "step": { "init": { - "title": "Android TV Options", "data": { "apps": "Configure applications list", "get_sources": "Retrieve the running apps as the list of sources", diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index fa0f91861e2..c00b143a442 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Sony Bravia TV", - "description": "Set up Sony Bravia TV integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/braviatv \n\nEnsure that your TV is turned on.", + "description": "Ensure that your TV is turned on before trying to set it up.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index 0b00a3b30cd..9d7d42abefa 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -3,14 +3,13 @@ "flow_title": "{model} {serial_number}", "step": { "user": { - "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", "data": { "host": "[%key:common::config_flow::data::host%]", "type": "Type of the printer" } }, "zeroconf_confirm": { - "description": "Do you want to add the Brother Printer {model} with serial number `{serial_number}` to Home Assistant?", + "description": "Do you want to add the printer {model} with serial number `{serial_number}` to Home Assistant?", "title": "Discovered Brother Printer", "data": { "type": "Type of the printer" diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index eaa06c5ff88..e2a90a5c01b 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -3,14 +3,14 @@ "flow_title": "{name}", "step": { "user": { - "title": "Denon AVR Network Receivers", - "description": "Connect to your receiver, if the IP address is not set, auto-discovery is used", "data": { "host": "[%key:common::config_flow::data::ip%]" + }, + "data_description": { + "host": "Leave blank to use auto-discovery" } }, "confirm": { - "title": "Denon AVR Network Receivers", "description": "Please confirm adding the receiver" }, "select": { @@ -35,7 +35,6 @@ "options": { "step": { "init": { - "title": "Denon AVR Network Receivers", "description": "Specify optional settings", "data": { "show_all_sources": "Show all sources", diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index e710c587d5d..44fd07c405e 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -5,14 +5,15 @@ "data": { "events": "Comma separated list of events." }, - "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + "data_description": { + "events": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion" + } } } }, "config": { "step": { "user": { - "title": "Connect to the DoorBird", "data": { "password": "[%key:common::config_flow::data::password%]", "host": "[%key:common::config_flow::data::host%]", diff --git a/homeassistant/components/dunehd/strings.json b/homeassistant/components/dunehd/strings.json index 4c0d8879858..f7e12b39f16 100644 --- a/homeassistant/components/dunehd/strings.json +++ b/homeassistant/components/dunehd/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Dune HD", - "description": "Set up Dune HD integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/dunehd \n\nEnsure that your player is turned on.", + "description": "Ensure that your player is turned on.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json index cb48e5322de..53a5fd59de3 100644 --- a/homeassistant/components/freebox/strings.json +++ b/homeassistant/components/freebox/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Freebox", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json index ef2fec9f84f..18db42b69c0 100644 --- a/homeassistant/components/gios/strings.json +++ b/homeassistant/components/gios/strings.json @@ -3,7 +3,6 @@ "step": { "user": { "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)", - "description": "Set up GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", "data": { "name": "[%key:common::config_flow::data::name%]", "station_id": "ID of the measuring station" diff --git a/homeassistant/components/goalzero/strings.json b/homeassistant/components/goalzero/strings.json index 5147299b564..619b379c7a3 100644 --- a/homeassistant/components/goalzero/strings.json +++ b/homeassistant/components/goalzero/strings.json @@ -2,15 +2,13 @@ "config": { "step": { "user": { - "title": "Goal Zero Yeti", - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", + "description": "Please refer to the documentation to make sure all requirements are met.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" } }, "confirm_discovery": { - "title": "Goal Zero Yeti", "description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual." } }, diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 1e20c0b1e81..8e085ad7e86 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Honeywell Total Connect Comfort (US)", "description": "Please enter the credentials used to log into mytotalconnectcomfort.com.", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -17,7 +16,6 @@ "options": { "step": { "init": { - "title": "Honeywell Options", "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit.", "data": { "away_cool_temperature": "Away cool temperature", diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index b0130910c5f..ca88b43956f 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Insteon", "description": "Select the Insteon modem type.", "data": { "modem_type": "Modem type." @@ -46,8 +45,6 @@ "options": { "step": { "init": { - "title": "Insteon", - "description": "Select an option to configure.", "data": { "change_hub_config": "Change the Hub configuration.", "add_override": "Add a device override.", @@ -57,7 +54,6 @@ } }, "change_hub_config": { - "title": "Insteon", "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", "data": { "host": "[%key:common::config_flow::data::ip%]", @@ -67,7 +63,6 @@ } }, "add_override": { - "title": "Insteon", "description": "Add a device override.", "data": { "address": "Device address (i.e. 1a2b3c)", @@ -76,7 +71,6 @@ } }, "add_x10": { - "title": "Insteon", "description": "Change the Insteon Hub password.", "data": { "housecode": "Housecode (a - p)", @@ -85,15 +79,13 @@ "steps": "Dimmer steps (for light devices only, default 22)" } }, - "remove_override": { - "title": "Insteon", + "remove_override": { "description": "Remove a device override", "data": { "address": "Select a device address to remove" } }, - "remove_x10": { - "title": "Insteon", + "remove_x10": { "description": "Remove an X10 device", "data": { "address": "Select a device address to remove" diff --git a/homeassistant/components/iqvia/strings.json b/homeassistant/components/iqvia/strings.json index 7e16d3ae6e3..5dc0dea53d5 100644 --- a/homeassistant/components/iqvia/strings.json +++ b/homeassistant/components/iqvia/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "IQVIA", "description": "Fill out your U.S. or Canadian ZIP code.", "data": { "zip_code": "ZIP Code" diff --git a/homeassistant/components/kaleidescape/strings.json b/homeassistant/components/kaleidescape/strings.json index 07c5d5bafd9..92b9c931acd 100644 --- a/homeassistant/components/kaleidescape/strings.json +++ b/homeassistant/components/kaleidescape/strings.json @@ -3,14 +3,12 @@ "flow_title": "{model} ({name})", "step": { "user": { - "title": "Kaleidescape Setup", "data": { "host": "[%key:common::config_flow::data::host%]" } }, "discovery_confirm": { - "title": "Kaleidescape", - "description": "Do you want to set up the {model} player named {name}?" + "description": "Do you want to set up the {model} player named {name}?" } }, "abort": { @@ -24,4 +22,4 @@ "unsupported": "Unsupported device" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json index 9cb68a0ca73..3ff8d4308a3 100644 --- a/homeassistant/components/meteo_france/strings.json +++ b/homeassistant/components/meteo_france/strings.json @@ -2,14 +2,12 @@ "config": { "step": { "user": { - "title": "M\u00e9t\u00e9o-France", "description": "Enter the postal code (only for France, recommended) or city name", "data": { "city": "City" } }, "cities": { - "title": "M\u00e9t\u00e9o-France", "description": "Choose your city from the list", "data": { "city": "City" diff --git a/homeassistant/components/meteoclimatic/strings.json b/homeassistant/components/meteoclimatic/strings.json index 2353c22c7cc..5aedf7da01a 100644 --- a/homeassistant/components/meteoclimatic/strings.json +++ b/homeassistant/components/meteoclimatic/strings.json @@ -2,10 +2,11 @@ "config": { "step": { "user": { - "title": "Meteoclimatic", - "description": "Enter the Meteoclimatic station code (e.g., ESCAT4300000043206B)", "data": { "code": "Station code" + }, + "data_description": { + "code": "Looks like ESCAT4300000043206B" } } }, @@ -17,4 +18,4 @@ "not_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/modem_callerid/strings.json b/homeassistant/components/modem_callerid/strings.json index 17359128528..bb6ac1879da 100644 --- a/homeassistant/components/modem_callerid/strings.json +++ b/homeassistant/components/modem_callerid/strings.json @@ -1,26 +1,24 @@ { - "config": { - "step": { - "user": { - "title": "Phone Modem", - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call.", - "data": { - "name": "[%key:common::config_flow::data::name%]", - "port": "[%key:common::config_flow::data::port%]" - } - }, - "usb_confirm": { - "title": "Phone Modem", - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call." + "config": { + "step": { + "user": { + "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call.", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "port": "[%key:common::config_flow::data::port%]" } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "no_devices_found": "No remaining devices found" + "usb_confirm": { + "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call." } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "no_devices_found": "No remaining devices found" } - } \ No newline at end of file + } +} diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 627ae72ffe3..9b64da7add4 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -2,14 +2,12 @@ "config": { "step": { "user": { - "title": "Motion Blinds", "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", "data": { "host": "[%key:common::config_flow::data::ip%]" } }, "connect": { - "title": "Motion Blinds", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", @@ -37,8 +35,6 @@ "options": { "step": { "init": { - "title": "Motion Blinds", - "description": "Specify optional settings", "data": { "wait_for_push": "Wait for multicast push on update" } @@ -46,6 +42,3 @@ } } } - - - diff --git a/homeassistant/components/nfandroidtv/strings.json b/homeassistant/components/nfandroidtv/strings.json index 5940f86a406..fdc9f01d343 100644 --- a/homeassistant/components/nfandroidtv/strings.json +++ b/homeassistant/components/nfandroidtv/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Notifications for Android TV / Fire TV", - "description": "This integration requires the Notifications for Android TV app.\n\nFor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nYou should set up either DHCP reservation on your router (refer to your router's user manual) or a static IP address on the device. If not, the device will eventually become unavailable.", + "description": "Please refer to the documentation to make sure all requirements are met.", "data": { "host": "[%key:common::config_flow::data::host%]", "name": "[%key:common::config_flow::data::name%]" diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index ed9cf05cae8..f53ffeda6f6 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "init": { - "title": "OpenTherm Gateway", "data": { "name": "[%key:common::config_flow::data::name%]", "device": "Path or URL", @@ -19,7 +18,6 @@ "options": { "step": { "init": { - "description": "Options for the OpenTherm Gateway", "data": { "floor_temperature": "Floor Temperature", "read_precision": "Read Precision", diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json index 520cf1181ff..12d5c3e21f6 100644 --- a/homeassistant/components/openweathermap/strings.json +++ b/homeassistant/components/openweathermap/strings.json @@ -1,35 +1,34 @@ { - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": { - "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "language": "Language", - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]", - "mode": "[%key:common::config_flow::data::mode%]", - "name": "Name of the integration" - }, - "description": "Set up OpenWeatherMap integration. To generate API key go to https://openweathermap.org/appid", - "title": "OpenWeatherMap" - } - } + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" }, - "options": { - "step": { - "init": { - "data": { - "language": "Language", - "mode": "[%key:common::config_flow::data::mode%]" - } - } - } + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "language": "Language", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "mode": "[%key:common::config_flow::data::mode%]", + "name": "Name" + }, + "description": "To generate API key go to https://openweathermap.org/appid" + } } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Language", + "mode": "[%key:common::config_flow::data::mode%]" + } + } + } + } } diff --git a/homeassistant/components/peco/strings.json b/homeassistant/components/peco/strings.json index 2d195ed5d22..f89948bb268 100644 --- a/homeassistant/components/peco/strings.json +++ b/homeassistant/components/peco/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "PECO Outage Counter", "description": "Please choose your county below.", "data": { "county": "County" @@ -13,4 +12,4 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index c16a84b1cd8..f08b6f59862 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -3,11 +3,9 @@ "flow_title": "{name} ({host})", "step": { "user": { - "title": "Plex Media Server", "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server." }, "user_advanced": { - "title": "Plex Media Server", "data": { "setup_method": "Setup method" } diff --git a/homeassistant/components/poolsense/strings.json b/homeassistant/components/poolsense/strings.json index 71b30e0bf77..2ddf3ee77e8 100644 --- a/homeassistant/components/poolsense/strings.json +++ b/homeassistant/components/poolsense/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "PoolSense", - "description": "[%key:common::config_flow::description::confirm_setup%]", "data": { "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json index f2a59830b59..b44862c527b 100644 --- a/homeassistant/components/ps4/strings.json +++ b/homeassistant/components/ps4/strings.json @@ -2,25 +2,26 @@ "config": { "step": { "creds": { - "title": "PlayStation 4", "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." }, "mode": { - "title": "PlayStation 4", - "description": "Select mode for configuration. The [%key:common::config_flow::data::ip%] field can be left blank if selecting Auto Discovery, as devices will be automatically discovered.", "data": { "mode": "Config Mode", "ip_address": "[%key:common::config_flow::data::ip%] (Leave empty if using Auto Discovery)." + }, + "data_description": { + "ip_address": "Leave blank if selecting auto-discovery." } }, "link": { - "title": "PlayStation 4", - "description": "Enter your PlayStation 4 information. For [%key:common::config_flow::data::pin%], navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the [%key:common::config_flow::data::pin%] that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", "data": { "region": "Region", "name": "[%key:common::config_flow::data::name%]", "code": "[%key:common::config_flow::data::pin%]", "ip_address": "[%key:common::config_flow::data::ip%]" + }, + "data_description": { + "code": "Navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device' to get the pin." } } }, diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json index 89da917c8ea..a008ef9f4da 100644 --- a/homeassistant/components/pvpc_hourly_pricing/strings.json +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Sensor setup", - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "data": { "name": "Sensor Name", "tariff": "Applicable tariff by geographic zone", @@ -19,8 +17,6 @@ "options": { "step": { "init": { - "title": "Sensor setup", - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "data": { "tariff": "Applicable tariff by geographic zone", "power": "Contracted power (kW)", diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 68cbe528e87..04c504def03 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -9,7 +9,6 @@ } }, "discovery_confirm": { - "title": "Roku", "description": "Do you want to set up {name}?" } }, diff --git a/homeassistant/components/sentry/strings.json b/homeassistant/components/sentry/strings.json index 71196d52f8d..efcdb631f3c 100644 --- a/homeassistant/components/sentry/strings.json +++ b/homeassistant/components/sentry/strings.json @@ -2,10 +2,8 @@ "config": { "step": { "user": { - "title": "Sentry", - "description": "Enter your Sentry DSN", "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } }, diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 501cbfb5fff..6b13226aaee 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -3,7 +3,6 @@ "flow_title": "{name} ({host})", "step": { "user": { - "title": "Synology DSM", "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]", @@ -20,7 +19,6 @@ } }, "link": { - "title": "Synology DSM", "description": "Do you want to setup {name} ({host})?", "data": { "ssl": "[%key:common::config_flow::data::ssl%]", diff --git a/homeassistant/components/tasmota/strings.json b/homeassistant/components/tasmota/strings.json index 3d32b54b95d..2a23912b80c 100644 --- a/homeassistant/components/tasmota/strings.json +++ b/homeassistant/components/tasmota/strings.json @@ -5,8 +5,6 @@ "description": "Do you want to set up Tasmota?" }, "config": { - "title": "Tasmota", - "description": "Please enter the Tasmota configuration.", "data": { "discovery_prefix": "Discovery topic prefix" } diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json index 34e218741ca..2876bf5bd02 100644 --- a/homeassistant/components/tibber/strings.json +++ b/homeassistant/components/tibber/strings.json @@ -13,8 +13,7 @@ "data": { "access_token": "[%key:common::config_flow::data::access_token%]" }, - "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 63505c2446c..64ca1beafd8 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Total Connect", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/twentemilieu/strings.json b/homeassistant/components/twentemilieu/strings.json index 369484a6392..d9b59b2d02c 100644 --- a/homeassistant/components/twentemilieu/strings.json +++ b/homeassistant/components/twentemilieu/strings.json @@ -2,7 +2,6 @@ "config": { "step": { "user": { - "title": "Twente Milieu", "description": "Set up Twente Milieu providing waste collection information on your address.", "data": { "post_code": "Postal code", @@ -15,6 +14,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_address": "Address not found in Twente Milieu service area." }, - "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + } } } diff --git a/homeassistant/components/twinkly/strings.json b/homeassistant/components/twinkly/strings.json index 6dc7cc2eb97..630497ef28c 100644 --- a/homeassistant/components/twinkly/strings.json +++ b/homeassistant/components/twinkly/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Twinkly", - "description": "Set up your Twinkly led string", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/vallox/config_flow.py b/homeassistant/components/vallox/config_flow.py index 1658a987263..d30c8641d2c 100644 --- a/homeassistant/components/vallox/config_flow.py +++ b/homeassistant/components/vallox/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address from .const import DEFAULT_NAME, DOMAIN @@ -83,14 +82,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" - integration = await async_get_integration(self.hass, DOMAIN) - if user_input is None: return self.async_show_form( step_id="user", - description_placeholders={ - "integration_docs_url": integration.documentation - }, data_schema=STEP_USER_DATA_SCHEMA, ) @@ -120,9 +114,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - description_placeholders={ - "integration_docs_url": integration.documentation - }, data_schema=STEP_USER_DATA_SCHEMA, errors=errors, ) diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index dd341228db8..cada5a7febd 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Vallox", - "description": "Set up the Vallox integration. If you have problems with configuration go to {integration_docs_url}.", "data": { "host": "[%key:common::config_flow::data::host%]" } diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 66958f44a62..50d60f9a8ab 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -5,12 +5,13 @@ }, "step": { "user": { - "title": "Setup Vera controller", - "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.", "data": { "vera_controller_url": "Controller URL", "lights": "Vera switch device ids to treat as lights in Home Assistant.", "exclude": "Vera device ids to exclude from Home Assistant." + }, + "data_description": { + "vera_controller_url": "It should look like this: http://192.168.1.161:3480" } } } diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json index c9b8882c264..6577c99456c 100644 --- a/homeassistant/components/vilfo/strings.json +++ b/homeassistant/components/vilfo/strings.json @@ -2,8 +2,6 @@ "config": { "step": { "user": { - "title": "Connect to the Vilfo Router", - "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", "data": { "host": "[%key:common::config_flow::data::host%]", "access_token": "[%key:common::config_flow::data::access_token%]" diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json index e267a8e5327..0449a900c29 100644 --- a/homeassistant/components/wilight/strings.json +++ b/homeassistant/components/wilight/strings.json @@ -3,8 +3,7 @@ "flow_title": "{name}", "step": { "confirm": { - "title": "WiLight", - "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}" + "description": "The following components are supported: {components}" } }, "abort": { diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index b1675992174..66ad4d01354 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -3,8 +3,7 @@ "flow_title": "{name}", "step": { "user": { - "title": "Xiaomi Aqara Gateway", - "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used", + "description": "If the IP and MAC addresses are left empty, auto-discovery is used", "data": { "interface": "The network interface to use", "host": "[%key:common::config_flow::data::ip%] (optional)", @@ -12,7 +11,7 @@ } }, "settings": { - "title": "Xiaomi Aqara Gateway, optional settings", + "title": "Optional settings", "description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible", "data": { "key": "The key of your gateway", @@ -20,8 +19,7 @@ } }, "select": { - "title": "Select the Xiaomi Aqara Gateway that you wish to connect", - "description": "Run the setup again if you want to connect additional gateways", + "description": "Select the Xiaomi Aqara Gateway that you wish to connect", "data": { "select_ip": "[%key:common::config_flow::data::ip%]" } diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 1331d22e933..e359f54cc5a 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -28,30 +28,25 @@ "cloud_country": "Cloud server country", "manual": "Configure manually (not recommended)" }, - "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use." }, "select": { "data": { "select_device": "Miio device" }, - "description": "Select the Xiaomi Miio device to setup.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "Select the Xiaomi Miio device to setup." }, "manual": { "data": { "host": "[%key:common::config_flow::data::ip%]", "token": "[%key:common::config_flow::data::api_token%]" }, - "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "You will need the 32 character [%key:common::config_flow::data::api_token%], see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this [%key:common::config_flow::data::api_token%] is different from the key used by the Xiaomi Aqara integration." }, "connect": { "data": { "model": "Device model" - }, - "description": "Manually select the device model from the supported models.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + } } } }, @@ -61,8 +56,6 @@ }, "step": { "init": { - "title": "Xiaomi Miio", - "description": "Specify optional settings", "data": { "cloud_subdevices": "Use cloud to get connected subdevices" } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 38392832261..0dcdbc133a6 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -26,6 +26,7 @@ ALLOW_NAME_TRANSLATION = { "cert_expiry", "cpuspeed", "emulated_roku", + "faa_delays", "garages_amsterdam", "google_travel_time", "homekit_controller", @@ -50,6 +51,16 @@ MOVED_TRANSLATIONS_DIRECTORY_MSG = ( ) +def allow_name_translation(integration: Integration): + """Validate that the translation name is not the same as the integration name.""" + # Only enforce for core because custom integrations can't be + # added to allow list. + return integration.core and ( + integration.domain in ALLOW_NAME_TRANSLATION + or integration.quality_scale == "internal" + ) + + def check_translations_directory_name(integration: Integration) -> None: """Check that the correct name is used for the translations directory.""" legacy_translations = integration.path / ".translations" @@ -156,6 +167,21 @@ def gen_data_entry_schema( validators.append(validate_description_set) + if not allow_name_translation(integration): + + def name_validator(value): + """Validate name.""" + for step_id, info in value["step"].items(): + if info.get("title") == integration.name: + raise vol.Invalid( + f"Do not set title of step {step_id} if it's a brand name " + "or add exception to ALLOW_NAME_TRANSLATION" + ) + + return value + + validators.append(name_validator) + return vol.All(*validators) @@ -315,14 +341,9 @@ def validate_translation_file(config: Config, integration: Integration, all_stri if strings_file.name == "strings.json": find_references(strings, name, references) - if ( - integration.domain not in ALLOW_NAME_TRANSLATION - # Only enforce for core because custom integratinos can't be - # added to allow list. - and integration.core - and strings.get("title") == integration.name - and integration.quality_scale != "internal" - ): + if strings.get( + "title" + ) == integration.name and not allow_name_translation(integration): integration.add_error( "translations", "Don't specify title in translation strings if it's a brand name " From de130d3b28ed5f258e8c39e977ad3b5c9e0657fc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 28 Mar 2022 21:04:19 +0200 Subject: [PATCH 0766/1054] Update REST api mocking in SamsungTV (#68172) Co-authored-by: epenet --- .../components/samsungtv/test_config_flow.py | 16 ++++----- tests/components/samsungtv/test_init.py | 19 ++++------- .../components/samsungtv/test_media_player.py | 33 ++++++++++++------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index ba66cbc67ae..2d3be2e2bc1 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -731,11 +731,9 @@ async def test_ssdp_encrypted_websocket_not_supported( assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_websocket_cannot_connect( - hass: HomeAssistant, rest_api: Mock -) -> None: +@pytest.mark.usefixtures("rest_api_failing") +async def test_ssdp_websocket_cannot_connect(hass: HomeAssistant) -> None: """Test starting a flow from discovery and we cannot connect.""" - rest_api.rest_device_info.return_value = None with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), @@ -901,10 +899,9 @@ async def test_import_legacy(hass: HomeAssistant) -> None: assert entries[0].data[CONF_PORT] == LEGACY_PORT -@pytest.mark.usefixtures("remote", "remotews") -async def test_import_legacy_without_name(hass: HomeAssistant, rest_api: Mock) -> None: +@pytest.mark.usefixtures("remote", "remotews", "rest_api_failing") +async def test_import_legacy_without_name(hass: HomeAssistant) -> None: """Test importing from yaml without a name.""" - rest_api.rest_device_info.return_value = None with patch( "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", side_effect=WebSocketProtocolError("Boom"), @@ -1110,10 +1107,9 @@ async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, rest_api: Mock) -> assert result["reason"] == "not_supported" -@pytest.mark.usefixtures("remote", "remotews", "remoteencws") -async def test_zeroconf_no_device_info(hass: HomeAssistant, rest_api: Mock) -> None: +@pytest.mark.usefixtures("remote", "remotews", "remoteencws", "rest_api_failing") +async def test_zeroconf_no_device_info(hass: HomeAssistant) -> None: """Test starting a flow from zeroconf where device_info returns None.""" - rest_api.rest_device_info.return_value = None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 9d3289495a6..7d688a5febb 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,5 +1,4 @@ """Tests for the Samsung TV Integration.""" -from copy import deepcopy from unittest.mock import Mock, patch import pytest @@ -32,6 +31,7 @@ from .const import ( MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, + SAMPLE_DEVICE_INFO_UE48JU6400, ) from tests.common import MockConfigEntry @@ -68,12 +68,7 @@ REMOTE_CALL = { } -@pytest.fixture(name="autouse_rest_api", autouse=True) -def autouse_rest_api(rest_api) -> Mock: - """Enable auto use of the rest api fixture for these tests.""" - - -@pytest.mark.usefixtures("remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") async def test_setup(hass: HomeAssistant) -> None: """Test Samsung TV integration is setup.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -115,7 +110,7 @@ async def test_setup_from_yaml_without_port_device_offline(hass: HomeAssistant) assert config_entries_domain[0].state == ConfigEntryState.SETUP_RETRY -@pytest.mark.usefixtures("remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") async def test_setup_from_yaml_without_port_device_online(hass: HomeAssistant) -> None: """Test import from yaml when the device is online.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -144,7 +139,7 @@ async def test_setup_duplicate_config( assert "duplicate host entries found" in caplog.text -@pytest.mark.usefixtures("remote", "remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") async def test_setup_duplicate_entries(hass: HomeAssistant) -> None: """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) @@ -160,9 +155,7 @@ async def test_setup_h_j_model( hass: HomeAssistant, rest_api: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test Samsung TV integration is setup.""" - device_info = deepcopy(rest_api.rest_device_info.return_value) - device_info["device"]["modelName"] = "UE48JU6400" - rest_api.rest_device_info.return_value = device_info + rest_api.rest_device_info.return_value = SAMPLE_DEVICE_INFO_UE48JU6400 await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -170,7 +163,7 @@ async def test_setup_h_j_model( assert "H and J series use an encrypted protocol" in caplog.text -@pytest.mark.usefixtures("remote", "remotews", "remoteencws_failing") +@pytest.mark.usefixtures("remotews", "remoteencws_failing", "rest_api") async def test_setup_updates_from_ssdp(hass: HomeAssistant) -> None: """Test setting up the entry fetches data from ssdp cache.""" entry = MockConfigEntry(domain="samsungtv", data=MOCK_ENTRYDATA_WS) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index fa8cd871de9..f4bb40845a8 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -88,6 +88,7 @@ from . import ( from .const import ( MOCK_ENTRYDATA_ENCRYPTED_WS, SAMPLE_DEVICE_INFO_FRAME, + SAMPLE_DEVICE_INFO_WIFI, SAMPLE_EVENT_ED_INSTALLED_APP, ) @@ -153,11 +154,6 @@ MOCK_CONFIG_NOTURNON = { } -@pytest.fixture(name="autouse_rest_api", autouse=True) -def autouse_rest_api(rest_api) -> Mock: - """Enable auto use of the rest api fixture for these tests.""" - - @pytest.fixture(name="delay") def delay_fixture(): """Patch the delay script function.""" @@ -187,7 +183,7 @@ async def test_setup_without_turnon(hass: HomeAssistant) -> None: assert hass.states.get(ENTITY_ID_NOTURNON) -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_setup_websocket(hass: HomeAssistant) -> None: """Test setup of platform.""" with patch( @@ -212,6 +208,7 @@ async def test_setup_websocket(hass: HomeAssistant) -> None: assert config_entries[0].data[CONF_MAC] == "aa:bb:ww:ii:ff:ii" +@pytest.mark.usefixtures("rest_api") async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> None: """Test setup of platform from config entry.""" entity_id = f"{DOMAIN}.fake" @@ -250,6 +247,7 @@ async def test_setup_websocket_2(hass: HomeAssistant, mock_now: datetime) -> Non remote_class.assert_called_once_with(**MOCK_CALLS_WS) +@pytest.mark.usefixtures("rest_api") async def test_setup_encrypted_websocket( hass: HomeAssistant, mock_now: datetime ) -> None: @@ -351,7 +349,7 @@ async def test_update_off_ws_with_power_state( assert state.state == STATE_OFF # First update uses start_listening once, and initialises device_info - device_info = deepcopy(rest_api.rest_device_info.return_value) + device_info = deepcopy(SAMPLE_DEVICE_INFO_WIFI) device_info["device"]["PowerState"] = "on" rest_api.rest_device_info.return_value = device_info next_update = mock_now + timedelta(minutes=1) @@ -447,6 +445,7 @@ async def test_update_access_denied(hass: HomeAssistant, mock_now: datetime) -> assert state.state == STATE_UNAVAILABLE +@pytest.mark.usefixtures("rest_api") async def test_update_ws_connection_failure( hass: HomeAssistant, mock_now: datetime, @@ -476,6 +475,7 @@ async def test_update_ws_connection_failure( assert state.state == STATE_OFF +@pytest.mark.usefixtures("rest_api") async def test_update_ws_connection_closed( hass: HomeAssistant, mock_now: datetime, remotews: Mock ) -> None: @@ -494,6 +494,7 @@ async def test_update_ws_connection_closed( assert state.state == STATE_OFF +@pytest.mark.usefixtures("rest_api") async def test_update_ws_unauthorized_error( hass: HomeAssistant, mock_now: datetime, remotews: Mock ) -> None: @@ -627,6 +628,7 @@ async def test_send_key_unhandled_response(hass: HomeAssistant, remote: Mock) -> assert state.state == STATE_ON +@pytest.mark.usefixtures("rest_api") async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -638,6 +640,7 @@ async def test_send_key_websocketexception(hass: HomeAssistant, remotews: Mock) assert state.state == STATE_ON +@pytest.mark.usefixtures("rest_api") async def test_send_key_websocketexception_encrypted( hass: HomeAssistant, remoteencws: Mock ) -> None: @@ -651,6 +654,7 @@ async def test_send_key_websocketexception_encrypted( assert state.state == STATE_ON +@pytest.mark.usefixtures("rest_api") async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None: """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -662,6 +666,7 @@ async def test_send_key_os_error_ws(hass: HomeAssistant, remotews: Mock) -> None assert state.state == STATE_ON +@pytest.mark.usefixtures("rest_api") async def test_send_key_os_error_ws_encrypted( hass: HomeAssistant, remoteencws: Mock ) -> None: @@ -767,6 +772,7 @@ async def test_device_class(hass: HomeAssistant) -> None: assert state.attributes[ATTR_DEVICE_CLASS] is MediaPlayerDeviceClass.TV.value +@pytest.mark.usefixtures("rest_api") async def test_turn_off_websocket( hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -924,6 +930,7 @@ async def test_turn_off_os_error( assert "Could not establish connection" in caplog.text +@pytest.mark.usefixtures("rest_api") async def test_turn_off_ws_os_error( hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -937,6 +944,7 @@ async def test_turn_off_ws_os_error( assert "Error closing connection" in caplog.text +@pytest.mark.usefixtures("rest_api") async def test_turn_off_encryptedws_os_error( hass: HomeAssistant, remoteencws: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -1072,7 +1080,7 @@ async def test_turn_on_with_turnon(hass: HomeAssistant, delay: Mock) -> None: assert delay.call_count == 1 -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_turn_on_wol(hass: HomeAssistant) -> None: """Test turn on.""" entry = MockConfigEntry( @@ -1229,6 +1237,7 @@ async def test_select_source_invalid_source(hass: HomeAssistant) -> None: assert remote.call_count == 1 +@pytest.mark.usefixtures("rest_api") async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for play_media.""" await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -1251,6 +1260,7 @@ async def test_play_media_app(hass: HomeAssistant, remotews: Mock) -> None: assert commands[0].params["data"]["appId"] == "3201608010191" +@pytest.mark.usefixtures("rest_api") async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: """Test for select_source.""" remotews.app_list_data = SAMPLE_EVENT_ED_INSTALLED_APP @@ -1270,6 +1280,7 @@ async def test_select_source_app(hass: HomeAssistant, remotews: Mock) -> None: assert commands[0].params["data"]["appId"] == "3201608010191" +@pytest.mark.usefixtures("rest_api") async def test_websocket_unsupported_remote_control( hass: HomeAssistant, remotews: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -1318,7 +1329,7 @@ async def test_websocket_unsupported_remote_control( assert state.state == STATE_UNAVAILABLE -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_volume_control_upnp( hass: HomeAssistant, upnp_device: Mock, caplog: pytest.LogCaptureFixture ) -> None: @@ -1362,7 +1373,7 @@ async def test_volume_control_upnp( assert "Unable to set volume level on" in caplog.text -@pytest.mark.usefixtures("remotews") +@pytest.mark.usefixtures("remotews", "rest_api") async def test_upnp_not_available( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1379,7 +1390,7 @@ async def test_upnp_not_available( assert "Upnp services are not available" in caplog.text -@pytest.mark.usefixtures("remotews", "upnp_device") +@pytest.mark.usefixtures("remotews", "upnp_device", "rest_api") async def test_upnp_missing_service( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From c1a2be72fc8b76b55cfde1823c5688100e397369 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 28 Mar 2022 20:08:00 +0100 Subject: [PATCH 0767/1054] Generic IP Camera configflow 2 (#52360) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 + homeassistant/components/generic/__init__.py | 22 + homeassistant/components/generic/camera.py | 87 ++- .../components/generic/config_flow.py | 338 ++++++++++++ homeassistant/components/generic/const.py | 14 +- .../components/generic/manifest.json | 6 +- homeassistant/components/generic/strings.json | 76 +++ .../components/generic/translations/en.json | 76 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/generic/conftest.py | 36 ++ tests/components/generic/test_camera.py | 316 ++++++----- tests/components/generic/test_config_flow.py | 510 ++++++++++++++++++ 14 files changed, 1296 insertions(+), 190 deletions(-) create mode 100644 homeassistant/components/generic/config_flow.py create mode 100644 homeassistant/components/generic/strings.json create mode 100644 homeassistant/components/generic/translations/en.json create mode 100644 tests/components/generic/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 183d699d2ae..42eb1f896f8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -359,6 +359,8 @@ homeassistant/components/garages_amsterdam/* @klaasnicolaas tests/components/garages_amsterdam/* @klaasnicolaas homeassistant/components/gdacs/* @exxamalte tests/components/gdacs/* @exxamalte +homeassistant/components/generic/* @davet2001 +tests/components/generic/* @davet2001 homeassistant/components/generic_hygrostat/* @Shulyaka tests/components/generic_hygrostat/* @Shulyaka homeassistant/components/geniushub/* @zxdavb diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index 3ca526f2029..f243f1639b3 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -1,6 +1,28 @@ """The generic component.""" +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform +from homeassistant.core import HomeAssistant DOMAIN = "generic" PLATFORMS = [Platform.CAMERA] + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up generic IP camera from a config entry.""" + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index b4aaad38618..72fec27b733 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -12,6 +12,7 @@ from homeassistant.components.camera import ( SUPPORT_STREAM, Camera, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -23,15 +24,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, template as template_helper from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, PLATFORMS +from . import DOMAIN from .const import ( - ALLOWED_RTSP_TRANSPORT_PROTOCOLS, CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, @@ -41,6 +40,7 @@ from .const import ( DEFAULT_NAME, FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, + RTSP_TRANSPORTS, ) _LOGGER = logging.getLogger(__name__) @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( cv.small_float, cv.positive_int ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RTSP_TRANSPORT): vol.In(ALLOWED_RTSP_TRANSPORT_PROTOCOLS), + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS.keys()), } ) @@ -75,25 +75,78 @@ async def async_setup_platform( ) -> None: """Set up a generic IP Camera.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + _LOGGER.warning( + "Loading generic IP camera via configuration.yaml is deprecated, " + "it will be automatically imported. Once you have confirmed correct " + "operation, please remove 'generic' (IP camera) section(s) from " + "configuration.yaml" + ) + image = config.get(CONF_STILL_IMAGE_URL) + stream = config.get(CONF_STREAM_SOURCE) + config_new = { + CONF_NAME: config[CONF_NAME], + CONF_STILL_IMAGE_URL: image.template if image is not None else None, + CONF_STREAM_SOURCE: stream.template if stream is not None else None, + CONF_AUTHENTICATION: config.get(CONF_AUTHENTICATION), + CONF_USERNAME: config.get(CONF_USERNAME), + CONF_PASSWORD: config.get(CONF_PASSWORD), + CONF_LIMIT_REFETCH_TO_URL_CHANGE: config.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE), + CONF_CONTENT_TYPE: config.get(CONF_CONTENT_TYPE), + CONF_FRAMERATE: config.get(CONF_FRAMERATE), + CONF_VERIFY_SSL: config.get(CONF_VERIFY_SSL), + } - async_add_entities([GenericCamera(hass, config)]) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up a generic IP Camera.""" + + async_add_entities( + [GenericCamera(hass, entry.options, entry.unique_id, entry.title)] + ) + + +def generate_auth(device_info) -> httpx.Auth | None: + """Generate httpx.Auth object from credentials.""" + username = device_info.get(CONF_USERNAME) + password = device_info.get(CONF_PASSWORD) + authentication = device_info.get(CONF_AUTHENTICATION) + if username: + if authentication == HTTP_DIGEST_AUTHENTICATION: + return httpx.DigestAuth(username=username, password=password) + return httpx.BasicAuth(username=username, password=password) + return None class GenericCamera(Camera): """A generic implementation of an IP camera.""" - def __init__(self, hass, device_info): + def __init__(self, hass, device_info, identifier, title): """Initialize a generic camera.""" super().__init__() self.hass = hass + self._attr_unique_id = identifier self._authentication = device_info.get(CONF_AUTHENTICATION) - self._name = device_info.get(CONF_NAME) + self._name = device_info.get(CONF_NAME, title) self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) - if self._still_image_url: + if ( + not isinstance(self._still_image_url, template_helper.Template) + and self._still_image_url + ): + self._still_image_url = cv.template(self._still_image_url) + if self._still_image_url not in [None, ""]: self._still_image_url.hass = hass self._stream_source = device_info.get(CONF_STREAM_SOURCE) - if self._stream_source is not None: + if self._stream_source not in (None, ""): + if not isinstance(self._stream_source, template_helper.Template): + self._stream_source = cv.template(self._stream_source) self._stream_source.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] self._attr_frame_interval = 1 / device_info[CONF_FRAMERATE] @@ -104,17 +157,7 @@ class GenericCamera(Camera): self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[ CONF_RTSP_TRANSPORT ] - - username = device_info.get(CONF_USERNAME) - password = device_info.get(CONF_PASSWORD) - - if username and password: - if self._authentication == HTTP_DIGEST_AUTHENTICATION: - self._auth = httpx.DigestAuth(username=username, password=password) - else: - self._auth = httpx.BasicAuth(username=username, password=password) - else: - self._auth = None + self._auth = generate_auth(device_info) self._last_url = None self._last_image = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py new file mode 100644 index 00000000000..1b484821788 --- /dev/null +++ b/homeassistant/components/generic/config_flow.py @@ -0,0 +1,338 @@ +"""Config flow for generic (IP Camera).""" +from __future__ import annotations + +import contextlib +from errno import EHOSTUNREACH, EIO +from functools import partial +import imghdr +import logging +from types import MappingProxyType +from typing import Any +from urllib.parse import urlparse, urlunparse + +from async_timeout import timeout +import av +from httpx import HTTPStatusError, RequestError, TimeoutException +import voluptuous as vol + +from homeassistant.components.stream.const import SOURCE_TIMEOUT +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import config_validation as cv, template as template_helper +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.util import slugify + +from .camera import generate_auth +from .const import ( + CONF_CONTENT_TYPE, + CONF_FRAMERATE, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + CONF_RTSP_TRANSPORT, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DEFAULT_NAME, + DOMAIN, + FFMPEG_OPTION_MAP, + GET_IMAGE_TIMEOUT, + RTSP_TRANSPORTS, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_DATA = { + CONF_NAME: DEFAULT_NAME, + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_FRAMERATE: 2, + CONF_VERIFY_SSL: True, +} + +SUPPORTED_IMAGE_TYPES = ["png", "jpeg", "svg+xml"] + + +def build_schema( + user_input: dict[str, Any] | MappingProxyType[str, Any], + is_options_flow: bool = False, +): + """Create schema for camera config setup.""" + spec = { + vol.Optional( + CONF_STILL_IMAGE_URL, + description={"suggested_value": user_input.get(CONF_STILL_IMAGE_URL, "")}, + ): str, + vol.Optional( + CONF_STREAM_SOURCE, + description={"suggested_value": user_input.get(CONF_STREAM_SOURCE, "")}, + ): str, + vol.Optional( + CONF_RTSP_TRANSPORT, + description={"suggested_value": user_input.get(CONF_RTSP_TRANSPORT)}, + ): vol.In(RTSP_TRANSPORTS), + vol.Optional( + CONF_AUTHENTICATION, + description={"suggested_value": user_input.get(CONF_AUTHENTICATION)}, + ): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), + vol.Optional( + CONF_USERNAME, + description={"suggested_value": user_input.get(CONF_USERNAME, "")}, + ): str, + vol.Optional( + CONF_PASSWORD, + description={"suggested_value": user_input.get(CONF_PASSWORD, "")}, + ): str, + vol.Required( + CONF_FRAMERATE, + description={"suggested_value": user_input.get(CONF_FRAMERATE, 2)}, + ): int, + vol.Required( + CONF_VERIFY_SSL, default=user_input.get(CONF_VERIFY_SSL, True) + ): bool, + } + if is_options_flow: + spec[ + vol.Required( + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False), + ) + ] = bool + return vol.Schema(spec) + + +def get_image_type(image): + """Get the format of downloaded bytes that could be an image.""" + fmt = imghdr.what(None, h=image) + if fmt is None: + # if imghdr can't figure it out, could be svg. + with contextlib.suppress(UnicodeDecodeError): + if image.decode("utf-8").startswith(" tuple[dict[str, str], str | None]: + """Verify that the still image is valid before we create an entity.""" + fmt = None + if not (url := info.get(CONF_STILL_IMAGE_URL)): + return {}, None + if not isinstance(url, template_helper.Template) and url: + url = cv.template(url) + url.hass = hass + try: + url = url.async_render(parse_result=False) + except TemplateError as err: + _LOGGER.error("Error parsing template %s: %s", url, err) + return {CONF_STILL_IMAGE_URL: "template_error"}, None + verify_ssl = info.get(CONF_VERIFY_SSL) + auth = generate_auth(info) + try: + async_client = get_async_client(hass, verify_ssl=verify_ssl) + async with timeout(GET_IMAGE_TIMEOUT): + response = await async_client.get(url, auth=auth, timeout=GET_IMAGE_TIMEOUT) + response.raise_for_status() + image = response.content + except ( + TimeoutError, + RequestError, + HTTPStatusError, + TimeoutException, + ) as err: + _LOGGER.error("Error getting camera image from %s: %s", url, type(err).__name__) + return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + + if not image: + return {CONF_STILL_IMAGE_URL: "unable_still_load"}, None + fmt = get_image_type(image) + _LOGGER.debug( + "Still image at '%s' detected format: %s", + info[CONF_STILL_IMAGE_URL], + fmt, + ) + if fmt not in SUPPORTED_IMAGE_TYPES: + return {CONF_STILL_IMAGE_URL: "invalid_still_image"}, None + return {}, f"image/{fmt}" + + +def slug_url(url) -> str | None: + """Convert a camera url into a string suitable for a camera name.""" + if not url: + return None + url_no_scheme = urlparse(url)._replace(scheme="") + return slugify(urlunparse(url_no_scheme).strip("/")) + + +async def async_test_stream(hass, info) -> dict[str, str]: + """Verify that the stream is valid before we create an entity.""" + if not (stream_source := info.get(CONF_STREAM_SOURCE)): + return {} + try: + # For RTSP streams, prefer TCP. This code is duplicated from + # homeassistant.components.stream.__init__.py:create_stream() + # It may be possible & better to call create_stream() directly. + stream_options: dict[str, str] = {} + if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": + stream_options = { + "rtsp_flags": "prefer_tcp", + "stimeout": "5000000", + } + if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): + stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport + _LOGGER.debug("Attempting to open stream %s", stream_source) + container = await hass.async_add_executor_job( + partial( + av.open, + stream_source, + options=stream_options, + timeout=SOURCE_TIMEOUT, + ) + ) + _ = container.streams.video[0] + except (av.error.FileNotFoundError): # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "stream_file_not_found"} + except (av.error.HTTPNotFoundError): # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "stream_http_not_found"} + except (av.error.TimeoutError): # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "timeout"} + except av.error.HTTPUnauthorizedError: # pylint: disable=c-extension-no-member + return {CONF_STREAM_SOURCE: "stream_unauthorised"} + except (KeyError, IndexError): + return {CONF_STREAM_SOURCE: "stream_no_video"} + except PermissionError: + return {CONF_STREAM_SOURCE: "stream_not_permitted"} + except OSError as err: + if err.errno == EHOSTUNREACH: + return {CONF_STREAM_SOURCE: "stream_no_route_to_host"} + if err.errno == EIO: # input/output error + return {CONF_STREAM_SOURCE: "stream_io_error"} + raise err + return {} + + +class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for generic IP camera.""" + + VERSION = 1 + + @staticmethod + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> GenericOptionsFlowHandler: + """Get the options flow for this handler.""" + return GenericOptionsFlowHandler(config_entry) + + def check_for_existing(self, options): + """Check whether an existing entry is using the same URLs.""" + return any( + entry.options[CONF_STILL_IMAGE_URL] == options[CONF_STILL_IMAGE_URL] + and entry.options[CONF_STREAM_SOURCE] == options[CONF_STREAM_SOURCE] + for entry in self._async_current_entries() + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the start of the config flow.""" + errors = {} + if user_input: + # Secondary validation because serialised vol can't seem to handle this complexity: + if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get( + CONF_STREAM_SOURCE + ): + errors["base"] = "no_still_image_or_stream_url" + else: + errors, still_format = await async_test_still(self.hass, user_input) + errors = errors | await async_test_stream(self.hass, user_input) + still_url = user_input.get(CONF_STILL_IMAGE_URL) + stream_url = user_input.get(CONF_STREAM_SOURCE) + name = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + + if not errors: + user_input[CONF_CONTENT_TYPE] = still_format + user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False + await self.async_set_unique_id(self.flow_id) + return self.async_create_entry( + title=name, data={}, options=user_input + ) + else: + user_input = DEFAULT_DATA.copy() + + return self.async_show_form( + step_id="user", + data_schema=build_schema(user_input), + errors=errors, + ) + + async def async_step_import(self, import_config) -> FlowResult: + """Handle config import from yaml.""" + # abort if we've already got this one. + if self.check_for_existing(import_config): + return self.async_abort(reason="already_exists") + errors, still_format = await async_test_still(self.hass, import_config) + errors = errors | await async_test_stream(self.hass, import_config) + still_url = import_config.get(CONF_STILL_IMAGE_URL) + stream_url = import_config.get(CONF_STREAM_SOURCE) + name = import_config.get( + CONF_NAME, slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + ) + if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: + import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False + if not errors: + import_config[CONF_CONTENT_TYPE] = still_format + await self.async_set_unique_id(self.flow_id) + return self.async_create_entry(title=name, data={}, options=import_config) + _LOGGER.error( + "Error importing generic IP camera platform config: unexpected error '%s'", + list(errors.values()), + ) + return self.async_abort(reason="unknown") + + +class GenericOptionsFlowHandler(OptionsFlow): + """Handle Generic IP Camera options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize Generic IP Camera options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage Generic IP Camera options.""" + errors: dict[str, str] = {} + + if user_input is not None: + errors, still_format = await async_test_still(self.hass, user_input) + errors = errors | await async_test_stream(self.hass, user_input) + still_url = user_input.get(CONF_STILL_IMAGE_URL) + stream_url = user_input.get(CONF_STREAM_SOURCE) + if not errors: + return self.async_create_entry( + title=slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME, + data={ + CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION), + CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE), + CONF_PASSWORD: user_input.get(CONF_PASSWORD), + CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL), + CONF_CONTENT_TYPE: still_format, + CONF_USERNAME: user_input.get(CONF_USERNAME), + CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[ + CONF_LIMIT_REFETCH_TO_URL_CHANGE + ], + CONF_FRAMERATE: user_input[CONF_FRAMERATE], + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + }, + ) + return self.async_show_form( + step_id="init", + data_schema=build_schema(user_input or self.config_entry.options, True), + errors=errors, + ) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 1b3ba657ecc..60b4cec61a6 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -1,5 +1,6 @@ """Constants for the generic (IP Camera) integration.""" +DOMAIN = "generic" DEFAULT_NAME = "Generic Camera" CONF_CONTENT_TYPE = "content_type" CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" @@ -8,6 +9,15 @@ CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" CONF_RTSP_TRANSPORT = "rtsp_transport" FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"} -ALLOWED_RTSP_TRANSPORT_PROTOCOLS = {"tcp", "udp", "udp_multicast", "http"} - +RTSP_TRANSPORTS = { + "tcp": "TCP", + "udp": "UDP", + "udp_multicast": "UDP Multicast", + "http": "HTTP", +} GET_IMAGE_TIMEOUT = 10 + +DEFAULT_USERNAME = None +DEFAULT_PASSWORD = None +DEFAULT_IMAGE_URL = None +DEFAULT_STREAM_SOURCE = None diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index ab6aa18c4d2..7b967108e77 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -1,7 +1,11 @@ { "domain": "generic", "name": "Generic Camera", + "config_flow": true, + "requirements": ["av==9.0.0"], "documentation": "https://www.home-assistant.io/integrations/generic", - "codeowners": [], + "codeowners": [ + "@davet2001" + ], "iot_class": "local_push" } diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json new file mode 100644 index 00000000000..eb1bfcc3c55 --- /dev/null +++ b/homeassistant/components/generic/strings.json @@ -0,0 +1,76 @@ +{ + "config": { + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "already_exists": "A camera with these URL settings already exists.", + "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "invalid_still_image": "URL did not return a valid still image", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "timeout": "Timeout while loading URL", + "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_no_video": "Stream has no video" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + }, + "step": { + "user": { + "description": "Enter the settings to connect to the camera.", + "data": { + "still_image_url": "Still Image URL (e.g. http://...)", + "stream_source": "Stream Source URL (e.g. rtsp://...)", + "rtsp_transport": "RTSP transport protocol", + "authentication": "Authentication", + "limit_refetch_to_url_change": "Limit refetch to url change", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "content_type": "Content Type", + "framerate": "Frame Rate (Hz)", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "still_image_url": "[%key:component::generic::config::step::user::data::still_image_url%]", + "stream_source": "[%key:component::generic::config::step::user::data::stream_source%]", + "rtsp_transport": "[%key:component::generic::config::step::user::data::rtsp_transport%]", + "authentication": "[%key:component::generic::config::step::user::data::authentication%]", + "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "content_type": "[%key:component::generic::config::step::user::data::content_type%]", + "framerate": "[%key:component::generic::config::step::user::data::framerate%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + } + }, + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]", + "already_exists": "[%key:component::generic::config::error::already_exists%]", + "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", + "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", + "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", + "stream_file_not_found": "[%key:component::generic::config::error::stream_file_not_found%]", + "stream_http_not_found": "[%key:component::generic::config::error::stream_http_not_found%]", + "timeout": "[%key:component::generic::config::error::timeout%]", + "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", + "stream_io_error": "[%key:component::generic::config::error::stream_io_error%]", + "stream_unauthorised": "[%key:component::generic::config::error::stream_unauthorised%]", + "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]", + "stream_no_video": "[%key:component::generic::config::error::stream_no_video%]" + } + } +} diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json new file mode 100644 index 00000000000..5346c2b5106 --- /dev/null +++ b/homeassistant/components/generic/translations/en.json @@ -0,0 +1,76 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "already_exists": "A camera with these URL settings already exists.", + "invalid_still_image": "URL did not return a valid still image", + "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "timeout": "Timeout while loading URL", + "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "unknown": "Unexpected error" + }, + "step": { + "confirm": { + "description": "Do you want to start set up?" + }, + "user": { + "data": { + "authentication": "Authentication", + "content_type": "Content Type", + "framerate": "Frame Rate (Hz)", + "limit_refetch_to_url_change": "Limit refetch to url change", + "password": "Password", + "rtsp_transport": "RTSP transport protocol", + "still_image_url": "Still Image URL (e.g. http://...)", + "stream_source": "Stream Source URL (e.g. rtsp://...)", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + }, + "description": "Enter the settings to connect to the camera." + } + } + }, + "options": { + "error": { + "already_exists": "A camera with these URL settings already exists.", + "invalid_still_image": "URL did not return a valid still image", + "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "timeout": "Timeout while loading URL", + "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", + "unknown": "Unexpected error" + }, + "step": { + "init": { + "data": { + "authentication": "Authentication", + "content_type": "Content Type", + "framerate": "Frame Rate (Hz)", + "limit_refetch_to_url_change": "Limit refetch to url change", + "password": "Password", + "rtsp_transport": "RTSP transport protocol", + "still_image_url": "Still Image URL (e.g. http://...)", + "stream_source": "Stream Source URL (e.g. rtsp://...)", + "username": "Username", + "verify_ssl": "Verify SSL certificate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1795f0c74b4..b8a5651a9e8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -119,6 +119,7 @@ FLOWS = { "fronius", "garages_amsterdam", "gdacs", + "generic", "geofency", "geonetnz_quakes", "geonetnz_volcano", diff --git a/requirements_all.txt b/requirements_all.txt index 0ff5a4d328a..36432c9c5ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -347,6 +347,7 @@ auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 +# homeassistant.components.generic # homeassistant.components.stream av==9.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50a8b9e110e..0d984a4a06c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -274,6 +274,7 @@ auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 +# homeassistant.components.generic # homeassistant.components.stream av==9.0.0 diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 04de1aedca9..63f7a87cba0 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -1,9 +1,14 @@ """Test fixtures for the generic component.""" from io import BytesIO +from unittest.mock import Mock, patch from PIL import Image import pytest +import respx + +from homeassistant import config_entries, setup +from homeassistant.components.generic.const import DOMAIN @pytest.fixture(scope="package") @@ -29,3 +34,34 @@ def fakeimgbytes_svg(): '', encoding="utf-8", ) + + +@pytest.fixture +def fakeimg_png(fakeimgbytes_png): + """Set up respx to respond to test url with fake image bytes.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + + +@pytest.fixture(scope="package") +def mock_av_open(): + """Fake container object with .streams.video[0] != None.""" + fake = Mock() + fake.streams.video = ["fakevid"] + return patch( + "homeassistant.components.generic.config_flow.av.open", + return_value=fake, + ) + + +@pytest.fixture +async def user_flow(hass): + """Initiate a user flow.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + return result diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index b08ed58841d..5a96391e10e 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -8,47 +8,47 @@ import httpx import pytest import respx -from homeassistant import config as hass_config from homeassistant.components.camera import async_get_mjpeg_stream -from homeassistant.components.generic import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import SERVICE_RELOAD +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.setup import async_setup_component -from tests.common import AsyncMock, Mock, get_fixture_path +from tests.common import AsyncMock, Mock @respx.mock -async def test_fetching_url(hass, hass_client, fakeimgbytes_png): +async def test_fetching_url(hass, hass_client, fakeimgbytes_png, mock_av_open): """Test that it fetches the given url.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "username": "user", - "password": "pass", - } - }, - ) - await hass.async_block_till_done() + with mock_av_open: + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "username": "user", + "password": "pass", + "authentication": "basic", + } + }, + ) + await hass.async_block_till_done() client = await hass_client() resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 2 body = await resp.read() assert body == fakeimgbytes_png resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 3 @respx.mock @@ -110,11 +110,14 @@ async def test_fetching_url_with_verify_ssl(hass, hass_client, fakeimgbytes_png) @respx.mock async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_jpg): """Test that it fetches the given url.""" + respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/5a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/10a").respond(stream=fakeimgbytes_png) respx.get("http://example.com/15a").respond(stream=fakeimgbytes_jpg) respx.get("http://example.com/20a").respond(status_code=HTTPStatus.NOT_FOUND) + hass.states.async_set("sensor.temp", "0") + await async_setup_component( hass, "camera", @@ -140,19 +143,19 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 0 - assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert respx.calls.call_count == 2 + assert resp.status == HTTPStatus.OK hass.states.async_set("sensor.temp", "10") resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 3 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_png resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 3 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_png @@ -161,7 +164,7 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j # Url change = fetch new image resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 4 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_jpg @@ -169,31 +172,37 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j # Cause a template render error hass.states.async_remove("sensor.temp") resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 4 assert resp.status == HTTPStatus.OK body = await resp.read() assert body == fakeimgbytes_jpg -async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): +@respx.mock +async def test_stream_source( + hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open +): """Test that the stream source is rendered.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) + respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, + hass.states.async_set("sensor.temp", "0") + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, + }, }, - }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() hass.states.async_set("sensor.temp", "5") @@ -217,26 +226,30 @@ async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png assert msg["result"]["url"][-13:] == "playlist.m3u8" -async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbytes_png): +@respx.mock +async def test_stream_source_error( + hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open +): """Test that the stream source has an error.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - # Does not exist - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + # Does not exist + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, + }, }, - }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() with patch( "homeassistant.components.camera.Stream.endpoint_url", @@ -261,31 +274,38 @@ async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbyt } -async def test_setup_alternative_options(hass, hass_ws_client): +@respx.mock +async def test_setup_alternative_options( + hass, hass_ws_client, fakeimgbytes_png, mock_av_open +): """Test that the stream source is setup with different config options.""" - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - "authentication": "digest", - "username": "user", - "password": "pass", - "stream_source": "rtsp://example.com:554/rtsp/", - "rtsp_transport": "udp", + respx.get("https://example.com").respond(stream=fakeimgbytes_png) + + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "https://example.com", + "authentication": "digest", + "username": "user", + "password": "pass", + "stream_source": "rtsp://example.com:554/rtsp/", + "rtsp_transport": "udp", + }, }, - }, - ) - await hass.async_block_till_done() + ) + await hass.async_block_till_done() assert hass.states.get("camera.config_test") +@respx.mock async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): """Test a stream request without stream source option set.""" - respx.get("http://example.com").respond(stream=fakeimgbytes_png) + respx.get("https://example.com").respond(stream=fakeimgbytes_png) assert await async_setup_component( hass, @@ -326,7 +346,7 @@ async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_ @respx.mock async def test_camera_content_type( - hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg + hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg, mock_av_open ): """Test generic camera with custom content_type.""" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" @@ -338,90 +358,54 @@ async def test_camera_content_type( "platform": "generic", "still_image_url": urlsvg, "content_type": "image/svg+xml", + "limit_refetch_to_url_change": False, + "framerate": 2, + "verify_ssl": True, } cam_config_jpg = { "name": "config_test_jpg", "platform": "generic", "still_image_url": urljpg, "content_type": "image/jpeg", + "limit_refetch_to_url_change": False, + "framerate": 2, + "verify_ssl": True, } - await async_setup_component( - hass, "camera", {"camera": [cam_config_svg, cam_config_jpg]} - ) - await hass.async_block_till_done() + with mock_av_open: + result1 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_jpg, + context={"source": SOURCE_IMPORT, "unique_id": 12345}, + ) + await hass.async_block_till_done() + with mock_av_open: + result2 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_svg, + context={"source": SOURCE_IMPORT, "unique_id": 54321}, + ) + await hass.async_block_till_done() + assert result1["type"] == "create_entry" + assert result2["type"] == "create_entry" client = await hass_client() resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 3 assert resp_1.status == HTTPStatus.OK assert resp_1.content_type == "image/svg+xml" body = await resp_1.read() assert body == fakeimgbytes_svg resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") - assert respx.calls.call_count == 2 + assert respx.calls.call_count == 4 assert resp_2.status == HTTPStatus.OK assert resp_2.content_type == "image/jpeg" body = await resp_2.read() assert body == fakeimgbytes_jpg -@respx.mock -async def test_reloading(hass, hass_client): - """Test we can cleanly reload.""" - respx.get("http://example.com").respond(text="hello world") - - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "username": "user", - "password": "pass", - } - }, - ) - await hass.async_block_till_done() - - client = await hass_client() - - resp = await client.get("/api/camera_proxy/camera.config_test") - - assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 1 - body = await resp.text() - assert body == "hello world" - - yaml_path = get_fixture_path("configuration.yaml", "generic") - - with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=True, - ) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - - resp = await client.get("/api/camera_proxy/camera.config_test") - - assert resp.status == HTTPStatus.NOT_FOUND - - resp = await client.get("/api/camera_proxy/camera.reload") - - assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 2 - body = await resp.text() - assert body == "hello world" - - @respx.mock async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbytes_jpg): """Test that timeouts and cancellations return last image.""" @@ -448,7 +432,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == HTTPStatus.OK - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 2 assert await resp.read() == fakeimgbytes_png respx.get("http://example.com").respond(stream=fakeimgbytes_jpg) @@ -458,7 +442,7 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt side_effect=asyncio.CancelledError(), ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert respx.calls.call_count == 1 + assert respx.calls.call_count == 2 assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR respx.get("http://example.com").side_effect = [ @@ -466,27 +450,28 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt httpx.TimeoutException, ] - for total_calls in range(2, 3): + for total_calls in range(3, 5): resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == total_calls assert resp.status == HTTPStatus.OK assert await resp.read() == fakeimgbytes_png -async def test_no_still_image_url(hass, hass_client): +async def test_no_still_image_url(hass, hass_client, mock_av_open): """Test that the component can grab images from stream with no still_image_url.""" - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", + with mock_av_open: + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + }, }, - }, - ) - await hass.async_block_till_done() + ) + await hass.async_block_till_done() client = await hass_client() @@ -518,22 +503,23 @@ async def test_no_still_image_url(hass, hass_client): assert await resp.read() == b"stream_keyframe_image" -async def test_frame_interval_property(hass): +async def test_frame_interval_property(hass, mock_av_open): """Test that the frame interval is calculated and returned correctly.""" - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - "framerate": 5, + with mock_av_open: + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + "framerate": 5, + }, }, - }, - ) - await hass.async_block_till_done() + ) + await hass.async_block_till_done() request = Mock() with patch( diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py new file mode 100644 index 00000000000..65bd8f7e386 --- /dev/null +++ b/tests/components/generic/test_config_flow.py @@ -0,0 +1,510 @@ +"""Test The generic (IP Camera) config flow.""" + +import errno +from unittest.mock import patch + +import av +import httpx +import pytest +import respx + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.generic.const import ( + CONF_CONTENT_TYPE, + CONF_FRAMERATE, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + CONF_RTSP_TRANSPORT, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DOMAIN, +) +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, +) + +from tests.common import MockConfigEntry + +TESTDATA = { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, +} + +TESTDATA_OPTIONS = { + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + **TESTDATA, +} + +TESTDATA_YAML = { + CONF_NAME: "Yaml Defined Name", + **TESTDATA, +} + + +@respx.mock +async def test_form(hass, fakeimg_png, mock_av_open, user_flow): + """Test the form with a normal set of settings.""" + + with mock_av_open as mock_setup: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["options"] == { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: "image/png", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + +@respx.mock +async def test_form_only_stillimage(hass, fakeimg_png, user_flow): + """Test we complete ok if the user wants still images only.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["options"] == { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: "image/png", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert respx.calls.call_count == 1 + + +@respx.mock +async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): + """Test we complete ok if the user enters a stream url.""" + with mock_av_open as mock_setup: + data = TESTDATA + data[CONF_RTSP_TRANSPORT] = "tcp" + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], data + ) + assert "errors" not in result2, f"errors={result2['errors']}" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_1" + assert result2["options"] == { + CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_RTSP_TRANSPORT: "tcp", + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: "image/png", + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + +async def test_form_only_stream(hass, mock_av_open): + """Test we complete ok if the user wants stream only.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + data = TESTDATA.copy() + data.pop(CONF_STILL_IMAGE_URL) + with mock_av_open as mock_setup: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127_0_0_1_testurl_2" + assert result2["options"] == { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_STREAM_SOURCE: "http://127.0.0.1/testurl/2", + CONF_RTSP_TRANSPORT: "tcp", + CONF_USERNAME: "fred_flintstone", + CONF_PASSWORD: "bambam", + CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, + CONF_CONTENT_TYPE: None, + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + +async def test_form_still_and_stream_not_provided(hass, user_flow): + """Test we show a suitable error if neither still or stream URL are provided.""" + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + { + CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, + CONF_FRAMERATE: 5, + CONF_VERIFY_SSL: False, + }, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_still_image_or_stream_url"} + + +@respx.mock +async def test_form_image_timeout(hass, mock_av_open, user_flow): + """Test we handle invalid image timeout.""" + respx.get("http://127.0.0.1/testurl/1").side_effect = [ + httpx.TimeoutException, + ] + + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "unable_still_load"} + + +@respx.mock +async def test_form_stream_invalidimage(hass, mock_av_open, user_flow): + """Test we handle invalid image when a stream is specified.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "invalid_still_image"} + + +@respx.mock +async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow): + """Test we handle invalid image when a stream is specified.""" + respx.get("http://127.0.0.1/testurl/1").respond(content=None) + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "unable_still_load"} + + +@respx.mock +async def test_form_stream_invalidimage3(hass, mock_av_open, user_flow): + """Test we handle invalid image when a stream is specified.""" + respx.get("http://127.0.0.1/testurl/1").respond(content=bytes([0xFF])) + with mock_av_open: + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["errors"] == {"still_image_url": "invalid_still_image"} + + +@respx.mock +async def test_form_stream_file_not_found(hass, fakeimg_png, user_flow): + """Test we handle file not found.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.FileNotFoundError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_file_not_found"} + + +@respx.mock +async def test_form_stream_http_not_found(hass, fakeimg_png, user_flow): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.HTTPNotFoundError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_http_not_found"} + + +@respx.mock +async def test_form_stream_timeout(hass, fakeimg_png, user_flow): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.TimeoutError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "timeout"} + + +@respx.mock +async def test_form_stream_unauthorised(hass, fakeimg_png, user_flow): + """Test we handle invalid auth.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=av.error.HTTPUnauthorizedError(0, 0), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_unauthorised"} + + +@respx.mock +async def test_form_stream_novideo(hass, fakeimg_png, user_flow): + """Test we handle invalid stream.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", side_effect=KeyError() + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_no_video"} + + +@respx.mock +async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow): + """Test we handle permission error.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=PermissionError(), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_not_permitted"} + + +@respx.mock +async def test_form_no_route_to_host(hass, fakeimg_png, user_flow): + """Test we handle no route to host.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError(errno.EHOSTUNREACH, "No route to host"), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_no_route_to_host"} + + +@respx.mock +async def test_form_stream_io_error(hass, fakeimg_png, user_flow): + """Test we handle no io error when setting up stream.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError(errno.EIO, "Input/output error"), + ): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + assert result2["type"] == "form" + assert result2["errors"] == {"stream_source": "stream_io_error"} + + +@respx.mock +async def test_form_oserror(hass, fakeimg_png, user_flow): + """Test we handle OS error when setting up stream.""" + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError("Some other OSError"), + ), pytest.raises(OSError): + await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + TESTDATA, + ) + + +@respx.mock +async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): + """Test the options flow with a template error.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_entry = MockConfigEntry( + title="Test Camera", + domain=DOMAIN, + data={}, + options=TESTDATA, + ) + + with mock_av_open: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + # try updating the still image url + data = TESTDATA.copy() + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=data, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "init" + + # verify that an invalid template reports the correct UI error. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/{{1/0}}" + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input=data, + ) + assert result4.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result4["errors"] == {"still_image_url": "template_error"} + + +# These below can be deleted after deprecation period is finished. +@respx.mock +async def test_import(hass, fakeimg_png, mock_av_open): + """Test configuration.yaml import used during migration.""" + with mock_av_open: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + # duplicate import should be aborted + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Yaml Defined Name" + await hass.async_block_till_done() + # Any name defined in yaml should end up as the entity id. + assert hass.states.get("camera.yaml_defined_name") + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +@respx.mock +async def test_import_invalid_still_image(hass, mock_av_open): + """Test configuration.yaml import used during migration.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") + with mock_av_open: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +@respx.mock +async def test_import_other_error(hass, fakeimgbytes_png): + """Test that non-specific import errors are raised.""" + respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) + with patch( + "homeassistant.components.generic.config_flow.av.open", + side_effect=OSError("other error"), + ), pytest.raises(OSError): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + + +# These above can be deleted after deprecation period is finished. + + +async def test_unload_entry(hass, fakeimg_png, mock_av_open): + """Test unloading the generic IP Camera entry.""" + mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is config_entries.ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_reload_on_title_change(hass) -> None: + """Test the integration gets reloaded when the title is updated.""" + + test_data = TESTDATA_OPTIONS + test_data[CONF_CONTENT_TYPE] = "image/png" + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="54321", options=test_data, title="My Title" + ) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert mock_entry.state is config_entries.ConfigEntryState.LOADED + assert hass.states.get("camera.my_title").attributes["friendly_name"] == "My Title" + + hass.config_entries.async_update_entry(mock_entry, title="New Title") + assert mock_entry.title == "New Title" + await hass.async_block_till_done() + + assert hass.states.get("camera.my_title").attributes["friendly_name"] == "New Title" From 38ef183433f4ba57589fbccf56d7352292ba2c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 28 Mar 2022 22:15:42 +0300 Subject: [PATCH 0768/1054] Change newly appeared DISABLED_USERs in tests to RegistryEntryDisablers (#68799) --- tests/components/homewizard/test_init.py | 4 ++-- tests/components/tomorrowio/test_init.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index 02e0b5c0c23..984a5431004 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -108,7 +108,7 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): config_entry=original_entry, original_name="Switch Disabled", suggested_object_id="socket_disabled", - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) # Update some user-customs ent_reg.async_update_entity(old_entity_active_power.entity_id, name="new_name") @@ -158,7 +158,7 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): assert new_entity_disabled_sensor.name is None assert new_entity_disabled_sensor.original_name == "Switch Disabled" assert new_entity_disabled_sensor.unique_id == "socket_disabled_unique_id" - assert new_entity_disabled_sensor.disabled_by == er.DISABLED_USER + assert new_entity_disabled_sensor.disabled_by == er.RegistryEntryDisabler.USER async def test_load_detect_api_disabled(aioclient_mock, hass): diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index f0c963d0dee..35dc7fd2d0a 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -84,7 +84,7 @@ async def test_climacell_migration_logic( original_name="ClimaCell - Hourly", suggested_object_id="climacell_hourly", device_id=old_device.id, - disabled_by=er.DISABLED_USER, + disabled_by=er.RegistryEntryDisabler.USER, ) old_entity_nowcast = ent_reg.async_get_or_create( "weather", @@ -140,7 +140,7 @@ async def test_climacell_migration_logic( assert new_entity_hourly.original_name == "ClimaCell - Hourly" assert new_entity_hourly.device_id != old_device.id assert new_entity_hourly.unique_id == f"{_get_unique_id(hass, new_data)}_hourly" - assert new_entity_hourly.disabled_by == er.DISABLED_USER + assert new_entity_hourly.disabled_by == er.RegistryEntryDisabler.USER new_entity_nowcast = ent_reg.async_get(old_entity_nowcast.entity_id) assert new_entity_nowcast.platform == DOMAIN From 9881538c279f9aa3031d395f1ce38b2e7d0ae3f3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 28 Mar 2022 13:22:49 -0700 Subject: [PATCH 0769/1054] Improve caldav tests to use APIs rather than entity methods (#68745) * Improve caldav tests to use APIs rather than entity methods Update caldav tests to use the best practice of exercising the public APIs rather than directly calling the entity methods directly. This is motivated by additional calendar API cleanup and possibly future breaking changes. * Remove unnecessary start/end arguments which are ignored --- tests/components/caldav/test_calendar.py | 46 +++++++++++++++++------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 7bfb8b620f6..cb23381d1c6 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,5 +1,6 @@ """The tests for the webdav calendar component.""" import datetime +from http import HTTPStatus from unittest.mock import MagicMock, Mock, patch from caldav.objects import Event @@ -274,6 +275,23 @@ def mock_private_cal(): yield _calendar +@pytest.fixture +def get_api_events(hass_client): + """Fixture to return events for a specific calendar using the API.""" + + async def api_call(entity_id): + client = await hass_client() + response = await client.get( + # The start/end times are arbitrary since they are ignored by `_mock_calendar` + # which just returns all events for the calendar. + f"/api/calendars/{entity_id}?start=2022-01-01&end=2022-01-01" + ) + assert response.status == HTTPStatus.OK + return await response.json() + + return api_call + + def _local_datetime(hours, minutes): """Build a datetime object for testing in the correct timezone.""" return dt.as_local(datetime.datetime(2017, 11, 27, hours, minutes, 0)) @@ -893,18 +911,17 @@ async def test_event_rrule_hourly_ended(mock_now, hass, calendar): assert state.state == STATE_OFF -async def test_get_events(hass, calendar): +async def test_get_events(hass, calendar, get_api_events): """Test that all events are returned on API.""" assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) await hass.async_block_till_done() - entity = hass.data["calendar"].get_entity("calendar.private") - events = await entity.async_get_events( - hass, datetime.date(2015, 11, 27), datetime.date(2015, 11, 28) - ) + + events = await get_api_events("calendar.private") assert len(events) == 14 + assert calendar.call -async def test_get_events_custom_calendars(hass, calendar): +async def test_get_events_custom_calendars(hass, calendar, get_api_events): """Test that only searched events are returned on API.""" config = dict(CALDAV_CONFIG) config["custom_calendars"] = [ @@ -914,9 +931,14 @@ async def test_get_events_custom_calendars(hass, calendar): assert await async_setup_component(hass, "calendar", {"calendar": config}) await hass.async_block_till_done() - entity = hass.data["calendar"].get_entity("calendar.private_private") - events = await entity.async_get_events( - hass, datetime.date(2015, 11, 27), datetime.date(2015, 11, 28) - ) - assert len(events) == 1 - assert events[0]["summary"] == "This is a normal event" + events = await get_api_events("calendar.private_private") + assert events == [ + { + "description": "Surprisingly rainy", + "end": "2017-11-27T10:00:00-08:00", + "location": "Hamburg", + "start": "2017-11-27T09:00:00-08:00", + "summary": "This is a normal event", + "uid": "1", + } + ] From f2aee38841e4dabe2d3027313b95afa8541f2e66 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 28 Mar 2022 23:56:29 +0200 Subject: [PATCH 0770/1054] Run KNX protocol logic in a separate thread (#68807) --- homeassistant/components/knx/__init__.py | 8 +++++++- tests/components/knx/test_init.py | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5c2d7e3b68c..d2e7f4c9d31 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -394,6 +394,7 @@ class KNXModule: connection_type=ConnectionType.ROUTING, local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), auto_reconnect=True, + threaded=True, ) if _conn_type == CONF_KNX_TUNNELING: return ConnectionConfig( @@ -403,6 +404,7 @@ class KNXModule: local_ip=self.config.get(ConnectionSchema.CONF_KNX_LOCAL_IP), route_back=self.config.get(ConnectionSchema.CONF_KNX_ROUTE_BACK, False), auto_reconnect=True, + threaded=True, ) if _conn_type == CONF_KNX_TUNNELING_TCP: return ConnectionConfig( @@ -410,8 +412,12 @@ class KNXModule: gateway_ip=self.config[CONF_HOST], gateway_port=self.config[CONF_PORT], auto_reconnect=True, + threaded=True, ) - return ConnectionConfig(auto_reconnect=True) + return ConnectionConfig( + auto_reconnect=True, + threaded=True, + ) async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: """Call invoked after a KNX connection state change was received.""" diff --git a/tests/components/knx/test_init.py b/tests/components/knx/test_init.py index 4380b132cbd..655cfdb81dc 100644 --- a/tests/components/knx/test_init.py +++ b/tests/components/knx/test_init.py @@ -28,7 +28,7 @@ from tests.common import MockConfigEntry CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS, CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, }, - ConnectionConfig(), + ConnectionConfig(threaded=True), ), ( { @@ -36,7 +36,9 @@ from tests.common import MockConfigEntry ConnectionSchema.CONF_KNX_LOCAL_IP: "192.168.1.1", }, ConnectionConfig( - connection_type=ConnectionType.ROUTING, local_ip="192.168.1.1" + connection_type=ConnectionType.ROUTING, + local_ip="192.168.1.1", + threaded=True, ), ), ( @@ -54,6 +56,7 @@ from tests.common import MockConfigEntry gateway_port=3675, local_ip="192.168.1.112", auto_reconnect=True, + threaded=True, ), ), ], From f0e2f964e842d25d5156b00f2a04a25bf9299aeb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 28 Mar 2022 23:58:06 +0200 Subject: [PATCH 0771/1054] Add zha typing [core.gateway] (3) (#68685) --- homeassistant/components/zha/core/gateway.py | 34 ++++++++++++-------- homeassistant/components/zha/entity.py | 23 +++++++------ 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 6c600bf93d6..64f7b24ff99 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -11,7 +11,7 @@ import logging import os import time import traceback -from typing import TYPE_CHECKING, Any, Union +from typing import TYPE_CHECKING, Any, NamedTuple, Union from serial import SerialException from zigpy.application import ControllerApplication @@ -31,6 +31,7 @@ from homeassistant.helpers.device_registry import ( async_get_registry as get_dev_reg, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import ( EntityRegistry, async_entries_for_device, @@ -96,16 +97,22 @@ if TYPE_CHECKING: from logging import Filter, LogRecord from ..entity import ZhaEntity + from .channels.base import ZigbeeChannel from .store import ZhaStorage _LogFilterType = Union[Filter, Callable[[LogRecord], int]] _LOGGER = logging.getLogger(__name__) -EntityReference = collections.namedtuple( - "EntityReference", - "reference_id zha_device cluster_channels device_info remove_future", -) + +class EntityReference(NamedTuple): + """Describes an entity reference.""" + + reference_id: str + zha_device: ZHADevice + cluster_channels: dict[str, ZigbeeChannel] + device_info: DeviceInfo + remove_future: asyncio.Future[Any] class DevicePairingStatus(Enum): @@ -362,7 +369,7 @@ class ZHAGateway: self, device: ZHADevice, entity_refs: list[EntityReference] | None ) -> None: if entity_refs is not None: - remove_tasks = [] + remove_tasks: list[asyncio.Future[Any]] = [] for entity_ref in entity_refs: remove_tasks.append(entity_ref.remove_future) if remove_tasks: @@ -413,13 +420,14 @@ class ZHAGateway: ): if entity_id == entity_reference.reference_id: return entity_reference + return None def remove_entity_reference(self, entity: ZhaEntity) -> None: """Remove entity reference for given entity_id if found.""" if entity.zha_device.ieee in self.device_registry: entity_refs = self.device_registry.get(entity.zha_device.ieee) self.device_registry[entity.zha_device.ieee] = [ - e for e in entity_refs if e.reference_id != entity.entity_id + e for e in entity_refs if e.reference_id != entity.entity_id # type: ignore[union-attr] ] def _cleanup_group_entity_registry_entries( @@ -470,12 +478,12 @@ class ZHAGateway: def register_entity_reference( self, - ieee, - reference_id, - zha_device, - cluster_channels, - device_info, - remove_future, + ieee: EUI64, + reference_id: str, + zha_device: ZHADevice, + cluster_channels: dict[str, ZigbeeChannel], + device_info: DeviceInfo, + remove_future: asyncio.Future[Any], ): """Record the creation of a hass entity associated with ieee.""" self._device_registry[ieee].append( diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 0b7f95efb64..4a9b0f7577c 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -2,10 +2,9 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable import functools import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback @@ -32,6 +31,10 @@ from .core.const import ( from .core.helpers import LogMixin from .core.typing import CALLABLE_T, ChannelType, ZhaDeviceType +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" @@ -43,7 +46,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): unique_id_suffix: str | None = None - def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs) -> None: + def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" self._name: str = "" self._force_update: bool = False @@ -53,9 +56,9 @@ class BaseZhaEntity(LogMixin, entity.Entity): self._unique_id += f"-{self.unique_id_suffix}" self._state: Any = None self._extra_state_attributes: dict[str, Any] = {} - self._zha_device: ZhaDeviceType = zha_device + self._zha_device = zha_device self._unsubs: list[CALLABLE_T] = [] - self.remove_future: Awaitable[None] = None + self.remove_future: asyncio.Future[Any] = asyncio.Future() @property def name(self) -> str: @@ -68,7 +71,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._unique_id @property - def zha_device(self) -> ZhaDeviceType: + def zha_device(self) -> ZHADevice: """Return the zha device this entity is attached to.""" return self._zha_device @@ -159,9 +162,9 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], - **kwargs, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs: Any, ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) @@ -170,7 +173,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" if self.unique_id_suffix: self._name += f" {self.unique_id_suffix}" - self.cluster_channels: dict[str, ChannelType] = {} + self.cluster_channels: dict[str, ZigbeeChannel] = {} for channel in channels: self.cluster_channels[channel.name] = channel From 38a7c7438ee5abf55773408b316d529f09924d08 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 28 Mar 2022 23:58:33 +0200 Subject: [PATCH 0772/1054] Add zha typing [core.device] (1) (#68686) --- homeassistant/components/zha/core/device.py | 57 ++++++++++---------- homeassistant/components/zha/core/helpers.py | 9 +++- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e8b38fb9699..91ac905707f 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from datetime import timedelta from enum import Enum import logging @@ -13,6 +14,7 @@ from zigpy import types import zigpy.exceptions from zigpy.profiles import PROFILES import zigpy.quirks +from zigpy.types.named import EUI64, NWK from zigpy.zcl.clusters.general import Groups import zigpy.zdo.types as zdo_types @@ -88,6 +90,8 @@ class DeviceStatus(Enum): class ZHADevice(LogMixin): """ZHA Zigbee device object.""" + _ha_device_id: str + def __init__( self, hass: HomeAssistant, @@ -101,7 +105,7 @@ class ZHADevice(LogMixin): self._available = False self._available_signal = f"{self.name}_{self.ieee}_{SIGNAL_AVAILABLE}" self._checkins_missed_count = 0 - self.unsubs = [] + self.unsubs: list[Callable[[], None]] = [] self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice) self.quirk_class = ( f"{self._zigpy_device.__class__.__module__}." @@ -129,16 +133,15 @@ class ZHADevice(LogMixin): self.hass, self._check_available, timedelta(seconds=keep_alive_interval) ) ) - self._ha_device_id = None - self.status = DeviceStatus.CREATED + self.status: DeviceStatus = DeviceStatus.CREATED self._channels = channels.Channels(self) @property - def device_id(self): + def device_id(self) -> str: """Return the HA device registry device id.""" return self._ha_device_id - def set_device_id(self, device_id): + def set_device_id(self, device_id: str) -> None: """Set the HA device registry device id.""" self._ha_device_id = device_id @@ -159,24 +162,24 @@ class ZHADevice(LogMixin): self._channels = value @property - def name(self): + def name(self) -> str: """Return device name.""" return f"{self.manufacturer} {self.model}" @property - def ieee(self): + def ieee(self) -> EUI64: """Return ieee address for device.""" return self._zigpy_device.ieee @property - def manufacturer(self): + def manufacturer(self) -> str: """Return manufacturer for device.""" if self._zigpy_device.manufacturer is None: return UNKNOWN_MANUFACTURER return self._zigpy_device.manufacturer @property - def model(self): + def model(self) -> str: """Return model for device.""" if self._zigpy_device.model is None: return UNKNOWN_MODEL @@ -191,7 +194,7 @@ class ZHADevice(LogMixin): return self._zigpy_device.node_desc.manufacturer_code @property - def nwk(self): + def nwk(self) -> NWK: """Return nwk for device.""" return self._zigpy_device.nwk @@ -206,7 +209,7 @@ class ZHADevice(LogMixin): return self._zigpy_device.rssi @property - def last_seen(self): + def last_seen(self) -> float | None: """Return last_seen for device.""" return self._zigpy_device.last_seen @@ -227,7 +230,7 @@ class ZHADevice(LogMixin): return self._zigpy_device.node_desc.logical_type.name @property - def power_source(self): + def power_source(self) -> str: """Return the power source for the device.""" return ( POWER_MAINS_POWERED if self.is_mains_powered else POWER_BATTERY_OR_UNKNOWN @@ -258,14 +261,14 @@ class ZHADevice(LogMixin): return self._zigpy_device.node_desc.is_end_device @property - def is_groupable(self): + def is_groupable(self) -> bool: """Return true if this device has a group cluster.""" return self.is_coordinator or ( - self.available and self.async_get_groupable_endpoints() + self.available and bool(self.async_get_groupable_endpoints()) ) @property - def skip_configuration(self): + def skip_configuration(self) -> bool: """Return true if the device should not issue configuration related commands.""" return self._zigpy_device.skip_configuration @@ -275,7 +278,7 @@ class ZHADevice(LogMixin): return self._zha_gateway @property - def device_automation_triggers(self): + def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]: """Return the device automation triggers for this device.""" triggers = { ("device_offline", "device_offline"): { @@ -289,7 +292,7 @@ class ZHADevice(LogMixin): return triggers @property - def available_signal(self): + def available_signal(self) -> str: """Signal to use to subscribe to device availability changes.""" return self._available_signal @@ -332,7 +335,7 @@ class ZHADevice(LogMixin): return zha_dev @callback - def async_update_sw_build_id(self, sw_version: int): + def async_update_sw_build_id(self, sw_version: int) -> None: """Update device sw version.""" if self.device_id is None: return @@ -340,7 +343,7 @@ class ZHADevice(LogMixin): self.device_id, sw_version=f"0x{sw_version:08x}" ) - async def _check_available(self, *_): + async def _check_available(self, *_: Any) -> None: # don't flip the availability state of the coordinator if self.is_coordinator: return @@ -400,7 +403,7 @@ class ZHADevice(LogMixin): async_dispatcher_send(self.hass, f"{self._available_signal}_entity") @property - def device_info(self): + def device_info(self) -> dict[str, Any]: """Return a device description for device.""" ieee = str(self.ieee) time_struct = time.localtime(self.last_seen) @@ -423,7 +426,7 @@ class ZHADevice(LogMixin): ATTR_SIGNATURE: self.zigbee_signature, } - async def async_configure(self): + async def async_configure(self) -> None: """Configure the device.""" should_identify = async_get_zha_config_value( self._zha_gateway.config_entry, @@ -446,7 +449,7 @@ class ZHADevice(LogMixin): EFFECT_OKAY, EFFECT_DEFAULT_VARIANT ) - async def async_initialize(self, from_cache=False): + async def async_initialize(self, from_cache: bool = False) -> None: """Initialize channels.""" self.debug("started initialization") await self._channels.async_initialize(from_cache) @@ -461,15 +464,15 @@ class ZHADevice(LogMixin): unsubscribe() @callback - def async_update_last_seen(self, last_seen): + def async_update_last_seen(self, last_seen: float | None) -> None: """Set last seen on the zigpy device.""" if self._zigpy_device.last_seen is None and last_seen is not None: self._zigpy_device.last_seen = last_seen @property - def zha_device_info(self): + def zha_device_info(self) -> dict[str, Any]: """Get ZHA device information.""" - device_info = {} + device_info: dict[str, Any] = {} device_info.update(self.device_info) device_info["entities"] = [ { @@ -496,7 +499,7 @@ class ZHADevice(LogMixin): ] # Return endpoint device type Names - names = [] + names: list[dict[str, str]] = [] for endpoint in (ep for epid, ep in self.device.endpoints.items() if epid): profile = PROFILES.get(endpoint.profile_id) if profile and endpoint.device_type is not None: @@ -768,7 +771,7 @@ class ZHADevice(LogMixin): fmt = f"{log_msg[1]} completed: %s" zdo.debug(fmt, *(log_msg[2] + (outcome,))) - def log(self, level, msg, *args): + def log(self, level: int, msg: str, *args: Any) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index d150ab9df45..5e98799e387 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -15,7 +15,7 @@ import itertools import logging from random import uniform import re -from typing import Any +from typing import Any, TypeVar import voluptuous as vol import zigpy.exceptions @@ -23,6 +23,7 @@ import zigpy.types import zigpy.util import zigpy.zdo.types as zdo_types +from homeassistant.config_entries import ConfigEntry from homeassistant.core import State, callback from .const import ( @@ -35,6 +36,8 @@ from .const import ( from .registries import BINDABLE_CLUSTERS from .typing import ZhaDeviceType, ZigpyClusterType +_T = TypeVar("_T") + @dataclass class BindingPair: @@ -130,7 +133,9 @@ def async_is_bindable_target(source_zha_device, target_zha_device): @callback -def async_get_zha_config_value(config_entry, section, config_key, default): +def async_get_zha_config_value( + config_entry: ConfigEntry, section: str, config_key: str, default: _T +) -> _T: """Get the value for the specified configuration from the zha config entry.""" return ( config_entry.options.get(CUSTOM_CONFIGURATION, {}) From f067aa6d71970db54a6bf356f00ec49c593ed5d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:07:29 +0200 Subject: [PATCH 0773/1054] Pin click to fix typer issue (#68808) Co-authored-by: epenet --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7673e102929..4c74f29fe1a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -101,3 +101,7 @@ multidict>=6.0.2 # Required for compatibility with point integration - ensure_active_token # https://github.com/home-assistant/core/pull/68176 authlib<1.0 + +# Required for compatibility with typer, used by pyunifiprotect integration +# https://github.com/tiangolo/typer/pull/375 +click<=8.0.4" diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c48a59f90d3..dd3ff865173 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -118,6 +118,10 @@ multidict>=6.0.2 # Required for compatibility with point integration - ensure_active_token # https://github.com/home-assistant/core/pull/68176 authlib<1.0 + +# Required for compatibility with typer, used by pyunifiprotect integration +# https://github.com/tiangolo/typer/pull/375 +click<=8.0.4" """ IGNORE_PRE_COMMIT_HOOK_ID = ( From d0e5e51863d7649c19bab67f87b0d147d030243c Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 28 Mar 2022 15:19:16 -0700 Subject: [PATCH 0774/1054] Add alarm control panel to Overkiz integration (#67164) --- .coveragerc | 1 + .../components/overkiz/alarm_control_panel.py | 305 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 5 + 3 files changed, 311 insertions(+) create mode 100644 homeassistant/components/overkiz/alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index 254515c3745..3579aad4b48 100644 --- a/.coveragerc +++ b/.coveragerc @@ -861,6 +861,7 @@ omit = homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/overkiz/__init__.py + homeassistant/components/overkiz/alarm_control_panel.py homeassistant/components/overkiz/binary_sensor.py homeassistant/components/overkiz/button.py homeassistant/components/overkiz/climate.py diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py new file mode 100644 index 00000000000..f6bdb6eef0e --- /dev/null +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -0,0 +1,305 @@ +"""Support for Overkiz alarm control panel.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState +from pyoverkiz.enums.ui import UIWidget +from pyoverkiz.types import StateType as OverkizStateType + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityDescription, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .coordinator import OverkizDataUpdateCoordinator +from .entity import OverkizDescriptiveEntity + + +@dataclass +class OverkizAlarmDescriptionMixin: + """Define an entity description mixin for switch entities.""" + + supported_features: int + fn_state: Callable[[Callable[[str], OverkizStateType]], str] + + +@dataclass +class OverkizAlarmDescription( + AlarmControlPanelEntityDescription, OverkizAlarmDescriptionMixin +): + """Class to describe an Overkiz alarm control panel.""" + + alarm_disarm: str | None = None + alarm_disarm_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_home: str | None = None + alarm_arm_home_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_night: str | None = None + alarm_arm_night_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_away: str | None = None + alarm_arm_away_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_trigger: str | None = None + alarm_trigger_args: OverkizStateType | list[OverkizStateType] | None = None + + +MAP_INTERNAL_STATUS_STATE: dict[str, str] = { + OverkizCommandParam.OFF: STATE_ALARM_DISARMED, + OverkizCommandParam.ZONE_1: STATE_ALARM_ARMED_HOME, + OverkizCommandParam.ZONE_2: STATE_ALARM_ARMED_NIGHT, + OverkizCommandParam.TOTAL: STATE_ALARM_ARMED_AWAY, +} + + +def _state_tsk_alarm_controller(select_state: Callable[[str], OverkizStateType]) -> str: + """Return the state of the device.""" + if ( + cast(str, select_state(OverkizState.INTERNAL_INTRUSION_DETECTED)) + == OverkizCommandParam.DETECTED + ): + return STATE_ALARM_TRIGGERED + + if cast(str, select_state(OverkizState.INTERNAL_CURRENT_ALARM_MODE)) != cast( + str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE) + ): + return STATE_ALARM_PENDING + + return MAP_INTERNAL_STATUS_STATE[ + cast(str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE)) + ] + + +def _state_stateful_alarm_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + if state := cast(list, select_state(OverkizState.CORE_ACTIVE_ZONES)): + if [ + OverkizCommandParam.A, + OverkizCommandParam.B, + OverkizCommandParam.C, + ] in state: + return STATE_ALARM_ARMED_AWAY + + if [OverkizCommandParam.A, OverkizCommandParam.B] in state: + return STATE_ALARM_ARMED_NIGHT + + if OverkizCommandParam.A in state: + return STATE_ALARM_ARMED_HOME + + return STATE_ALARM_DISARMED + + +MAP_MYFOX_STATUS_STATE: dict[str, str] = { + OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY, + OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED, + OverkizCommandParam.PARTIAL: STATE_ALARM_ARMED_NIGHT, +} + + +def _state_myfox_alarm_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + if ( + cast(str, select_state(OverkizState.CORE_INTRUSION)) + == OverkizCommandParam.DETECTED + ): + return STATE_ALARM_TRIGGERED + + return MAP_MYFOX_STATUS_STATE[ + cast(str, select_state(OverkizState.MYFOX_ALARM_STATUS)) + ] + + +MAP_ARM_TYPE: dict[str, str] = { + OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED, + OverkizCommandParam.ARMED_DAY: STATE_ALARM_ARMED_HOME, + OverkizCommandParam.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY, +} + + +def _state_alarm_panel_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + return MAP_ARM_TYPE[ + cast(str, select_state(OverkizState.VERISURE_ALARM_PANEL_MAIN_ARM_TYPE)) + ] + + +ALARM_DESCRIPTIONS: list[OverkizAlarmDescription] = [ + # TSKAlarmController + # Disabled by default since all Overkiz hubs have this + # virtual device, but only a few users actually use this. + OverkizAlarmDescription( + key=UIWidget.TSKALARM_CONTROLLER, + entity_registry_enabled_default=False, + supported_features=( + SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ), + fn_state=_state_tsk_alarm_controller, + alarm_disarm=OverkizCommand.ALARM_OFF, + alarm_arm_home=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_home_args=OverkizCommandParam.PARTIAL_1, + alarm_arm_night=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_night_args=OverkizCommandParam.PARTIAL_2, + alarm_arm_away=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_away_args=OverkizCommandParam.TOTAL, + alarm_trigger=OverkizCommand.ALARM_ON, + ), + # StatefulAlarmController + OverkizAlarmDescription( + key=UIWidget.STATEFUL_ALARM_CONTROLLER, + supported_features=( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ), + fn_state=_state_stateful_alarm_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_home=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_home_args=[OverkizCommandParam.A], + alarm_arm_night=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_night_args=[OverkizCommandParam.A, OverkizCommandParam.B], + alarm_arm_away=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_away_args=[ + OverkizCommandParam.A, + OverkizCommandParam.B, + OverkizCommandParam.C, + ], + ), + # MyFoxAlarmController + OverkizAlarmDescription( + key=UIWidget.MY_FOX_ALARM_CONTROLLER, + supported_features=SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT, + fn_state=_state_myfox_alarm_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_night=OverkizCommand.PARTIAL, + alarm_arm_away=OverkizCommand.ARM, + ), + # AlarmPanelController + OverkizAlarmDescription( + key=UIWidget.ALARM_PANEL_CONTROLLER, + supported_features=( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ), + fn_state=_state_alarm_panel_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_home=OverkizCommand.ARM_PARTIAL_DAY, + alarm_arm_night=OverkizCommand.ARM_PARTIAL_NIGHT, + alarm_arm_away=OverkizCommand.ARM, + ), +] + +SUPPORTED_DEVICES = {description.key: description for description in ALARM_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz alarm control panel from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizAlarmControlPanel] = [] + + for device in data.platforms[Platform.ALARM_CONTROL_PANEL]: + if description := SUPPORTED_DEVICES.get(device.widget) or SUPPORTED_DEVICES.get( + device.ui_class + ): + entities.append( + OverkizAlarmControlPanel( + device.device_url, + data.coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity): + """Representation of an Overkiz Alarm Control Panel.""" + + entity_description: OverkizAlarmDescription + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator, description) + + self._attr_supported_features = self.entity_description.supported_features + + @property + def state(self) -> str: + """Return the state of the device.""" + return self.entity_description.fn_state(self.executor.select_state) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + assert self.entity_description.alarm_disarm + await self.executor.async_execute_command( + self.entity_description.alarm_disarm, + self.entity_description.alarm_disarm_args, + ) + + async def async_alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + assert self.entity_description.alarm_arm_home + await self.executor.async_execute_command( + self.entity_description.alarm_arm_home, + self.entity_description.alarm_arm_home_args, + ) + + async def async_alarm_arm_night(self, code: str | None = None) -> None: + """Send arm night command.""" + assert self.entity_description.alarm_arm_night + await self.executor.async_execute_command( + self.entity_description.alarm_arm_night, + self.entity_description.alarm_arm_night_args, + ) + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + assert self.entity_description.alarm_arm_away + await self.executor.async_execute_command( + self.entity_description.alarm_arm_away, + self.entity_description.alarm_arm_away_args, + ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Send alarm trigger command.""" + assert self.entity_description.alarm_trigger + await self.executor.async_execute_command( + self.entity_description.alarm_trigger, + self.entity_description.alarm_trigger_args, + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 7f031ef3b6a..119f7a32262 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -19,6 +19,7 @@ UPDATE_INTERVAL: Final = timedelta(seconds=30) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, @@ -58,15 +59,19 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.SWINGING_SHUTTER: Platform.COVER, UIClass.VENETIAN_BLIND: Platform.COVER, UIClass.WINDOW: Platform.COVER, + UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) + UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.MY_FOX_SECURITY_CAMERA: Platform.SWITCH, # widgetName, uiClass is Camera (not supported) UIWidget.RTD_INDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTS_GENERIC: Platform.COVER, # widgetName, uiClass is Generic (not supported) UIWidget.SIREN_STATUS: None, # widgetName, uiClass is Siren (siren) UIWidget.STATELESS_ALARM_CONTROLLER: Platform.SWITCH, # widgetName, uiClass is Alarm (not supported) + UIWidget.STATEFUL_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported) + UIWidget.TSKALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) } # Map Overkiz camelCase to Home Assistant snake_case for translation From 8eb2e131e5e1d3e1f0e1eb68ce05df081e5ed8dc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:23:38 +0200 Subject: [PATCH 0775/1054] Use DmrDevice to communicate with SamsungTV (#68777) Co-authored-by: epenet --- .../components/samsungtv/media_player.py | 127 ++++++++++++------ tests/components/samsungtv/__init__.py | 24 ---- tests/components/samsungtv/conftest.py | 44 ++++++ .../components/samsungtv/test_media_player.py | 124 ++++++++++++----- 4 files changed, 222 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index c20674efdfd..33a7c44d45d 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -2,15 +2,22 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine +from collections.abc import Coroutine, Sequence import contextlib from datetime import datetime, timedelta from typing import Any -from async_upnp_client.aiohttp import AiohttpSessionRequester -from async_upnp_client.client import UpnpDevice, UpnpService +from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable from async_upnp_client.client_factory import UpnpFactory -from async_upnp_client.exceptions import UpnpActionResponseError, UpnpConnectionError +from async_upnp_client.exceptions import ( + UpnpActionResponseError, + UpnpConnectionError, + UpnpError, + UpnpResponseError, +) +from async_upnp_client.profiles.dlna import DmrDevice +from async_upnp_client.utils import async_get_local_ip import voluptuous as vol from wakeonlan import send_magic_packet @@ -35,7 +42,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_component from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -54,7 +61,6 @@ from .const import ( DEFAULT_NAME, DOMAIN, LOGGER, - UPNP_SVC_RENDERING_CONTROL, ) SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} @@ -114,7 +120,7 @@ class SamsungTVDevice(MediaPlayerEntity): self._config_entry = config_entry self._host: str | None = config_entry.data[CONF_HOST] self._mac: str | None = config_entry.data.get(CONF_MAC) - self._ssdp_rendering_control_location = config_entry.data.get( + self._ssdp_rendering_control_location: str | None = config_entry.data.get( CONF_SSDP_RENDERING_CONTROL_LOCATION ) self._on_script = on_script @@ -157,7 +163,8 @@ class SamsungTVDevice(MediaPlayerEntity): self._bridge.register_reauth_callback(self.access_denied) self._bridge.register_app_list_callback(self._app_list_callback) - self._upnp_device: UpnpDevice | None = None + self._dmr_device: DmrDevice | None = None + self._upnp_server: AiohttpNotifyServer | None = None def _update_sources(self) -> None: self._attr_source_list = list(SOURCES) @@ -185,6 +192,10 @@ class SamsungTVDevice(MediaPlayerEntity): ) ) + async def async_will_remove_from_hass(self) -> None: + """Handle removal.""" + await self._async_shutdown_dmr() + async def async_update(self) -> None: """Update state of device.""" if self._auth_failed or self.hass.is_stopping: @@ -204,24 +215,22 @@ class SamsungTVDevice(MediaPlayerEntity): if not self._app_list_event.is_set(): startup_tasks.append(self._async_startup_app_list()) - if not self._upnp_device and self._ssdp_rendering_control_location: - startup_tasks.append(self._async_startup_upnp()) + if not self._dmr_device and self._ssdp_rendering_control_location: + startup_tasks.append(self._async_startup_dmr()) if startup_tasks: await asyncio.gather(*startup_tasks) - if not (service := self._get_upnp_service()): + self._update_from_upnp() + + @callback + def _update_from_upnp(self) -> None: + if (dmr_device := self._dmr_device) is None: return - get_volume, get_mute = await asyncio.gather( - service.action("GetVolume").async_call(InstanceID=0, Channel="Master"), - service.action("GetMute").async_call(InstanceID=0, Channel="Master"), - ) - LOGGER.debug("Upnp GetVolume on %s: %s", self._host, get_volume) - if (volume_level := get_volume.get("CurrentVolume")) is not None: - self._attr_volume_level = volume_level / 100 - LOGGER.debug("Upnp GetMute on %s: %s", self._host, get_mute) - if (is_muted := get_mute.get("CurrentMute")) is not None: + if (volume_level := dmr_device.volume_level) is not None: + self._attr_volume_level = volume_level + if (is_muted := dmr_device.is_volume_muted) is not None: self._attr_is_volume_muted = is_muted async def _async_startup_app_list(self) -> None: @@ -239,33 +248,66 @@ class SamsungTVDevice(MediaPlayerEntity): "Failed to load app list from %s: %s", self._host, err.__repr__() ) - async def _async_startup_upnp(self) -> None: + async def _async_startup_dmr(self) -> None: assert self._ssdp_rendering_control_location is not None - if self._upnp_device is None: + if self._dmr_device is None: session = async_get_clientsession(self.hass) upnp_requester = AiohttpSessionRequester(session) upnp_factory = UpnpFactory(upnp_requester) + upnp_device: UpnpDevice | None = None with contextlib.suppress(UpnpConnectionError): - self._upnp_device = await upnp_factory.async_create_device( + upnp_device = await upnp_factory.async_create_device( self._ssdp_rendering_control_location ) - - def _get_upnp_service(self, log: bool = False) -> UpnpService | None: - if self._upnp_device is None: - if log: - LOGGER.info("Upnp services are not available on %s", self._host) - return None - - if service := self._upnp_device.services.get(UPNP_SVC_RENDERING_CONTROL): - return service - - if log: - LOGGER.info( - "Upnp service %s is not available on %s", - UPNP_SVC_RENDERING_CONTROL, - self._host, + if not upnp_device: + return + _, event_ip = await async_get_local_ip( + self._ssdp_rendering_control_location, self.hass.loop ) - return None + source = (event_ip or "0.0.0.0", 0) + self._upnp_server = AiohttpNotifyServer( + requester=upnp_requester, + source=source, + callback_url=None, + loop=self.hass.loop, + ) + await self._upnp_server.async_start_server() + self._dmr_device = DmrDevice(upnp_device, self._upnp_server.event_handler) + + try: + self._dmr_device.on_event = self._on_upnp_event + await self._dmr_device.async_subscribe_services(auto_resubscribe=True) + except UpnpResponseError as err: + # Device rejected subscription request. This is OK, variables + # will be polled instead. + LOGGER.debug("Device rejected subscription: %r", err) + except UpnpError as err: + # Don't leave the device half-constructed + self._dmr_device.on_event = None + self._dmr_device = None + await self._upnp_server.async_stop_server() + self._upnp_server = None + LOGGER.debug("Error while subscribing during device connect: %r", err) + raise + + async def _async_shutdown_dmr(self) -> None: + """Handle removal.""" + if (dmr_device := self._dmr_device) is not None: + self._dmr_device = None + dmr_device.on_event = None + await dmr_device.async_unsubscribe_services() + + if (upnp_server := self._upnp_server) is not None: + self._upnp_server = None + await upnp_server.async_stop_server() + + def _on_upnp_event( + self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] + ) -> None: + """State variable(s) changed, let home-assistant know.""" + self._update_from_upnp() + + self.async_write_ha_state() async def _async_launch_app(self, app_id: str) -> None: """Send launch_app to the tv.""" @@ -308,12 +350,11 @@ class SamsungTVDevice(MediaPlayerEntity): async def async_set_volume_level(self, volume: float) -> None: """Set volume level on the media player.""" - if not (service := self._get_upnp_service(log=True)): + if (dmr_device := self._dmr_device) is None: + LOGGER.info("Upnp services are not available on %s", self._host) return try: - await service.action("SetVolume").async_call( - InstanceID=0, Channel="Master", DesiredVolume=int(volume * 100) - ) + await dmr_device.async_set_volume_level(volume) except UpnpActionResponseError as err: LOGGER.warning( "Unable to set volume level on %s: %s", self._host, err.__repr__() diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 516aa5d8c95..53e47f6170b 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -2,9 +2,6 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import Mock - -from async_upnp_client.client import UpnpAction, UpnpService from homeassistant.components.samsungtv.const import DOMAIN, ENTRY_RELOAD_COOLDOWN from homeassistant.config_entries import ConfigEntry @@ -36,24 +33,3 @@ async def setup_samsungtv_entry(hass: HomeAssistant, data: ConfigType) -> Config await hass.async_block_till_done() return entry - - -def upnp_get_action_mock(device: Mock, service_type: str, action: str) -> Mock: - """Get or Add UpnpService/UpnpAction to UpnpDevice mock.""" - upnp_service: Mock | None - if (upnp_service := device.services.get(service_type)) is None: - upnp_service = Mock(UpnpService) - upnp_service.actions = {} - - def _get_action(action: str): - return upnp_service.actions.get(action) - - upnp_service.action.side_effect = _get_action - device.services[service_type] = upnp_service - - upnp_action: Mock | None - if (upnp_action := upnp_service.actions.get(action)) is None: - upnp_action = Mock(UpnpAction) - upnp_service.actions[action] = upnp_action - - return upnp_action diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index e4ef0666423..0f3bc53905a 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -3,10 +3,12 @@ from __future__ import annotations from collections.abc import Awaitable, Callable from datetime import datetime +from socket import AddressFamily from typing import Any from unittest.mock import AsyncMock, Mock, patch from async_upnp_client.client import UpnpDevice +from async_upnp_client.event_handler import UpnpEventHandler from async_upnp_client.exceptions import UpnpConnectionError import pytest from samsungctl import Remote @@ -39,6 +41,16 @@ def samsungtv_mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip.""" +@pytest.fixture(autouse=True) +def samsungtv_mock_async_get_local_ip(): + """Mock upnp util's async_get_local_ip.""" + with patch( + "homeassistant.components.samsungtv.media_player.async_get_local_ip", + return_value=(AddressFamily.AF_INET, "10.10.10.10"), + ): + yield + + @pytest.fixture(autouse=True) def fake_host_fixture() -> None: """Patch gethostbyname.""" @@ -78,6 +90,38 @@ async def upnp_device_fixture(upnp_factory: Mock) -> Mock: yield upnp_device +@pytest.fixture(name="dmr_device") +async def dmr_device_fixture(upnp_device: Mock) -> Mock: + """Patch async_upnp_client.""" + with patch( + "homeassistant.components.samsungtv.media_player.DmrDevice", + autospec=True, + ) as dmr_device_class: + dmr_device: Mock = dmr_device_class.return_value + dmr_device.volume_level = 0.44 + dmr_device.is_volume_muted = False + dmr_device.on_event = None + + def _raise_event(service, state_variables): + if dmr_device.on_event: + dmr_device.on_event(service, state_variables) + + dmr_device.raise_event = _raise_event + yield dmr_device + + +@pytest.fixture(name="upnp_notify_server") +async def upnp_notify_server_fixture(upnp_factory: Mock) -> Mock: + """Patch async_upnp_client.""" + with patch( + "homeassistant.components.samsungtv.media_player.AiohttpNotifyServer", + autospec=True, + ) as notify_server_class: + notify_server: Mock = notify_server_class.return_value + notify_server.event_handler = Mock(UpnpEventHandler) + yield notify_server + + @pytest.fixture(name="remote") def remote_fixture() -> Mock: """Patch the samsungctl Remote.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index f4bb40845a8..7f9f5936f11 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -4,7 +4,11 @@ from datetime import datetime, timedelta import logging from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch -from async_upnp_client.exceptions import UpnpActionResponseError +from async_upnp_client.exceptions import ( + UpnpActionResponseError, + UpnpError, + UpnpResponseError, +) import pytest from samsungctl import exceptions from samsungtvws.async_remote import SamsungTVWSAsyncRemote @@ -42,10 +46,7 @@ from homeassistant.components.samsungtv.const import ( METHOD_WEBSOCKET, TIMEOUT_WEBSOCKET, ) -from homeassistant.components.samsungtv.media_player import ( - SUPPORT_SAMSUNGTV, - UPNP_SVC_RENDERING_CONTROL, -) +from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -80,11 +81,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import ( - async_wait_config_entry_reload, - setup_samsungtv_entry, - upnp_get_action_mock, -) +from . import async_wait_config_entry_reload, setup_samsungtv_entry from .const import ( MOCK_ENTRYDATA_ENCRYPTED_WS, SAMPLE_DEVICE_INFO_FRAME, @@ -1329,39 +1326,30 @@ async def test_websocket_unsupported_remote_control( assert state.state == STATE_UNAVAILABLE -@pytest.mark.usefixtures("remotews", "rest_api") +@pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server") async def test_volume_control_upnp( - hass: HomeAssistant, upnp_device: Mock, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, dmr_device: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test for Upnp volume control.""" - upnp_get_volume = upnp_get_action_mock( - upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetVolume" - ) - upnp_get_volume.async_call.return_value = {"CurrentVolume": 44} - - upnp_get_mute = upnp_get_action_mock( - upnp_device, UPNP_SVC_RENDERING_CONTROL, "GetMute" - ) - upnp_get_mute.async_call.return_value = {"CurrentMute": False} - await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) - upnp_get_volume.async_call.assert_called_once() - upnp_get_mute.async_call.assert_called_once() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.44 + assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is False # Upnp action succeeds - upnp_set_volume = upnp_get_action_mock( - upnp_device, UPNP_SVC_RENDERING_CONTROL, "SetVolume" - ) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_SET, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, True, ) + dmr_device.async_set_volume_level.assert_called_once_with(0.5) assert "Unable to set volume level on" not in caplog.text # Upnp action failed - upnp_set_volume.async_call.side_effect = UpnpActionResponseError( + dmr_device.async_set_volume_level.reset_mock() + dmr_device.async_set_volume_level.side_effect = UpnpActionResponseError( status=500, error_code=501, error_desc="Action Failed" ) assert await hass.services.async_call( @@ -1370,6 +1358,7 @@ async def test_volume_control_upnp( {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, ) + dmr_device.async_set_volume_level.assert_called_once_with(0.6) assert "Unable to set volume level on" in caplog.text @@ -1390,7 +1379,7 @@ async def test_upnp_not_available( assert "Upnp services are not available" in caplog.text -@pytest.mark.usefixtures("remotews", "upnp_device", "rest_api") +@pytest.mark.usefixtures("remotews", "rest_api", "upnp_factory") async def test_upnp_missing_service( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: @@ -1404,4 +1393,79 @@ async def test_upnp_missing_service( {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.6}, True, ) - assert f"Upnp service {UPNP_SVC_RENDERING_CONTROL} is not available" in caplog.text + assert "Upnp services are not available" in caplog.text + + +@pytest.mark.usefixtures("remotews", "rest_api") +async def test_upnp_shutdown( + hass: HomeAssistant, + dmr_device: Mock, + upnp_notify_server: Mock, +) -> None: + """Ensure that Upnp cleanup takes effect.""" + entry = await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + assert await entry.async_unload(hass) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + dmr_device.async_unsubscribe_services.assert_called_once() + upnp_notify_server.async_stop_server.assert_called_once() + + +@pytest.mark.usefixtures("remotews", "rest_api", "upnp_notify_server") +async def test_upnp_subscribe_events(hass: HomeAssistant, dmr_device: Mock) -> None: + """Test for Upnp event feedback.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.44 + assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is False + + # DMR Devices gets updated, and raise event + dmr_device.volume_level = 0 + dmr_device.is_volume_muted = True + dmr_device.raise_event(None, None) + + # State gets updated without the need to wait for next update + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0 + assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is True + + +@pytest.mark.usefixtures("remotews", "rest_api") +async def test_upnp_subscribe_events_upnperror( + hass: HomeAssistant, + dmr_device: Mock, + upnp_notify_server: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test for failure to subscribe Upnp services.""" + with patch.object(dmr_device, "async_subscribe_services", side_effect=UpnpError): + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + upnp_notify_server.async_stop_server.assert_called_once() + assert "Error while subscribing during device connect" in caplog.text + + +@pytest.mark.usefixtures("remotews", "rest_api") +async def test_upnp_subscribe_events_upnpresponseerror( + hass: HomeAssistant, + dmr_device: Mock, + upnp_notify_server: Mock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test for failure to subscribe Upnp services.""" + with patch.object( + dmr_device, + "async_subscribe_services", + side_effect=UpnpResponseError(status=501), + ): + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + upnp_notify_server.async_stop_server.assert_not_called() + assert "Device rejected subscription" in caplog.text From 98ca9754d78856aa359179e5bf1f3c3122c05fa2 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 29 Mar 2022 01:01:17 +0200 Subject: [PATCH 0776/1054] Motion Blinds dhcp discovery (#68809) Co-authored-by: J. Nick Koston --- .../components/motion_blinds/config_flow.py | 21 +++++++++++-- .../components/motion_blinds/manifest.json | 6 ++++ .../components/motion_blinds/strings.json | 1 + .../motion_blinds/translations/en.json | 2 +- homeassistant/generated/dhcp.py | 2 ++ .../motion_blinds/test_config_flow.py | 31 +++++++++++++++++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index d93a1abd865..a956289d72e 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -5,9 +5,11 @@ from motionblinds import AsyncMotionMulticast, MotionDiscovery import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import network +from homeassistant.components import dhcp, network from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_INTERFACE, @@ -72,6 +74,21 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow.""" return OptionsFlowHandler(config_entry) + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac_address = format_mac(discovery_info.macaddress).replace(":", "") + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + short_mac = mac_address[-6:].upper() + self.context["title_placeholders"] = { + "short_mac": short_mac, + "ip_address": discovery_info.ip, + } + + self._host = discovery_info.ip + return await self.async_step_connect() + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} @@ -137,7 +154,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mac_address = motion_gateway.mac - await self.async_set_unique_id(mac_address) + await self.async_set_unique_id(mac_address, raise_on_progress=False) self._abort_if_unique_id_configured( updates={ CONF_HOST: self._host, diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index adfe7722ca4..c218c4cdd1f 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -5,6 +5,12 @@ "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "requirements": ["motionblinds==0.6.2"], "dependencies": ["network"], + "dhcp": [ + {"registered_devices": true}, + { + "hostname": "motion_*" + } + ], "codeowners": ["@starkillerOG"], "iot_class": "local_push", "loggers": ["motionblinds"] diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 9b64da7add4..c62c6dc2873 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{short_mac} ({ip_address})", "step": { "user": { "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 5e111278a8d..92931ee27ab 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -9,7 +9,7 @@ "discovery_error": "Failed to discover a Motion Gateway", "invalid_interface": "Invalid network interface" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 48760218f3c..3780b7914c4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -55,6 +55,8 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, + {'domain': 'motion_blinds', 'registered_devices': True}, + {'domain': 'motion_blinds', 'hostname': 'motion_*'}, {'domain': 'myq', 'macaddress': '645299*'}, {'domain': 'nest', 'macaddress': '18B430*'}, {'domain': 'nest', 'macaddress': '641666*'}, diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 77f5e242974..fce86f5f343 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.motion_blinds import const from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME from homeassistant.const import CONF_API_KEY, CONF_HOST @@ -337,6 +338,36 @@ async def test_config_flow_invalid_interface(hass): assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"} +async def test_dhcp_flow(hass): + """Successful flow from DHCP discovery.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="MOTION_abcdef", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, + } + + async def test_options_flow(hass): """Test specifying non default settings using options flow.""" config_entry = MockConfigEntry( From fefd6a1d1a0b461af1120fb0c92980c7b685328d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 29 Mar 2022 01:05:47 +0200 Subject: [PATCH 0777/1054] Update aioairzone to v0.2.1 (#68798) Co-authored-by: J. Nick Koston --- homeassistant/components/airzone/__init__.py | 24 +++++++++++++------ homeassistant/components/airzone/climate.py | 9 +++---- .../components/airzone/config_flow.py | 2 +- .../components/airzone/coordinator.py | 2 +- .../components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 0555b888fd8..6952ac2a42d 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -4,8 +4,15 @@ from __future__ import annotations from typing import Any from aioairzone.common import ConnectionOptions -from aioairzone.const import AZD_ID, AZD_NAME, AZD_SYSTEM, AZD_ZONES -from aioairzone.localapi_device import AirzoneLocalApi +from aioairzone.const import ( + AZD_ID, + AZD_NAME, + AZD_SYSTEM, + AZD_THERMOSTAT_FW, + AZD_THERMOSTAT_MODEL, + AZD_ZONES, +) +from aioairzone.localapi import AirzoneLocalApi from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform @@ -33,15 +40,18 @@ class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): """Initialize.""" super().__init__(coordinator) - self._attr_device_info: DeviceInfo = { - "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, - "manufacturer": MANUFACTURER, - "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", - } self.system_id = zone_data[AZD_SYSTEM] self.system_zone_id = system_zone_id self.zone_id = zone_data[AZD_ID] + self._attr_device_info: DeviceInfo = { + "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, + "manufacturer": MANUFACTURER, + "model": self.get_zone_value(AZD_THERMOSTAT_MODEL), + "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", + "sw_version": self.get_zone_value(AZD_THERMOSTAT_FW), + } + def get_zone_value(self, key): """Return zone value by key.""" value = None diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 763603a758b..aac53ec35db 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -13,6 +13,7 @@ from aioairzone.const import ( API_ZONE_ID, AZD_DEMAND, AZD_HUMIDITY, + AZD_MASTER, AZD_MODE, AZD_MODES, AZD_NAME, @@ -119,12 +120,8 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): self.get_zone_value(AZD_TEMP_UNIT) ] self._attr_hvac_modes = [ - HVAC_MODE_LIB_TO_HASS[mode] - for mode in self.get_zone_value(AZD_MODES) - or [self.get_zone_value(AZD_MODE)] + HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_zone_value(AZD_MODES) ] - if HVAC_MODE_OFF not in self._attr_hvac_modes: - self._attr_hvac_modes.append(HVAC_MODE_OFF) self._async_update_attrs() async def _async_update_hvac_params(self, params) -> None: @@ -147,7 +144,7 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity): if hvac_mode == HVAC_MODE_OFF: params[API_ON] = 0 else: - if self.get_zone_value(AZD_MODES): + if self.get_zone_value(AZD_MASTER): mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] if mode != self.get_zone_value(AZD_MODE): params[API_MODE] = mode diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index 47281deab73..57dde088820 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -5,7 +5,7 @@ from typing import Any from aioairzone.common import ConnectionOptions from aioairzone.exceptions import InvalidHost -from aioairzone.localapi_device import AirzoneLocalApi +from aioairzone.localapi import AirzoneLocalApi from aiohttp.client_exceptions import ClientConnectorError import voluptuous as vol diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index 40d87a16b27..f81b75b42bf 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from aioairzone.localapi_device import AirzoneLocalApi +from aioairzone.localapi import AirzoneLocalApi from aiohttp.client_exceptions import ClientConnectorError import async_timeout diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 49167699e42..019c3347027 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.1.0"], + "requirements": ["aioairzone==0.2.1"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 36432c9c5ea..ce78324ce8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.airzone -aioairzone==0.1.0 +aioairzone==0.2.1 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d984a4a06c..d43357bf29d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.5 # homeassistant.components.airzone -aioairzone==0.1.0 +aioairzone==0.2.1 # homeassistant.components.ambient_station aioambient==2021.11.0 From 9b70c10c8e36234b5e862dcf44cf97033c758b95 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 29 Mar 2022 01:13:02 +0200 Subject: [PATCH 0778/1054] Implement coordinator for trafikverket_weather (#65233) --- .coveragerc | 1 + .../trafikverket_weatherstation/__init__.py | 21 ++-- .../trafikverket_weatherstation/const.py | 2 - .../coordinator.py | 43 ++++++++ .../trafikverket_weatherstation/sensor.py | 100 ++++++++---------- 5 files changed, 98 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/trafikverket_weatherstation/coordinator.py diff --git a/.coveragerc b/.coveragerc index 3579aad4b48..a666e001c6b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1273,6 +1273,7 @@ omit = homeassistant/components/tradfri/switch.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/__init__.py + homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index 854b5d54d70..eab54f8f45b 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -1,22 +1,21 @@ """The trafikverket_weatherstation component.""" from __future__ import annotations -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import PLATFORMS - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, PLATFORMS +from .coordinator import TVDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trafikverket Weatherstation from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + coordinator = TVDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - _LOGGER.debug("Loaded entry for %s", entry.title) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -24,10 +23,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Trafikverket Weatherstation config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: - _LOGGER.debug("Unloaded entry for %s", entry.title) - return unload_ok - - return False + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/trafikverket_weatherstation/const.py b/homeassistant/components/trafikverket_weatherstation/const.py index 7bb53dc5356..0d4680e9b37 100644 --- a/homeassistant/components/trafikverket_weatherstation/const.py +++ b/homeassistant/components/trafikverket_weatherstation/const.py @@ -5,8 +5,6 @@ DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" PLATFORMS = [Platform.SENSOR] ATTRIBUTION = "Data provided by Trafikverket" -ATTR_MEASURE_TIME = "measure_time" -ATTR_ACTIVE = "active" NONE_IS_ZERO_SENSORS = { "air_temp", diff --git a/homeassistant/components/trafikverket_weatherstation/coordinator.py b/homeassistant/components/trafikverket_weatherstation/coordinator.py new file mode 100644 index 00000000000..990dcc0bc0b --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/coordinator.py @@ -0,0 +1,43 @@ +"""DataUpdateCoordinator for the Trafikverket Weather integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_STATION, DOMAIN + +_LOGGER = logging.getLogger(__name__) +TIME_BETWEEN_UPDATES = timedelta(minutes=10) + + +class TVDataUpdateCoordinator(DataUpdateCoordinator[WeatherStationInfo]): + """A Sensibo Data Update Coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Sensibo coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=TIME_BETWEEN_UPDATES, + ) + self._weather_api = TrafikverketWeather( + async_get_clientsession(hass), entry.data[CONF_API_KEY] + ) + self._station = entry.data[CONF_STATION] + + async def _async_update_data(self) -> WeatherStationInfo: + """Fetch data from Trafikverket.""" + try: + weatherdata = await self._weather_api.async_get_weather(self._station) + except ValueError as error: + raise UpdateFailed from error + return weatherdata diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 0a5f069824c..e659e42f82b 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -1,13 +1,8 @@ """Weather information for air and road temperature (by Trafikverket).""" from __future__ import annotations -import asyncio from dataclasses import dataclass -from datetime import timedelta -import logging - -import aiohttp -from pytrafikverket.trafikverket_weather import TrafikverketWeather, WeatherStationInfo +from datetime import datetime from homeassistant.components.sensor import ( SensorDeviceClass, @@ -17,7 +12,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_API_KEY, DEGREE, LENGTH_MILLIMETERS, PERCENTAGE, @@ -25,26 +19,17 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import as_utc, get_time_zone -from .const import ( - ATTR_ACTIVE, - ATTR_MEASURE_TIME, - ATTRIBUTION, - CONF_STATION, - DOMAIN, - NONE_IS_ZERO_SENSORS, -) +from .const import ATTRIBUTION, CONF_STATION, DOMAIN, NONE_IS_ZERO_SENSORS +from .coordinator import TVDataUpdateCoordinator -_LOGGER = logging.getLogger(__name__) - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) - -SCAN_INTERVAL = timedelta(seconds=300) +STOCKHOLM_TIMEZONE = get_time_zone("Europe/Stockholm") @dataclass @@ -143,6 +128,14 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( icon="mdi:weather-pouring", entity_registry_enabled_default=False, ), + TrafikverketSensorEntityDescription( + key="measure_time", + api_key="measure_time", + name="Measure Time", + icon="mdi:clock", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + ), ) @@ -151,20 +144,25 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - web_session = async_get_clientsession(hass) - weather_api = TrafikverketWeather(web_session, entry.data[CONF_API_KEY]) + coordinator: TVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [ + async_add_entities( TrafikverketWeatherStation( - weather_api, entry.entry_id, entry.data[CONF_STATION], description + coordinator, entry.entry_id, entry.data[CONF_STATION], description ) for description in SENSOR_TYPES - ] - - async_add_entities(entities, True) + ) -class TrafikverketWeatherStation(SensorEntity): +def _to_datetime(measuretime: str) -> datetime: + """Return isoformatted utc time.""" + time_obj = datetime.strptime(measuretime, "%Y-%m-%dT%H:%M:%S") + return as_utc(time_obj.replace(tzinfo=STOCKHOLM_TIMEZONE)) + + +class TrafikverketWeatherStation( + CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity +): """Representation of a Trafikverket sensor.""" entity_description: TrafikverketSensorEntityDescription @@ -172,17 +170,16 @@ class TrafikverketWeatherStation(SensorEntity): def __init__( self, - weather_api: TrafikverketWeather, + coordinator: TVDataUpdateCoordinator, entry_id: str, sensor_station: str, description: TrafikverketSensorEntityDescription, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.entity_description = description self._attr_name = f"{sensor_station} {description.name}" self._attr_unique_id = f"{entry_id}_{description.key}" - self._station = sensor_station - self._weather_api = weather_api self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, @@ -191,26 +188,23 @@ class TrafikverketWeatherStation(SensorEntity): name=sensor_station, configuration_url="https://api.trafikinfo.trafikverket.se/", ) - self._weather: WeatherStationInfo | None = None - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Get the latest data from Trafikverket and updates the states.""" - try: - self._weather = await self._weather_api.async_get_weather(self._station) - except (asyncio.TimeoutError, aiohttp.ClientError, ValueError) as error: - _LOGGER.error("Could not fetch weather data: %s", error) - return - self._attr_native_value = getattr( - self._weather, self.entity_description.api_key + @property + def native_value(self) -> StateType | datetime: + """Return state of sensor.""" + if self.entity_description.api_key == "measure_time": + return _to_datetime(self.coordinator.data.measure_time) + + state: StateType = getattr( + self.coordinator.data, self.entity_description.api_key ) - if ( - self._attr_native_value is None - and self.entity_description.key in NONE_IS_ZERO_SENSORS - ): - self._attr_native_value = 0 - self._attr_extra_state_attributes = { - ATTR_ACTIVE: self._weather.active, - ATTR_MEASURE_TIME: self._weather.measure_time, - } + # For zero value state the api reports back None for certain sensors. + if state is None and self.entity_description.key in NONE_IS_ZERO_SENSORS: + return 0 + return state + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.data.active and super().available From 963d161f7260dcf5495bf8e102b91bc3c20b0070 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Mar 2022 02:17:16 +0200 Subject: [PATCH 0779/1054] Update core services files with new selectors (#68810) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/climate/services.yaml | 42 ++++++++++++------- homeassistant/components/fan/services.yaml | 6 ++- .../components/frontend/services.yaml | 12 +++--- homeassistant/components/group/services.yaml | 2 +- homeassistant/components/light/services.yaml | 41 +++++++++--------- homeassistant/components/logger/services.yaml | 18 +++++--- .../components/media_player/services.yaml | 9 ++-- .../components/system_log/services.yaml | 15 ++++--- 8 files changed, 88 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 7b9d7fe4a72..40d518456b4 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -71,13 +71,20 @@ set_temperature: selector: select: options: - - "off" - - "auto" - - "cool" - - "dry" - - "fan_only" - - "heat_cool" - - "heat" + - label: "Off" + value: "off" + - label: "Auto" + value: "auto" + - label: "Cool" + value: "cool" + - label: "Dry" + value: "dry" + - label: "Fan Only" + value: "fan_only" + - label: "Heat/Cool" + value: "heat_cool" + - label: "Heat" + value: "heat" set_humidity: name: Set target humidity @@ -124,13 +131,20 @@ set_hvac_mode: selector: select: options: - - "off" - - "auto" - - "cool" - - "dry" - - "fan_only" - - "heat_cool" - - "heat" + - label: "Off" + value: "off" + - label: "Auto" + value: "auto" + - label: "Cool" + value: "cool" + - label: "Dry" + value: "dry" + - label: "Fan Only" + value: "fan_only" + - label: "Heat/Cool" + value: "heat_cool" + - label: "Heat" + value: "heat" set_swing_mode: name: Set swing mode diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index ee39229699d..cfc44029e23 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -116,8 +116,10 @@ set_direction: selector: select: options: - - "forward" - - "reverse" + - label: "Forward" + value: "forward" + - label: "Reverse" + value: "reverse" increase_speed: name: Increase speed diff --git a/homeassistant/components/frontend/services.yaml b/homeassistant/components/frontend/services.yaml index 478202a4a0a..2a562ab348a 100644 --- a/homeassistant/components/frontend/services.yaml +++ b/homeassistant/components/frontend/services.yaml @@ -5,12 +5,12 @@ set_theme: description: Set a theme unless the client selected per-device theme. fields: name: - name: Name - description: Name of a predefined theme, 'default' or 'none'. + name: Theme + description: Name of a predefined theme required: true example: "default" selector: - text: + theme: mode: name: Mode description: The mode the theme is for. @@ -18,8 +18,10 @@ set_theme: selector: select: options: - - "dark" - - "light" + - label: "Dark" + value: "dark" + - label: "Light" + value: "light" reload_themes: name: Reload themes diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index 3e7f1eb203d..cba11b1723d 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -25,7 +25,7 @@ set: description: Name of icon for the group. example: "mdi:camera" selector: - text: + icon: entities: name: Entities description: List of all members in the group. Not compatible with 'delta'. diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index f3fe306eb90..572fe9e28fb 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -18,12 +18,10 @@ turn_on: max: 300 unit_of_measurement: seconds rgb_color: - name: RGB-color - description: A list containing three integers between 0 and 255 representing the RGB (red, green, blue) color for the light. - advanced: true - example: "[255, 100, 100]" + name: Color + description: The color for the light (based on RGB - red, green, blue). selector: - object: + color_rgb: rgbw_color: name: RGBW-color description: A list containing four integers between 0 and 255 representing the RGBW (red, green, blue, white) color for the light. @@ -209,14 +207,12 @@ turn_on: selector: object: color_temp: - name: Color temperature (mireds) + name: Color temperature description: Color temperature for the light in mireds. - advanced: true selector: - number: - min: 153 - max: 500 - unit_of_measurement: mireds + color_temp: + min_mireds: 153 + max_mireds: 500 kelvin: name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. @@ -290,8 +286,10 @@ turn_on: selector: select: options: - - long - - short + - label: "Long" + value: "long" + - label: "Short" + value: "short" effect: name: Effect description: Light effect. @@ -320,8 +318,10 @@ turn_off: selector: select: options: - - long - - short + - label: "Long" + value: "long" + - label: "Short" + value: "short" toggle: name: Toggle @@ -522,10 +522,7 @@ toggle: description: Color temperature for the light in mireds. advanced: true selector: - number: - min: 153 - max: 500 - unit_of_measurement: mireds + color_temp: kelvin: name: Color temperature (Kelvin) description: Color temperature for the light in Kelvin. @@ -579,8 +576,10 @@ toggle: selector: select: options: - - long - - short + - label: "Long" + value: "long" + - label: "Short" + value: "short" effect: name: Effect description: Light effect. diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 5930a4e5d9e..c20d1171bb2 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -8,12 +8,18 @@ set_default_level: selector: select: options: - - debug - - info - - warning - - error - - fatal - - critical + - label: "Debug" + value: "debug" + - label: "Info" + value: "info" + - label: "Warning" + value: "warning" + - label: "Error" + value: "error" + - label: "Fatal" + value: "fatal" + - label: "Critical" + value: "critical" set_level: name: Set level diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index f897ae8ad7f..2e8585d0127 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -215,9 +215,12 @@ repeat_set: selector: select: options: - - "off" - - "all" - - "one" + - label: "Off" + value: "off" + - label: "Repeat all" + value: "all" + - label: "Repeat one" + value: "one" join: name: Join diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index b6444bcecc5..d473a590e0f 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -20,11 +20,16 @@ write: selector: select: options: - - "debug" - - "info" - - "warning" - - "error" - - "critical" + - label: "Debug" + value: "debug" + - label: "Info" + value: "info" + - label: "Warning" + value: "warning" + - label: "Error" + value: "error" + - label: "Critical" + value: "critical" logger: name: Logger description: From 349060be2f1b1cfdd3e3488431049e8d8abfb893 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Mar 2022 14:24:54 -1000 Subject: [PATCH 0780/1054] Disable Unifi Protect Oldest Recording sensor by default (#68804) --- homeassistant/components/unifiprotect/sensor.py | 1 + tests/components/unifiprotect/test_sensor.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index b90688efc70..00ac1304d88 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -154,6 +154,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Oldest Recording", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, ufp_value="stats.video.recording_start", ), ProtectSensorEntityDescription( diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 5b14ef3f2fb..971ee47405b 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -403,8 +403,8 @@ async def test_sensor_setup_camera( hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime ): """Test sensor entity setup for camera devices.""" - # 3 from all, 8 from camera, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 24, 14) + # 3 from all, 7 from camera, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 24, 13) entity_registry = er.async_get(hass) @@ -528,8 +528,8 @@ async def test_sensor_update_motion( hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime ): """Test sensor motion entity.""" - # 3 from all, 8 from camera, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 24, 14) + # 3 from all, 7 from camera, 12 NVR + assert_entity_counts(hass, Platform.SENSOR, 24, 13) _, entity_id = ids_from_device_description( Platform.SENSOR, camera, MOTION_SENSORS[0] From c6ba987995e4c726614ffbde2b31ce01a034aab3 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 28 Mar 2022 17:47:18 -0700 Subject: [PATCH 0781/1054] Use device properties for WeMo Insight sensors (#63525) --- homeassistant/components/wemo/sensor.py | 117 ++++++++++++------------ tests/components/wemo/conftest.py | 12 +++ tests/components/wemo/test_sensor.py | 29 ++---- 3 files changed, 79 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/wemo/sensor.py b/homeassistant/components/wemo/sensor.py index eed5c510936..b79ce08a1e0 100644 --- a/homeassistant/components/wemo/sensor.py +++ b/homeassistant/components/wemo/sensor.py @@ -1,5 +1,10 @@ """Support for power sensors in WeMo Insight devices.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,13 +18,42 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity from .wemo_device import DeviceCoordinator +@dataclass +class AttributeSensorDescription(SensorEntityDescription): + """SensorEntityDescription for WeMo AttributeSensor entities.""" + + state_conversion: Callable[[StateType], StateType] | None = None + unique_id_suffix: str | None = None + + +ATTRIBUTE_SENSORS = ( + AttributeSensorDescription( + name="Current Power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + key="current_power_watts", + unique_id_suffix="currentpower", + state_conversion=lambda state: round(cast(float, state), 2), + ), + AttributeSensorDescription( + name="Today Energy", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + key="today_kwh", + unique_id_suffix="todaymw", + state_conversion=lambda state: round(cast(float, state), 2), + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -30,7 +64,9 @@ async def async_setup_entry( async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" async_add_entities( - [InsightCurrentPower(coordinator), InsightTodayEnergy(coordinator)] + AttributeSensor(coordinator, description) + for description in ATTRIBUTE_SENSORS + if hasattr(coordinator.wemo, description.key) ) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.sensor", _discovered_wemo) @@ -43,66 +79,35 @@ async def async_setup_entry( ) -class InsightSensor(WemoEntity, SensorEntity): - """Common base for WeMo Insight power sensors.""" +class AttributeSensor(WemoEntity, SensorEntity): + """Sensor that reads attributes of a wemo device.""" + + entity_description: AttributeSensorDescription + + def __init__( + self, coordinator: DeviceCoordinator, description: AttributeSensorDescription + ) -> None: + """Init AttributeSensor.""" + super().__init__(coordinator) + self.entity_description = description @property - def name_suffix(self) -> str: - """Return the name of the entity if any.""" - assert self.entity_description.name + def name_suffix(self) -> str | None: + """Return the name of the entity.""" return self.entity_description.name @property - def unique_id_suffix(self) -> str: - """Return the id of this entity.""" - return self.entity_description.key + def unique_id_suffix(self) -> str | None: + """Suffix to append to the WeMo device's unique ID.""" + return self.entity_description.unique_id_suffix - @property - def available(self) -> bool: - """Return true if sensor is available.""" - return ( - self.entity_description.key in self.wemo.insight_params - and super().available - ) - - -class InsightCurrentPower(InsightSensor): - """Current instantaineous power consumption.""" - - entity_description = SensorEntityDescription( - key="currentpower", - name="Current Power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ) + def convert_state(self, value: StateType) -> StateType: + """Convert native state to a value appropriate for the sensor.""" + if (convert := self.entity_description.state_conversion) is None: + return None + return convert(value) @property def native_value(self) -> StateType: - """Return the current power consumption.""" - milliwatts = convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - assert isinstance(milliwatts, float) - return milliwatts / 1000.0 - - -class InsightTodayEnergy(InsightSensor): - """Energy used today.""" - - entity_description = SensorEntityDescription( - key="todaymw", - name="Today Energy", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ) - - @property - def native_value(self) -> StateType: - """Return the current energy use today.""" - milliwatt_seconds = convert( - self.wemo.insight_params.get(self.entity_description.key), float, 0.0 - ) - assert isinstance(milliwatt_seconds, float) - return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) + """Return the value of the device attribute.""" + return self.convert_state(getattr(self.wemo, self.entity_description.key)) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index cf974f523a8..af7002a2500 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -15,6 +15,9 @@ MOCK_PORT = 50000 MOCK_NAME = "WemoDeviceName" MOCK_SERIAL_NUMBER = "WemoSerialNumber" MOCK_FIRMWARE_VERSION = "WeMo_WW_2.00.XXXXX.PVT-OWRT" +MOCK_INSIGHT_CURRENT_WATTS = 0.01 +MOCK_INSIGHT_TODAY_KWH = 3.33 +MOCK_INSIGHT_STATE_THRESHOLD_POWER = 8.0 @pytest.fixture(name="pywemo_model") @@ -64,6 +67,15 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): device.get_state.return_value = 0 # Default to Off device.supports_long_press.return_value = cls.supports_long_press() + if issubclass(cls, pywemo.Insight): + device.get_standby_state = pywemo.StandbyState.OFF + device.current_power_watts = MOCK_INSIGHT_CURRENT_WATTS + device.today_kwh = MOCK_INSIGHT_TODAY_KWH + device.threshold_power_watts = MOCK_INSIGHT_STATE_THRESHOLD_POWER + device.on_for = 1234 + device.today_on_time = 5678 + device.total_on_time = 9012 + url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" with patch("pywemo.setup_url_for_address", return_value=url), patch( "pywemo.discovery.device_from_description", return_value=device diff --git a/tests/components/wemo/test_sensor.py b/tests/components/wemo/test_sensor.py index 305aad6102c..ab6753975f1 100644 --- a/tests/components/wemo/test_sensor.py +++ b/tests/components/wemo/test_sensor.py @@ -2,13 +2,7 @@ import pytest -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE -from homeassistant.setup import async_setup_component - +from .conftest import MOCK_INSIGHT_CURRENT_WATTS, MOCK_INSIGHT_TODAY_KWH from .entity_test_helpers import EntityTestHelpers @@ -38,7 +32,6 @@ class InsightTestTemplate(EntityTestHelpers): ENTITY_ID_SUFFIX: str EXPECTED_STATE_VALUE: str - INSIGHT_PARAM_NAME: str @pytest.fixture(name="wemo_entity_suffix") @classmethod @@ -46,30 +39,20 @@ class InsightTestTemplate(EntityTestHelpers): """Select the appropriate entity for the test.""" return cls.ENTITY_ID_SUFFIX - async def test_state_unavailable(self, hass, wemo_entity, pywemo_device): - """Test that there is no failure if the insight_params is not populated.""" - del pywemo_device.insight_params[self.INSIGHT_PARAM_NAME] - await async_setup_component(hass, HA_DOMAIN, {}) - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, - blocking=True, - ) - assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + def test_state(self, hass, wemo_entity): + """Test the sensor state.""" + assert hass.states.get(wemo_entity.entity_id).state == self.EXPECTED_STATE_VALUE class TestInsightCurrentPower(InsightTestTemplate): """Test the InsightCurrentPower class.""" ENTITY_ID_SUFFIX = "_current_power" - EXPECTED_STATE_VALUE = "0.001" - INSIGHT_PARAM_NAME = "currentpower" + EXPECTED_STATE_VALUE = str(MOCK_INSIGHT_CURRENT_WATTS) class TestInsightTodayEnergy(InsightTestTemplate): """Test the InsightTodayEnergy class.""" ENTITY_ID_SUFFIX = "_today_energy" - EXPECTED_STATE_VALUE = "3.33" - INSIGHT_PARAM_NAME = "todaymw" + EXPECTED_STATE_VALUE = str(MOCK_INSIGHT_TODAY_KWH) From 69fcce3b2c2857fa06537b7156639bec268c7c27 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Mar 2022 21:56:04 -0700 Subject: [PATCH 0782/1054] Remove energy usage from the switch base class (#68821) * Remove energy usage from the switch base class * Remove unused attributes from integrations --- homeassistant/components/aten_pe/switch.py | 2 - homeassistant/components/dlink/switch.py | 8 ---- homeassistant/components/edimax/switch.py | 25 ------------ homeassistant/components/elv/switch.py | 19 +--------- .../components/emulated_kasa/__init__.py | 5 --- homeassistant/components/fibaro/__init__.py | 17 +-------- homeassistant/components/fibaro/switch.py | 15 -------- homeassistant/components/hive/switch.py | 5 --- .../components/homematicip_cloud/light.py | 16 -------- .../components/homematicip_cloud/switch.py | 12 ------ homeassistant/components/mfi/switch.py | 13 ------- homeassistant/components/mqtt/switch.py | 8 ---- homeassistant/components/mystrom/switch.py | 10 ++--- homeassistant/components/netio/switch.py | 23 ----------- homeassistant/components/switch/__init__.py | 33 ---------------- homeassistant/components/vera/__init__.py | 17 +-------- homeassistant/components/vera/climate.py | 8 ---- homeassistant/components/vera/switch.py | 8 ---- homeassistant/components/wemo/switch.py | 18 --------- tests/components/emulated_kasa/test_init.py | 38 +------------------ .../homematicip_cloud/test_light.py | 6 --- .../homematicip_cloud/test_switch.py | 12 +----- tests/components/mfi/test_switch.py | 18 --------- tests/components/mqtt/test_switch.py | 3 +- 24 files changed, 11 insertions(+), 328 deletions(-) diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 1383a77cda6..92a58f37c8c 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -116,7 +116,5 @@ class AtenSwitch(SwitchEntity): status = await self._device.displayOutletStatus(self._outlet) if status == "on": self._attr_is_on = True - self._attr_current_power_w = await self._device.outletPower(self._outlet) elif status == "off": self._attr_is_on = False - self._attr_current_power_w = 0.0 diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 788127805c0..4d5d7b5c639 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -101,14 +101,6 @@ class SmartPlugSwitch(SwitchEntity): return attrs - @property - def current_power_w(self): - """Return the current power usage in Watt.""" - try: - return float(self.data.current_consumption) - except (ValueError, TypeError): - return None - @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 3574ee88172..6f780f8da61 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -48,10 +48,7 @@ class SmartPlugSwitch(SwitchEntity): """Initialize the switch.""" self.smartplug = smartplug self._name = name - self._now_power = None - self._now_energy_day = None self._state = False - self._supports_power_monitoring = False self._info = None self._mac = None @@ -65,16 +62,6 @@ class SmartPlugSwitch(SwitchEntity): """Return the name of the Smart Plug, if any.""" return self._name - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self._now_power - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self._now_energy_day - @property def is_on(self): """Return true if switch is on.""" @@ -93,17 +80,5 @@ class SmartPlugSwitch(SwitchEntity): if not self._info: self._info = self.smartplug.info self._mac = self._info["mac"] - self._supports_power_monitoring = self._info["model"] != "SP1101W" - - if self._supports_power_monitoring: - try: - self._now_power = float(self.smartplug.now_power) - except (TypeError, ValueError): - self._now_power = None - - try: - self._now_energy_day = float(self.smartplug.now_energy_day) - except (TypeError, ValueError): - self._now_energy_day = None self._state = self.smartplug.state == "ON" diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 103915eecb4..8a7da161da0 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -6,7 +6,7 @@ import logging import pypca from serial import SerialException -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity +from homeassistant.components.switch import SwitchEntity from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,8 +14,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -ATTR_TOTAL_ENERGY_KWH = "total_energy_kwh" - DEFAULT_NAME = "PCA 301" @@ -57,7 +55,6 @@ class SmartPlugSwitch(SwitchEntity): self._name = "PCA 301" self._state = None self._available = True - self._emeter_params = {} self._pca = pca @property @@ -83,23 +80,11 @@ class SmartPlugSwitch(SwitchEntity): """Turn the switch off.""" self._pca.turn_off(self._device_id) - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._emeter_params - def update(self): """Update the PCA switch's state.""" try: - self._emeter_params[ - ATTR_CURRENT_POWER_W - ] = f"{self._pca.get_current_power(self._device_id):.1f}" - self._emeter_params[ - ATTR_TOTAL_ENERGY_KWH - ] = f"{self._pca.get_total_consumption(self._device_id):.2f}" - - self._available = True self._state = self._pca.get_state(self._device_id) + self._available = True except (OSError) as ex: if self._available: diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index 967edc8d157..9643962ecb4 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -5,7 +5,6 @@ from sense_energy import PlugInstance, SenseLink import voluptuous as vol from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import ATTR_CURRENT_POWER_W from homeassistant.const import ( CONF_ENTITIES, CONF_NAME, @@ -105,8 +104,6 @@ async def validate_configs(hass, entity_configs): entity_config[CONF_POWER] = power_val elif state.domain == SENSOR_DOMAIN: pass - elif ATTR_CURRENT_POWER_W in state.attributes: - pass else: _LOGGER.debug("No power value defined for: %s", entity_id) @@ -132,8 +129,6 @@ def get_plug_devices(hass, entity_configs): power = float(hass.states.get(power_val).state) elif isinstance(power_val, Template): power = float(power_val.async_render()) - elif ATTR_CURRENT_POWER_W in state.attributes: - power = float(state.attributes[ATTR_CURRENT_POWER_W]) elif state.domain == SENSOR_DOMAIN: power = float(state.state) else: diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index fffc30580d8..1db73538fa0 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -27,14 +27,12 @@ from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert, slugify +from homeassistant.util import slugify from .const import CONF_IMPORT_PLUGINS, DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_CURRENT_POWER_W = "current_power_w" - CONF_COLOR = "color" CONF_DEVICE_CONFIG = "device_config" CONF_DIMMING = "dimming" @@ -529,15 +527,6 @@ class FibaroDevice(Entity): else: self.dont_know_message(cmd) - @property - def current_power_w(self): - """Return the current power usage in W.""" - if "power" in self.fibaro_device.properties and ( - power := self.fibaro_device.properties.power - ): - return convert(power, float, 0.0) - return None - @property def current_binary_state(self): """Return the current binary state.""" @@ -567,10 +556,6 @@ class FibaroDevice(Entity): ) if "fibaroAlarmArm" in self.fibaro_device.interfaces: attr[ATTR_ARMED] = bool(self.fibaro_device.properties.armed) - if "power" in self.fibaro_device.interfaces: - attr[ATTR_CURRENT_POWER_W] = convert( - self.fibaro_device.properties.power, float, 0.0 - ) except (ValueError, KeyError): pass diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index a075c4e0704..73056c095c6 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -5,7 +5,6 @@ from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice from .const import DOMAIN @@ -45,20 +44,6 @@ class FibaroSwitch(FibaroDevice, SwitchEntity): self.call_turn_off() self._state = False - @property - def current_power_w(self): - """Return the current power usage in W.""" - if "power" in self.fibaro_device.interfaces: - return convert(self.fibaro_device.properties.power, float, 0.0) - return None - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - if "energy" in self.fibaro_device.interfaces: - return convert(self.fibaro_device.properties.energy, float, 0.0) - return None - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 4eb286462f4..cb9ac79d51e 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -69,11 +69,6 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): ATTR_MODE: self.attributes.get(ATTR_MODE), } - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self.device["status"].get("power_usage") - @property def is_on(self): """Return true if switch is on.""" diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index a45759f9633..457f2fdcd25 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -32,9 +32,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP -ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" -ATTR_CURRENT_POWER_W = "current_power_w" - async def async_setup_entry( hass: HomeAssistant, @@ -94,19 +91,6 @@ class HomematicipLight(HomematicipGenericEntity, LightEntity): class HomematicipLightMeasuring(HomematicipLight): """Representation of the HomematicIP measuring light.""" - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the light.""" - state_attr = super().extra_state_attributes - - current_power_w = self._device.currentPowerConsumption - if current_power_w > 0.05: - state_attr[ATTR_CURRENT_POWER_W] = round(current_power_w, 2) - - state_attr[ATTR_TODAY_ENERGY_KWH] = round(self._device.energyCounter, 2) - - return state_attr - class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): """Representation of HomematicIP Cloud dimmer.""" diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 3237fdf1f00..82eafe1f212 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -166,15 +166,3 @@ class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): class HomematicipSwitchMeasuring(HomematicipSwitch): """Representation of the HomematicIP measuring switch.""" - - @property - def current_power_w(self) -> float: - """Return the current power usage in W.""" - return self._device.currentPowerConsumption - - @property - def today_energy_kwh(self) -> int: - """Return the today total energy usage in kWh.""" - if self._device.energyCounter is None: - return 0 - return round(self._device.energyCounter) diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 7564229d526..76a55e46ba1 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -110,16 +110,3 @@ class MfiSwitch(SwitchEntity): """Turn the switch off.""" self._port.control(False) self._target_state = False - - @property - def current_power_w(self): - """Return the current power usage in W.""" - return int(self._port.data.get("active_pwr", 0)) - - @property - def extra_state_attributes(self): - """Return the state attributes for the device.""" - return { - "volts": round(self._port.data.get("v_rms", 0), 1), - "amps": round(self._port.data.get("i_rms", 0), 1), - } diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 1a471ea2ad0..db54ae08953 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -41,13 +41,6 @@ from .mixins import ( async_setup_platform_helper, ) -MQTT_SWITCH_ATTRIBUTES_BLOCKED = frozenset( - { - switch.ATTR_CURRENT_POWER_W, - switch.ATTR_TODAY_ENERGY_KWH, - } -) - DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" @@ -106,7 +99,6 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" _entity_id_format = switch.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_SWITCH_ATTRIBUTES_BLOCKED def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index b8089eb116f..41303b25a9f 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -72,11 +72,6 @@ class MyStromSwitch(SwitchEntity): """Return a unique ID.""" return self.plug._mac # pylint: disable=protected-access - @property - def current_power_w(self): - """Return the current power consumption in W.""" - return self.plug.consumption - @property def available(self): """Could the device be accessed during the last update call.""" @@ -103,5 +98,6 @@ class MyStromSwitch(SwitchEntity): self.relay = self.plug.relay self._available = True except MyStromConnectionError: - self._available = False - _LOGGER.error("No route to myStrom plug") + if self._available: + self._available = False + _LOGGER.error("No route to myStrom plug") diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 2ce3fdf5d85..cfd736c9538 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -175,26 +175,3 @@ class NetioSwitch(SwitchEntity): def update(self): """Update the state.""" self.netio.update() - - @property - def extra_state_attributes(self): - """Return optional state attributes.""" - return { - ATTR_TOTAL_CONSUMPTION_KWH: self.cumulated_consumption_kwh, - ATTR_START_DATE: self.start_date.split("|")[0], - } - - @property - def current_power_w(self): - """Return actual power.""" - return self.netio.consumptions[int(self.outlet) - 1] - - @property - def cumulated_consumption_kwh(self): - """Return the total enerygy consumption since start_date.""" - return self.netio.cumulated_consumptions[int(self.outlet) - 1] - - @property - def start_date(self): - """Point in time when the energy accumulation started.""" - return self.netio.start_dates[int(self.outlet) - 1] diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 2abb9ff81ca..7ef4a754b55 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, final import voluptuous as vol @@ -32,16 +31,8 @@ SCAN_INTERVAL = timedelta(seconds=30) ENTITY_ID_FORMAT = DOMAIN + ".{}" -ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" -ATTR_CURRENT_POWER_W = "current_power_w" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -PROP_TO_ATTR = { - "current_power_w": ATTR_CURRENT_POWER_W, - "today_energy_kwh": ATTR_TODAY_ENERGY_KWH, -} - _LOGGER = logging.getLogger(__name__) @@ -107,14 +98,7 @@ class SwitchEntity(ToggleEntity): """Base class for switch entities.""" entity_description: SwitchEntityDescription - _attr_current_power_w: float | None = None _attr_device_class: SwitchDeviceClass | str | None - _attr_today_energy_kwh: float | None = None - - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - return self._attr_current_power_w @property def device_class(self) -> SwitchDeviceClass | str | None: @@ -124,20 +108,3 @@ class SwitchEntity(ToggleEntity): if hasattr(self, "entity_description"): return self.entity_description.device_class return None - - @property - def today_energy_kwh(self) -> float | None: - """Return the today total energy usage in kWh.""" - return self._attr_today_energy_kwh - - @final - @property - def state_attributes(self) -> dict[str, Any] | None: - """Return the optional state attributes.""" - data = {} - - for prop, attr in PROP_TO_ATTR.items(): - if (value := getattr(self, prop)) is not None: - data[attr] = value - - return data diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index d7cd8db051b..d923861112b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -28,7 +28,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from homeassistant.util import convert, slugify +from homeassistant.util import slugify from homeassistant.util.dt import utc_from_timestamp from .common import ( @@ -39,14 +39,7 @@ from .common import ( set_controller_data, ) from .config_flow import fix_device_id_list, new_options -from .const import ( - ATTR_CURRENT_ENERGY_KWH, - ATTR_CURRENT_POWER_W, - CONF_CONTROLLER, - CONF_LEGACY_UNIQUE_ID, - DOMAIN, - VERA_ID_FORMAT, -) +from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN, VERA_ID_FORMAT _LOGGER = logging.getLogger(__name__) @@ -279,12 +272,6 @@ class VeraDevice(Generic[_DeviceTypeT], Entity): tripped = self.vera_device.is_tripped attr[ATTR_TRIPPED] = "True" if tripped else "False" - if power := self.vera_device.power: - attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) - - if energy := self.vera_device.energy: - attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) - attr["Vera Device Id"] = self.vera_device.vera_device_id return attr diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 477351480d2..399d4ace278 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import convert from . import VeraDevice from .common import ControllerData, get_controller_data @@ -111,13 +110,6 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): self.schedule_update_ha_state() - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - if power := self.vera_device.power: - return convert(power, float, 0.0) - return None - @property def temperature_unit(self) -> str: """Return the unit of measurement.""" diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index f1de72870f7..b146ed39ade 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -10,7 +10,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import convert from . import VeraDevice from .common import ControllerData, get_controller_data @@ -55,13 +54,6 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): self._state = False self.schedule_update_ha_state() - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - if power := self.vera_device.power: - return convert(power, float, 0.0) - return None - @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 8f8e5dcb5e3..d3c6264ab89 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -111,24 +111,6 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): uptime.day - 1, uptime.hour, uptime.minute, uptime.second ) - @property - def current_power_w(self) -> float | None: - """Return the current power usage in W.""" - if not isinstance(self.wemo, Insight): - return None - milliwatts = convert(self.wemo.insight_params.get("currentpower"), float, 0.0) - assert isinstance(milliwatts, float) - return milliwatts / 1000.0 - - @property - def today_energy_kwh(self) -> float | None: - """Return the today total energy usage in kWh.""" - if not isinstance(self.wemo, Insight): - return None - milliwatt_seconds = convert(self.wemo.insight_params.get("todaymw"), float, 0.0) - assert isinstance(milliwatt_seconds, float) - return round(milliwatt_seconds / (1000.0 * 1000.0 * 60), 2) - @property def detail_state(self) -> str: """Return the state of the device.""" diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index 72fc4dd94e2..4295d162aa5 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -15,13 +15,9 @@ from homeassistant.components.fan import ( ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - DOMAIN as SWITCH_DOMAIN, -) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, CONF_ENTITIES, CONF_NAME, SERVICE_TURN_OFF, @@ -218,38 +214,6 @@ async def test_switch_power(hass): SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True ) - hass.states.async_set( - ENTITY_SWITCH, - STATE_ON, - attributes={ATTR_CURRENT_POWER_W: 100, ATTR_FRIENDLY_NAME: "AC"}, - ) - - switch = hass.states.get(ENTITY_SWITCH) - assert switch.state == STATE_ON - power = switch.attributes[ATTR_CURRENT_POWER_W] - assert power == 100 - assert switch.name == "AC" - - plug_it = emulated_kasa.get_plug_devices(hass, config) - plug = next(plug_it).generate_response() - - assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" - power = nested_value(plug, "emeter", "get_realtime", "power") - assert math.isclose(power, power) - - hass.states.async_set( - ENTITY_SWITCH, - STATE_ON, - attributes={ATTR_CURRENT_POWER_W: 120, ATTR_FRIENDLY_NAME: "AC"}, - ) - - plug_it = emulated_kasa.get_plug_devices(hass, config) - plug = next(plug_it).generate_response() - - assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" - power = nested_value(plug, "emeter", "get_realtime", "power") - assert math.isclose(power, 120) - # Turn off await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index b4dbd0d140e..b3feb9586cd 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -2,10 +2,6 @@ from homematicip.base.enums import RGBColorState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.homematicip_cloud.light import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, -) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, @@ -233,8 +229,6 @@ async def test_hmip_light_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 - assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 6.33 await hass.services.async_call( "light", "turn_off", {"entity_id": entity_id}, blocking=True diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index f2b3dfba32c..6d89ed31f5f 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -3,11 +3,7 @@ from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_GROUP_MEMBER_UNREACHABLE, ) -from homeassistant.components.switch import ( - ATTR_CURRENT_POWER_W, - ATTR_TODAY_ENERGY_KWH, - DOMAIN as SWITCH_DOMAIN, -) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -132,12 +128,6 @@ async def test_hmip_switch_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 - assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 36 - - await async_manipulate_test_data(hass, hmip_device, "energyCounter", None) - ha_state = hass.states.get(entity_id) - assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH) async def test_hmip_group_switch(hass, default_mock_hap_factory): diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 8eed41f8a32..c61585898de 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -103,21 +103,3 @@ async def test_turn_off(port, switch): assert port.control.call_args == mock.call(False) # pylint: disable=protected-access assert not switch._target_state - - -async def test_current_power_w(port, switch): - """Test current power.""" - port.data = {"active_pwr": 10} - assert switch.current_power_w == 10 - - -async def test_current_power_w_no_data(port, switch): - """Test current power if there is no data.""" - port.data = {"notpower": 123} - assert switch.current_power_w == 0 - - -async def test_extra_state_attributes(port, switch): - """Test the state attributes.""" - port.data = {"v_rms": 1.25, "i_rms": 2.75} - assert switch.extra_state_attributes == {"volts": 1.2, "amps": 2.8} diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index a458ac03baa..a7dd0d8c31e 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest from homeassistant.components import switch -from homeassistant.components.mqtt.switch import MQTT_SWITCH_ATTRIBUTES_BLOCKED from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_DEVICE_CLASS, @@ -292,7 +291,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, MQTT_SWITCH_ATTRIBUTES_BLOCKED + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, {} ) From 085b44e45afecdc25563b47503e6bef3f9ee73fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Mar 2022 07:09:42 +0200 Subject: [PATCH 0783/1054] Simplify is_on state for switch groups (#68805) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/group/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index cbee6902f02..6b879e55cea 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -168,6 +168,6 @@ class SwitchGroup(GroupEntity, SwitchEntity): self._attr_is_on = None else: # Set as ON if any / all member is ON - self._attr_is_on = self.mode(list(map(lambda x: x == STATE_ON, states))) + self._attr_is_on = self.mode(state == STATE_ON for state in states) self._attr_available = any(state != STATE_UNAVAILABLE for state in states) From 6110e7d194f6e5b03e885b5bbe0772c11f256d50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Mar 2022 19:22:58 -1000 Subject: [PATCH 0784/1054] Remove unneeded __repr__ in samsungtv logging (#68817) * Remove unneeded __repr__ in samsungtv logging * keep only the ones we need --- homeassistant/components/samsungtv/bridge.py | 22 +++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index d093dc35c72..9fb9fc0fe9b 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -528,7 +528,7 @@ class SamsungTVWSBridge(SamsungTVBridge): LOGGER.info( "Failed to get remote for %s, re-authentication required: %s", self.host, - err.__repr__(), + repr(err), ) self._notify_reauth_callback() self._remote = None @@ -536,7 +536,7 @@ class SamsungTVWSBridge(SamsungTVBridge): LOGGER.info( "Failed to get remote for %s: %s", self.host, - err.__repr__(), + repr(err), ) self._remote = None except ConnectionFailure as err: @@ -544,13 +544,11 @@ class SamsungTVWSBridge(SamsungTVBridge): "Unexpected ConnectionFailure trying to get remote for %s, " "please report this issue: %s", self.host, - err.__repr__(), + repr(err), ) self._remote = None except (WebSocketException, AsyncioTimeoutError, OSError) as err: - LOGGER.debug( - "Failed to get remote for %s: %s", self.host, err.__repr__() - ) + LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err)) self._remote = None else: LOGGER.debug("Created SamsungTVWSBridge for %s", self.host) @@ -614,9 +612,7 @@ class SamsungTVWSBridge(SamsungTVBridge): await self._remote.close() self._remote = None except OSError as err: - LOGGER.debug( - "Error closing connection to %s: %s", self.host, err.__repr__() - ) + LOGGER.debug("Error closing connection to %s: %s", self.host, err) class SamsungTVEncryptedBridge(SamsungTVBridge): @@ -772,9 +768,7 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): try: await self._remote.start_listening() except (WebSocketException, AsyncioTimeoutError, OSError) as err: - LOGGER.debug( - "Failed to get remote for %s: %s", self.host, err.__repr__() - ) + LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err)) self._remote = None else: LOGGER.debug("Created SamsungTVEncryptedBridge for %s", self.host) @@ -809,6 +803,4 @@ class SamsungTVEncryptedBridge(SamsungTVBridge): await self._remote.close() self._remote = None except OSError as err: - LOGGER.debug( - "Error closing connection to %s: %s", self.host, err.__repr__() - ) + LOGGER.debug("Error closing connection to %s: %s", self.host, err) From 8714beb5e7d775de71fccac0da98069e6bc28581 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Mar 2022 19:23:52 -1000 Subject: [PATCH 0785/1054] Update iot_class for samsungtv (#68812) #68777 Made volume updates local push, we should upgrade the iot class --- homeassistant/components/samsungtv/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index c189a20b241..123896a72ab 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -46,6 +46,6 @@ "@epenet" ], "config_flow": true, - "iot_class": "local_polling", + "iot_class": "local_push", "loggers": ["samsungctl", "samsungtvws"] } From fb14ae211eb88f43e8de36cdf82b580080514fa9 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Tue, 29 Mar 2022 02:47:20 -0400 Subject: [PATCH 0786/1054] Add support for static typing for the PECO library (#68707) --- homeassistant/components/peco/manifest.json | 2 +- homeassistant/components/peco/sensor.py | 79 +++++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/peco/test_sensor.py | 4 +- 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/peco/manifest.json b/homeassistant/components/peco/manifest.json index 2a577faddbf..2c2f9ba3679 100644 --- a/homeassistant/components/peco/manifest.json +++ b/homeassistant/components/peco/manifest.json @@ -8,6 +8,6 @@ ], "iot_class": "cloud_polling", "requirements": [ - "peco==0.0.21" + "peco==0.0.25" ] } \ No newline at end of file diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py index d4490cd5a9b..2524349567e 100644 --- a/homeassistant/components/peco/sensor.py +++ b/homeassistant/components/peco/sensor.py @@ -1,9 +1,13 @@ """Sensor component for PECO outage counter.""" -import asyncio -from datetime import timedelta -from typing import Final, cast +from __future__ import annotations -from peco import BadJSONError, HttpError +import asyncio +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta +from typing import Final + +from peco import BadJSONError, HttpError, OutageResults from homeassistant.components.sensor import ( SensorEntity, @@ -23,16 +27,44 @@ from homeassistant.helpers.update_coordinator import ( from .const import CONF_COUNTY, DOMAIN, LOGGER, SCAN_INTERVAL + +@dataclass +class PECOSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[OutageResults], int] + + +@dataclass +class PECOSensorEntityDescription( + SensorEntityDescription, PECOSensorEntityDescriptionMixin +): + """Description for PECO sensor.""" + + PARALLEL_UPDATES: Final = 0 -SENSOR_LIST = ( - SensorEntityDescription(key="customers_out", name="Customers Out"), - SensorEntityDescription( +SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = ( + PECOSensorEntityDescription( + key="customers_out", + name="Customers Out", + value_fn=lambda data: int(data.customers_out), + ), + PECOSensorEntityDescription( key="percent_customers_out", name="Percent Customers Out", native_unit_of_measurement=PERCENTAGE, + value_fn=lambda data: int(data.percent_customers_out), + ), + PECOSensorEntityDescription( + key="outage_count", + name="Outage Count", + value_fn=lambda data: int(data.outage_count), + ), + PECOSensorEntityDescription( + key="customers_served", + name="Customers Served", + value_fn=lambda data: int(data.customers_served), ), - SensorEntityDescription(key="outage_count", name="Outage Count"), - SensorEntityDescription(key="customers_served", name="Customers Served"), ) @@ -46,16 +78,13 @@ async def async_setup_entry( websession = async_get_clientsession(hass) county: str = config_entry.data[CONF_COUNTY] - async def async_update_data() -> dict[str, float]: + async def async_update_data() -> OutageResults: """Fetch data from API.""" try: - data = ( - cast(dict[str, float], await api.get_outage_totals(websession)) + data: OutageResults = ( + await api.get_outage_totals(websession) if county == "TOTAL" - else cast( - dict[str, float], - await api.get_outage_count(county, websession), - ) + else await api.get_outage_count(county, websession) ) except HttpError as err: raise UpdateFailed(f"Error fetching data: {err}") from err @@ -63,11 +92,6 @@ async def async_setup_entry( raise UpdateFailed(f"Error parsing data: {err}") from err except asyncio.TimeoutError as err: raise UpdateFailed(f"Timeout fetching data: {err}") from err - if data["percent_customers_out"] < 5: - percent_out = round( - data["customers_out"] / data["customers_served"] * 100, 3 - ) - data["percent_customers_out"] = percent_out return data coordinator = DataUpdateCoordinator( @@ -87,19 +111,18 @@ async def async_setup_entry( return -class PecoSensor( - CoordinatorEntity[DataUpdateCoordinator[dict[str, float]]], SensorEntity -): +class PecoSensor(CoordinatorEntity[DataUpdateCoordinator[OutageResults]], SensorEntity): """PECO outage counter sensor.""" _attr_state_class = SensorStateClass.MEASUREMENT _attr_icon: str = "mdi:power-plug-off" + entity_description: PECOSensorEntityDescription def __init__( self, - description: SensorEntityDescription, + description: PECOSensorEntityDescription, county: str, - coordinator: DataUpdateCoordinator[dict[str, float]], + coordinator: DataUpdateCoordinator[OutageResults], ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -108,6 +131,6 @@ class PecoSensor( self.entity_description = description @property - def native_value(self) -> float: + def native_value(self) -> int: """Return the value of the sensor.""" - return self.coordinator.data[self.entity_description.key] + return self.entity_description.value_fn(self.coordinator.data) diff --git a/requirements_all.txt b/requirements_all.txt index ce78324ce8d..98a48de6941 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ panasonic_viera==0.3.6 pdunehd==1.3.2 # homeassistant.components.peco -peco==0.0.21 +peco==0.0.25 # homeassistant.components.pencom pencompy==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d43357bf29d..e6eaa07ef30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ panasonic_viera==0.3.6 pdunehd==1.3.2 # homeassistant.components.peco -peco==0.0.21 +peco==0.0.25 # homeassistant.components.aruba # homeassistant.components.cisco_ios diff --git a/tests/components/peco/test_sensor.py b/tests/components/peco/test_sensor.py index ea879c8f07a..cb630f9436e 100644 --- a/tests/components/peco/test_sensor.py +++ b/tests/components/peco/test_sensor.py @@ -66,7 +66,7 @@ async def test_sensor_available( if sensor == "total_customers_out": assert sensor_entity.state == "123" elif sensor == "total_percent_customers_out": - assert sensor_entity.state == "15.589" + assert sensor_entity.state == "1" elif sensor == "total_outage_count": assert sensor_entity.state == "456" elif sensor == "total_customers_served": @@ -125,7 +125,7 @@ async def test_sensor_available( if sensor == "bucks_customers_out": assert sensor_entity.state == "123" elif sensor == "bucks_percent_customers_out": - assert sensor_entity.state == "15.589" + assert sensor_entity.state == "1" elif sensor == "bucks_outage_count": assert sensor_entity.state == "456" elif sensor == "bucks_customers_served": From 1d88ce0d04277f0e01fa1cd937638be4b2bf5613 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Tue, 29 Mar 2022 08:58:54 +0200 Subject: [PATCH 0787/1054] Update xknx to version 0.20.0 (#68818) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 924fa284e93..e8f6603a9d6 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.19.2" + "xknx==0.20.0" ], "codeowners": [ "@Julius2342", diff --git a/requirements_all.txt b/requirements_all.txt index 98a48de6941..f99aa380539 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2431,7 +2431,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.19.2 +xknx==0.20.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6eaa07ef30..3bd12c4b6db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1560,7 +1560,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.19.2 +xknx==0.20.0 # homeassistant.components.bluesound # homeassistant.components.fritz From 557fa198d8d24e35dfb9e91e331451148f2167b7 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Tue, 29 Mar 2022 03:08:40 -0400 Subject: [PATCH 0788/1054] Update sense library to 0.10.4 (#68816) --- homeassistant/components/emulated_kasa/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json index c7f9175bc5e..31319a656cd 100644 --- a/homeassistant/components/emulated_kasa/manifest.json +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_kasa", "name": "Emulated Kasa", "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", - "requirements": ["sense_energy==0.10.3"], + "requirements": ["sense_energy==0.10.4"], "codeowners": ["@kbickar"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 04b0b451db4..192a0f6defe 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.10.3"], + "requirements": ["sense_energy==0.10.4"], "codeowners": ["@kbickar"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index f99aa380539..4be372204a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2120,7 +2120,7 @@ sendgrid==6.8.2 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.3 +sense_energy==0.10.4 # homeassistant.components.sentry sentry-sdk==1.5.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bd12c4b6db..371c510f7b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1360,7 +1360,7 @@ securetar==2022.2.0 # homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.10.3 +sense_energy==0.10.4 # homeassistant.components.sentry sentry-sdk==1.5.8 From e85fb874389b25f297fe73c605d371db4957ed9a Mon Sep 17 00:00:00 2001 From: patagona Date: Tue, 29 Mar 2022 09:10:21 +0200 Subject: [PATCH 0789/1054] Handle MPD songs with multiple artists (#68759) --- homeassistant/components/mpd/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 9eda625b039..b84c05dc6af 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -265,7 +265,10 @@ class MpdDevice(MediaPlayerEntity): @property def media_artist(self): """Return the artist of current playing media (Music track only).""" - return self._currentsong.get("artist") + artists = self._currentsong.get("artist") + if isinstance(artists, list): + return ", ".join(artists) + return artists @property def media_album_name(self): From 14fbda84123d2a414299864cb978f9004a7d7a33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Mar 2022 21:10:49 -1000 Subject: [PATCH 0790/1054] Add option to connect to elkm1 non-secure when secure is discovered (#68735) Co-authored-by: Glenn Waters --- homeassistant/components/elkm1/config_flow.py | 42 +++-- homeassistant/components/elkm1/strings.json | 2 + .../components/elkm1/translations/en.json | 12 +- tests/components/elkm1/__init__.py | 1 + tests/components/elkm1/test_config_flow.py | 166 ++++++++++++++++-- 5 files changed, 190 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 05ae860d273..e4f6dc7f083 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -37,7 +37,9 @@ from .discovery import ( CONF_DEVICE = "device" +NON_SECURE_PORT = 2101 SECURE_PORT = 2601 +STANDARD_PORTS = {NON_SECURE_PORT, SECURE_PORT} _LOGGER = logging.getLogger(__name__) @@ -48,6 +50,7 @@ PROTOCOL_MAP = { "serial": "serial://", } + VALIDATE_TIMEOUT = 35 BASE_SCHEMA = { @@ -60,6 +63,11 @@ ALL_PROTOCOLS = [*SECURE_PROTOCOLS, "non-secure", "serial"] DEFAULT_SECURE_PROTOCOL = "secure" DEFAULT_NON_SECURE_PROTOCOL = "non-secure" +PORT_PROTOCOL_MAP = { + NON_SECURE_PORT: DEFAULT_NON_SECURE_PROTOCOL, + SECURE_PORT: DEFAULT_SECURE_PROTOCOL, +} + async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str]: """Validate the user input allows us to connect. @@ -97,6 +105,13 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str return {"title": device_name, CONF_HOST: url, CONF_PREFIX: slugify(prefix)} +def _address_from_discovery(device: ElkSystem): + """Append the port only if its non-standard.""" + if device.port in STANDARD_PORTS: + return device.ip_address + return f"{device.ip_address}:{device.port}" + + def _make_url_from_data(data): if host := data.get(CONF_HOST): return host @@ -109,7 +124,7 @@ def _make_url_from_data(data): def _placeholders_from_device(device: ElkSystem) -> dict[str, str]: return { "mac_address": _short_mac(device.mac_address), - "host": f"{device.ip_address}:{device.port}", + "host": _address_from_discovery(device), } @@ -258,26 +273,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device = self._discovered_device assert device is not None if user_input is not None: - user_input[CONF_ADDRESS] = f"{device.ip_address}:{device.port}" + user_input[CONF_ADDRESS] = _address_from_discovery(device) if self._async_current_entries(): user_input[CONF_PREFIX] = _short_mac(device.mac_address) else: user_input[CONF_PREFIX] = "" - if device.port != SECURE_PORT: - user_input[CONF_PROTOCOL] = DEFAULT_NON_SECURE_PROTOCOL errors, result = await self._async_create_or_error(user_input, False) if not errors: return result - base_schmea = BASE_SCHEMA.copy() - if device.port == SECURE_PORT: - base_schmea[ - vol.Required(CONF_PROTOCOL, default=DEFAULT_SECURE_PROTOCOL) - ] = vol.In(SECURE_PROTOCOLS) - + default_proto = PORT_PROTOCOL_MAP.get(device.port, DEFAULT_SECURE_PROTOCOL) return self.async_show_form( step_id="discovered_connection", - data_schema=vol.Schema(base_schmea), + data_schema=vol.Schema( + { + **BASE_SCHEMA, + vol.Required(CONF_PROTOCOL, default=default_proto): vol.In( + ALL_PROTOCOLS + ), + } + ), errors=errors, description_placeholders=_placeholders_from_device(device), ) @@ -337,7 +352,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() - return (await self._async_create_or_error(user_input, True))[1] + errors, result = await self._async_create_or_error(user_input, True) + if errors: + return self.async_abort(reason=list(errors.values())[0]) + return result def _url_already_configured(self, url): """See if we already have a elkm1 matching user input configured.""" diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index 35672d5df80..d1871c7536c 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -38,6 +38,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "An ElkM1 with this prefix is already configured", diff --git a/homeassistant/components/elkm1/translations/en.json b/homeassistant/components/elkm1/translations/en.json index 3b1993f3fed..0206859cf8e 100644 --- a/homeassistant/components/elkm1/translations/en.json +++ b/homeassistant/components/elkm1/translations/en.json @@ -4,7 +4,9 @@ "address_already_configured": "An ElkM1 with this address is already configured", "already_configured": "An ElkM1 with this prefix is already configured", "already_in_progress": "Configuration flow is already in progress", - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "The IP address or domain or serial port if connecting via serial.", - "device": "Device", - "password": "Password", - "prefix": "A unique prefix (leave blank if you only have one ElkM1).", - "protocol": "Protocol", - "temperature_unit": "The temperature unit ElkM1 uses.", - "username": "Username" + "device": "Device" }, "description": "Choose a discovered system or 'Manual Entry' if no devices have been discovered.", "title": "Connect to Elk-M1 Control" diff --git a/tests/components/elkm1/__init__.py b/tests/components/elkm1/__init__.py index 128d0a0d777..29cd2aa297d 100644 --- a/tests/components/elkm1/__init__.py +++ b/tests/components/elkm1/__init__.py @@ -9,6 +9,7 @@ MOCK_IP_ADDRESS = "127.0.0.1" MOCK_MAC = "aa:bb:cc:dd:ee:ff" ELK_DISCOVERY = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 2601) ELK_NON_SECURE_DISCOVERY = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 2101) +ELK_DISCOVERY_NON_STANDARD_PORT = ElkSystem(MOCK_MAC, MOCK_IP_ADDRESS, 444) def mock_elk(invalid_auth=None, sync_complete=None, exception=None): diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index a1ec9eeff8e..b8f9e51b6fc 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( ELK_DISCOVERY, + ELK_DISCOVERY_NON_STANDARD_PORT, ELK_NON_SECURE_DISCOVERY, MOCK_IP_ADDRESS, MOCK_MAC, @@ -24,6 +25,8 @@ from tests.common import MockConfigEntry DHCP_DISCOVERY = dhcp.DhcpServiceInfo(MOCK_IP_ADDRESS, "", MOCK_MAC) ELK_DISCOVERY_INFO = asdict(ELK_DISCOVERY) +ELK_DISCOVERY_INFO_NON_STANDARD_PORT = asdict(ELK_DISCOVERY_NON_STANDARD_PORT) + MODULE = "homeassistant.components.elkm1" @@ -322,7 +325,7 @@ async def test_form_user_with_secure_elk_with_discovery(hass): assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { "auto_configure": True, - "host": "elks://127.0.0.1:2601", + "host": "elks://127.0.0.1", "password": "test-password", "prefix": "", "username": "test-username", @@ -822,6 +825,102 @@ async def test_form_import_device_discovered(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_import_non_secure_device_discovered(hass): + """Test we can import non-secure with discovery.""" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "elk://127.0.0.1:2101", + "username": "", + "password": "", + "auto_configure": True, + "prefix": "ohana", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "ohana" + assert result["result"].unique_id == MOCK_MAC + assert result["data"] == { + "auto_configure": True, + "host": "elk://127.0.0.1:2101", + "password": "", + "prefix": "ohana", + "username": "", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_non_secure_non_stanadard_port_device_discovered(hass): + """Test we can import non-secure non standard port with discovery.""" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "elk://127.0.0.1:444", + "username": "", + "password": "", + "auto_configure": True, + "prefix": "ohana", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "ohana" + assert result["result"].unique_id == MOCK_MAC + assert result["data"] == { + "auto_configure": True, + "host": "elk://127.0.0.1:444", + "password": "", + "prefix": "ohana", + "username": "", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_non_secure_device_discovered_invalid_auth(hass): + """Test we abort import with invalid auth.""" + + mocked_elk = mock_elk(invalid_auth=True, sync_complete=False) + with _patch_discovery(), _patch_elk(elk=mocked_elk): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "elks://127.0.0.1", + "username": "invalid", + "password": "", + "auto_configure": False, + "prefix": "ohana", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "invalid_auth" + + async def test_form_import_existing(hass): """Test we abort on existing import.""" config_entry = MockConfigEntry( @@ -999,7 +1098,52 @@ async def test_discovered_by_discovery(hass): assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, - "host": "elks://127.0.0.1:2601", + "host": "elks://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_discovered_by_discovery_non_standard_port(hass): + """Test we can setup when discovered from discovery with a non-standard port.""" + + with _patch_discovery(), _patch_elk(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=ELK_DISCOVERY_INFO_NON_STANDARD_PORT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1 ddeeff" + assert result2["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1:444", "password": "test-password", "prefix": "", "username": "test-username", @@ -1063,7 +1207,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, - "host": "elks://127.0.0.1:2601", + "host": "elks://127.0.0.1", "password": "test-password", "prefix": "", "username": "test-username", @@ -1098,8 +1242,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + "protocol": "non-secure", }, ) await hass.async_block_till_done() @@ -1108,10 +1251,10 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass): assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, - "host": "elk://127.0.0.1:2101", - "password": "test-password", + "host": "elk://127.0.0.1", + "password": "", "prefix": "", - "username": "test-username", + "username": "", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1146,10 +1289,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "username": "test-username", - "password": "test-password", - }, + {"username": "test-username", "password": "test-password"}, ) await hass.async_block_till_done() @@ -1157,7 +1297,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass): assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { "auto_configure": True, - "host": "elks://127.0.0.1:2601", + "host": "elks://127.0.0.1", "password": "test-password", "prefix": "ddeeff", "username": "test-username", From 05ddd773ff64237f9cb2eca9a9b6dd998868b95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 29 Mar 2022 10:24:15 +0300 Subject: [PATCH 0791/1054] Fix _abort_if_unique_id_configured updates type hint (#68730) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e10574bdc4a..933cebebcf5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1230,7 +1230,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _abort_if_unique_id_configured( self, - updates: dict[Any, Any] | None = None, + updates: dict[str, Any] | None = None, reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" From d7634d1cb19dd0b268e613dccb61972d79d18677 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 28 Mar 2022 21:45:25 -1000 Subject: [PATCH 0792/1054] Additional strict typing for additional recorder internals (#68689) * Strict typing for additional recorder internals * revert * fix refactoring error --- .strict-typing | 3 + homeassistant/components/recorder/__init__.py | 6 +- homeassistant/components/recorder/models.py | 65 ++++++++++--------- homeassistant/components/recorder/pool.py | 19 ++++-- mypy.ini | 33 ++++++++++ 5 files changed, 84 insertions(+), 42 deletions(-) diff --git a/.strict-typing b/.strict-typing index 866e8bfd95c..d9c5e77f66d 100644 --- a/.strict-typing +++ b/.strict-typing @@ -164,11 +164,14 @@ homeassistant.components.pure_energie.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* +homeassistant.components.recorder.models homeassistant.components.recorder.history +homeassistant.components.recorder.pool homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.statistics homeassistant.components.recorder.util +homeassistant.components.recorder.websocket_api homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.ridwell.* diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index bbe1b676741..194d74611ca 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -232,8 +232,7 @@ def run_information(hass, point_in_time: datetime | None = None) -> RecorderRuns There is also the run that covers point_in_time. """ - run_info = run_information_from_instance(hass, point_in_time) - if run_info: + if run_info := run_information_from_instance(hass, point_in_time): return run_info with session_scope(hass=hass) as session: @@ -1028,8 +1027,7 @@ class Recorder(threading.Thread): try: if event.event_type == EVENT_STATE_CHANGED: - dbevent = Events.from_event(event, event_data="{}") - dbevent.event_data = None + dbevent = Events.from_event(event, event_data=None) else: dbevent = Events.from_event(event) except (TypeError, ValueError): diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 71ac332d34e..e91fe0cf3cc 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime, timedelta import json import logging -from typing import Any, TypedDict, overload +from typing import Any, TypedDict, cast, overload from fnvhash import fnv1a_32 from sqlalchemy import ( @@ -35,6 +35,7 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State +from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util from .const import JSON_DUMP @@ -113,11 +114,13 @@ class Events(Base): # type: ignore[misc,valid-type] ) @staticmethod - def from_event(event, event_data=None): + def from_event( + event: Event, event_data: UndefinedType | None = UNDEFINED + ) -> Events: """Create an event database object from a native event.""" return Events( event_type=event.event_type, - event_data=event_data or JSON_DUMP(event.data), + event_data=JSON_DUMP(event.data) if event_data is UNDEFINED else event_data, origin=str(event.origin.value), time_fired=event.time_fired, context_id=event.context.id, @@ -125,7 +128,7 @@ class Events(Base): # type: ignore[misc,valid-type] context_parent_id=event.context.parent_id, ) - def to_native(self, validate_entity_id=True): + def to_native(self, validate_entity_id: bool = True) -> Event | None: """Convert to a native HA Event.""" context = Context( id=self.context_id, @@ -185,7 +188,7 @@ class States(Base): # type: ignore[misc,valid-type] ) @staticmethod - def from_event(event) -> States: + def from_event(event: Event) -> States: """Create object from a state_changed event.""" entity_id = event.data["entity_id"] state: State | None = event.data.get("new_state") @@ -266,12 +269,12 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def hash_shared_attrs(shared_attrs: str) -> int: """Return the hash of json encoded shared attributes.""" - return fnv1a_32(shared_attrs.encode("utf-8")) + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return json.loads(self.shared_attrs) + return cast(dict[str, Any], json.loads(self.shared_attrs)) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state attributes: %s", self) @@ -311,8 +314,8 @@ class StatisticsBase: id = Column(Integer, Identity(), primary_key=True) created = Column(DATETIME_TYPE, default=dt_util.utcnow) - @declared_attr - def metadata_id(self): + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: """Define the metadata_id column for sub classes.""" return Column( Integer, @@ -329,7 +332,7 @@ class StatisticsBase: sum = Column(DOUBLE_TYPE) @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData): + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: """Create object from a statistics.""" return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, @@ -422,7 +425,7 @@ class RecorderRuns(Base): # type: ignore[misc,valid-type] f")>" ) - def entity_ids(self, point_in_time=None): + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: """Return the entity ids that existed in this run. Specify point_in_time if you want to know which existed at that point @@ -443,7 +446,7 @@ class RecorderRuns(Base): # type: ignore[misc,valid-type] return [row[0] for row in query] - def to_native(self, validate_entity_id=True): + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: """Return self, native format is this model.""" return self @@ -540,16 +543,16 @@ class LazyState(State): ) -> None: """Init the lazy state.""" self._row = row - self.entity_id = self._row.entity_id + self.entity_id: str = self._row.entity_id self.state = self._row.state or "" - self._attributes = None - self._last_changed = None - self._last_updated = None - self._context = None + self._attributes: dict[str, Any] | None = None + self._last_changed: datetime | None = None + self._last_updated: datetime | None = None + self._context: Context | None = None self._attr_cache = attr_cache @property # type: ignore[override] - def attributes(self): + def attributes(self) -> dict[str, Any]: # type: ignore[override] """State attributes.""" if self._attributes is None: source = self._row.shared_attrs or self._row.attributes @@ -574,47 +577,47 @@ class LazyState(State): return self._attributes @attributes.setter - def attributes(self, value): + def attributes(self, value: dict[str, Any]) -> None: """Set attributes.""" self._attributes = value @property # type: ignore[override] - def context(self): + def context(self) -> Context: # type: ignore[override] """State context.""" - if not self._context: - self._context = Context(id=None) + if self._context is None: + self._context = Context(id=None) # type: ignore[arg-type] return self._context @context.setter - def context(self, value): + def context(self, value: Context) -> None: """Set context.""" self._context = value @property # type: ignore[override] - def last_changed(self): + def last_changed(self) -> datetime: # type: ignore[override] """Last changed datetime.""" - if not self._last_changed: + if self._last_changed is None: self._last_changed = process_timestamp(self._row.last_changed) return self._last_changed @last_changed.setter - def last_changed(self, value): + def last_changed(self, value: datetime) -> None: """Set last changed datetime.""" self._last_changed = value @property # type: ignore[override] - def last_updated(self): + def last_updated(self) -> datetime: # type: ignore[override] """Last updated datetime.""" - if not self._last_updated: + if self._last_updated is None: self._last_updated = process_timestamp(self._row.last_updated) return self._last_updated @last_updated.setter - def last_updated(self, value): + def last_updated(self, value: datetime) -> None: """Set last updated datetime.""" self._last_updated = value - def as_dict(self): + def as_dict(self) -> dict[str, Any]: # type: ignore[override] """Return a dict representation of the LazyState. Async friendly. @@ -645,7 +648,7 @@ class LazyState(State): "last_updated": last_updated_isoformat, } - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Return the comparison.""" return ( other.__class__ in [self.__class__, State] diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 02985f9d0c3..633c084ade4 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -1,5 +1,6 @@ """A pool for sqlite connections.""" import threading +from typing import Any from sqlalchemy.pool import NullPool, SingletonThreadPool @@ -10,14 +11,16 @@ from .const import DB_WORKER_PREFIX POOL_SIZE = 5 -class RecorderPool(SingletonThreadPool, NullPool): +class RecorderPool(SingletonThreadPool, NullPool): # type: ignore[misc] """A hybrid of NullPool and SingletonThreadPool. When called from the creating thread or db executor acts like SingletonThreadPool When called from any other thread, acts like NullPool """ - def __init__(self, *args, **kw): # pylint: disable=super-init-not-called + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kw: Any + ) -> None: """Create the pool.""" kw["pool_size"] = POOL_SIZE SingletonThreadPool.__init__(self, *args, **kw) @@ -30,22 +33,24 @@ class RecorderPool(SingletonThreadPool, NullPool): thread_name == "Recorder" or thread_name.startswith(DB_WORKER_PREFIX) ) - def _do_return_conn(self, conn): + # Any can be switched out for ConnectionPoolEntry in the next version of sqlalchemy + def _do_return_conn(self, conn: Any) -> Any: if self.recorder_or_dbworker: return super()._do_return_conn(conn) conn.close() - def shutdown(self): + def shutdown(self) -> None: """Close the connection.""" if self.recorder_or_dbworker and self._conn and (conn := self._conn.current()): conn.close() - def dispose(self): + def dispose(self) -> None: """Dispose of the connection.""" if self.recorder_or_dbworker: - return super().dispose() + super().dispose() - def _do_get(self): + # Any can be switched out for ConnectionPoolEntry in the next version of sqlalchemy + def _do_get(self) -> Any: if self.recorder_or_dbworker: return super()._do_get() report( diff --git a/mypy.ini b/mypy.ini index 98a765d407c..f39c982fb0b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1606,6 +1606,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.models] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.history] check_untyped_defs = true disallow_incomplete_defs = true @@ -1617,6 +1628,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.pool] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recorder.purge] check_untyped_defs = true disallow_incomplete_defs = true @@ -1661,6 +1683,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.websocket_api] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.remote.*] check_untyped_defs = true disallow_incomplete_defs = true From 50b2ee8c23d20a24bb8990ec3c83a2d1a6aaec88 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Mar 2022 09:57:30 +0200 Subject: [PATCH 0793/1054] Fix light state on boot up (#68606) Fixes: #68038 --- homeassistant/components/xiaomi_aqara/light.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 637055144db..d6de34cf04c 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -61,8 +61,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): return False if value == 0: - if self._state: - self._state = False + self._state = False return True rgbhexstr = f"{value:x}" From 2bb42f48aa1dd6768357b39e1262c39115f95534 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:01:10 +0200 Subject: [PATCH 0794/1054] Add CONF_MODEL to core constants (#68806) * Add CONF_MODEL to core constants * Use CONF_MODEL in SamsungTV Co-authored-by: epenet --- homeassistant/components/samsungtv/__init__.py | 2 +- homeassistant/components/samsungtv/bridge.py | 2 +- homeassistant/components/samsungtv/config_flow.py | 2 +- homeassistant/components/samsungtv/const.py | 1 - homeassistant/components/samsungtv/media_player.py | 10 ++++++++-- homeassistant/const.py | 1 + tests/components/samsungtv/const.py | 7 ++----- tests/components/samsungtv/test_config_flow.py | 2 +- tests/components/samsungtv/test_media_player.py | 2 +- 9 files changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index b141d78fb9a..dae4033ad4c 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TOKEN, @@ -31,7 +32,6 @@ from homeassistant.helpers.typing import ConfigType from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( - CONF_MODEL, CONF_ON_ACTION, CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 9fb9fc0fe9b..ab88824b77b 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -36,6 +36,7 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TIMEOUT, @@ -47,7 +48,6 @@ from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_DESCRIPTION, - CONF_MODEL, CONF_SESSION_ID, ENCRYPTED_WEBSOCKET_PORT, LEGACY_PORT, diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 2acc0d1c7d5..be71519b0c2 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TOKEN, @@ -28,7 +29,6 @@ from homeassistant.helpers.device_registry import format_mac from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info from .const import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index cabd85901f6..2585d742be0 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -14,7 +14,6 @@ VALUE_CONF_ID = "ha.component.samsung" CONF_DESCRIPTION = "description" CONF_MANUFACTURER = "manufacturer" -CONF_MODEL = "model" CONF_SSDP_RENDERING_CONTROL_LOCATION = "ssdp_rendering_control_location" CONF_SSDP_MAIN_TV_AGENT_LOCATION = "ssdp_main_tv_agent_location" CONF_ON_ACTION = "turn_on_action" diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 33a7c44d45d..225d99c0cc7 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -41,7 +41,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_MODEL, + CONF_NAME, + STATE_OFF, + STATE_ON, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_component from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -55,7 +62,6 @@ from homeassistant.util import dt as dt_util from .bridge import SamsungTVBridge, SamsungTVWSBridge from .const import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_ON_ACTION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DEFAULT_NAME, diff --git a/homeassistant/const.py b/homeassistant/const.py index cf8f04ed4d9..fabdac85736 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -177,6 +177,7 @@ CONF_MEDIA_DIRS: Final = "media_dirs" CONF_METHOD: Final = "method" CONF_MINIMUM: Final = "minimum" CONF_MODE: Final = "mode" +CONF_MODEL: Final = "model" CONF_MONITORED_CONDITIONS: Final = "monitored_conditions" CONF_MONITORED_VARIABLES: Final = "monitored_variables" CONF_NAME: Final = "name" diff --git a/tests/components/samsungtv/const.py b/tests/components/samsungtv/const.py index 28e6e6a7120..a560a7dd92d 100644 --- a/tests/components/samsungtv/const.py +++ b/tests/components/samsungtv/const.py @@ -2,11 +2,7 @@ from samsungtvws.event import ED_INSTALLED_APP_EVENT from homeassistant.components import ssdp -from homeassistant.components.samsungtv.const import ( - CONF_MODEL, - CONF_SESSION_ID, - METHOD_WEBSOCKET, -) +from homeassistant.components.samsungtv.const import CONF_SESSION_ID, METHOD_WEBSOCKET from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_MANUFACTURER, @@ -18,6 +14,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TOKEN, diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 2d3be2e2bc1..ec4aa58d39d 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -22,7 +22,6 @@ from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.samsungtv.const import ( CONF_MANUFACTURER, - CONF_MODEL, CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, @@ -51,6 +50,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TOKEN, diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 7f9f5936f11..5d0ea5e6be0 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -37,7 +37,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, ) from homeassistant.components.samsungtv.const import ( - CONF_MODEL, CONF_ON_ACTION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN as SAMSUNGTV_DOMAIN, @@ -56,6 +55,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MAC, CONF_METHOD, + CONF_MODEL, CONF_NAME, CONF_PORT, CONF_TIMEOUT, From d655d54a8f1b19fa563170de35f8189acd307efb Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:34:09 +0200 Subject: [PATCH 0795/1054] Added more attributes (#67135) --- homeassistant/components/nina/__init__.py | 24 +++---- .../components/nina/binary_sensor.py | 6 ++ homeassistant/components/nina/const.py | 3 + tests/components/nina/test_binary_sensor.py | 62 ++++++++++++++++--- 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 6fa5cab6f7a..16b6d01b8c2 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -1,7 +1,6 @@ """The Nina integration.""" from __future__ import annotations -import datetime as dt from typing import Any from async_timeout import timeout @@ -12,14 +11,16 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util from .const import ( _LOGGER, + ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, ATTR_ID, + ATTR_SENDER, ATTR_SENT, + ATTR_SEVERITY, ATTR_START, CONF_FILTER_CORONA, CONF_REGIONS, @@ -89,22 +90,15 @@ class NINADataUpdateCoordinator(DataUpdateCoordinator): warn_obj: dict[str, Any] = { ATTR_ID: raw_warn.id, ATTR_HEADLINE: raw_warn.headline, - ATTR_SENT: self._to_utc(raw_warn.sent), - ATTR_START: self._to_utc(raw_warn.start), - ATTR_EXPIRES: self._to_utc(raw_warn.expires), + ATTR_DESCRIPTION: raw_warn.description, + ATTR_SENDER: raw_warn.sender, + ATTR_SEVERITY: raw_warn.severity, + ATTR_SENT: raw_warn.sent or "", + ATTR_START: raw_warn.start or "", + ATTR_EXPIRES: raw_warn.expires or "", } warnings_for_regions.append(warn_obj) return_data[region_id] = warnings_for_regions return return_data - - @staticmethod - def _to_utc(input_time: str) -> str | None: - if input_time: - return ( - dt.datetime.fromisoformat(input_time) - .astimezone(dt_util.UTC) - .isoformat() - ) - return None diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index a4277b2908d..29f985df618 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -14,10 +14,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NINADataUpdateCoordinator from .const import ( + ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, ATTR_ID, + ATTR_SENDER, ATTR_SENT, + ATTR_SEVERITY, ATTR_START, CONF_MESSAGE_SLOTS, CONF_REGIONS, @@ -83,6 +86,9 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti return { ATTR_HEADLINE: data[ATTR_HEADLINE], + ATTR_DESCRIPTION: data[ATTR_DESCRIPTION], + ATTR_SENDER: data[ATTR_SENDER], + ATTR_SEVERITY: data[ATTR_SEVERITY], ATTR_ID: data[ATTR_ID], ATTR_SENT: data[ATTR_SENT], ATTR_START: data[ATTR_START], diff --git a/homeassistant/components/nina/const.py b/homeassistant/components/nina/const.py index 18af5021544..758299bbd10 100644 --- a/homeassistant/components/nina/const.py +++ b/homeassistant/components/nina/const.py @@ -16,6 +16,9 @@ CONF_MESSAGE_SLOTS: str = "slots" CONF_FILTER_CORONA: str = "corona_filter" ATTR_HEADLINE: str = "Headline" +ATTR_DESCRIPTION: str = "Description" +ATTR_SENDER: str = "Sender" +ATTR_SEVERITY: str = "Severity" ATTR_ID: str = "ID" ATTR_SENT: str = "Sent" ATTR_START: str = "Start" diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index d0a65e190f0..210ebc66a60 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -1,13 +1,18 @@ """Test the Nina binary sensor.""" +from __future__ import annotations + from typing import Any from unittest.mock import patch from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.nina.const import ( + ATTR_DESCRIPTION, ATTR_EXPIRES, ATTR_HEADLINE, ATTR_ID, + ATTR_SENDER, ATTR_SENT, + ATTR_SEVERITY, ATTR_START, DOMAIN, ) @@ -58,10 +63,16 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w1.state == STATE_ON assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" + assert ( + state_w1.attributes.get(ATTR_DESCRIPTION) + == "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden." + ) + assert state_w1.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" + assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" - assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00" - assert state_w1.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00" - assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00" + assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" + assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" + assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -71,6 +82,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w2.state == STATE_OFF assert state_w2.attributes.get(ATTR_HEADLINE) is None + assert state_w2.attributes.get(ATTR_DESCRIPTION) is None + assert state_w2.attributes.get(ATTR_SENDER) is None + assert state_w2.attributes.get(ATTR_SEVERITY) is None assert state_w2.attributes.get(ATTR_ID) is None assert state_w2.attributes.get(ATTR_SENT) is None assert state_w2.attributes.get(ATTR_START) is None @@ -84,6 +98,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w3.state == STATE_OFF assert state_w3.attributes.get(ATTR_HEADLINE) is None + assert state_w3.attributes.get(ATTR_DESCRIPTION) is None + assert state_w3.attributes.get(ATTR_SENDER) is None + assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -97,6 +114,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w4.state == STATE_OFF assert state_w4.attributes.get(ATTR_HEADLINE) is None + assert state_w4.attributes.get(ATTR_DESCRIPTION) is None + assert state_w4.attributes.get(ATTR_SENDER) is None + assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -110,6 +130,9 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state_w5.state == STATE_OFF assert state_w5.attributes.get(ATTR_HEADLINE) is None + assert state_w5.attributes.get(ATTR_DESCRIPTION) is None + assert state_w5.attributes.get(ATTR_SENDER) is None + assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None @@ -147,10 +170,16 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: state_w1.attributes.get(ATTR_HEADLINE) == "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen" ) + assert ( + state_w1.attributes.get(ATTR_DESCRIPTION) + == "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen." + ) + assert state_w1.attributes.get(ATTR_SENDER) == "" + assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001" - assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T19:07:16+00:00" - assert state_w1.attributes.get(ATTR_START) is None - assert state_w1.attributes.get(ATTR_EXPIRES) is None + assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00" + assert state_w1.attributes.get(ATTR_START) == "" + assert state_w1.attributes.get(ATTR_EXPIRES) == "" assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -160,10 +189,16 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w2.state == STATE_ON assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" + assert ( + state_w2.attributes.get(ATTR_DESCRIPTION) + == "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden." + ) + assert state_w2.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst" + assert state_w2.attributes.get(ATTR_SEVERITY) == "Minor" assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000" - assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T04:20:00+00:00" - assert state_w2.attributes.get(ATTR_START) == "2021-11-01T04:20:00+00:00" - assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T04:19:00+00:00" + assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00" + assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00" + assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00" assert entry_w2.unique_id == "083350000000-2" assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY @@ -173,6 +208,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w3.state == STATE_OFF assert state_w3.attributes.get(ATTR_HEADLINE) is None + assert state_w3.attributes.get(ATTR_DESCRIPTION) is None + assert state_w3.attributes.get(ATTR_SENDER) is None + assert state_w3.attributes.get(ATTR_SEVERITY) is None assert state_w3.attributes.get(ATTR_ID) is None assert state_w3.attributes.get(ATTR_SENT) is None assert state_w3.attributes.get(ATTR_START) is None @@ -186,6 +224,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w4.state == STATE_OFF assert state_w4.attributes.get(ATTR_HEADLINE) is None + assert state_w4.attributes.get(ATTR_DESCRIPTION) is None + assert state_w4.attributes.get(ATTR_SENDER) is None + assert state_w4.attributes.get(ATTR_SEVERITY) is None assert state_w4.attributes.get(ATTR_ID) is None assert state_w4.attributes.get(ATTR_SENT) is None assert state_w4.attributes.get(ATTR_START) is None @@ -199,6 +240,9 @@ async def test_sensors_without_corona_filter(hass: HomeAssistant) -> None: assert state_w5.state == STATE_OFF assert state_w5.attributes.get(ATTR_HEADLINE) is None + assert state_w5.attributes.get(ATTR_DESCRIPTION) is None + assert state_w5.attributes.get(ATTR_SENDER) is None + assert state_w5.attributes.get(ATTR_SEVERITY) is None assert state_w5.attributes.get(ATTR_ID) is None assert state_w5.attributes.get(ATTR_SENT) is None assert state_w5.attributes.get(ATTR_START) is None From 88780b4c87490784fd9f0f4495ad02f94da522be Mon Sep 17 00:00:00 2001 From: yoedf Date: Tue, 29 Mar 2022 11:36:05 +0300 Subject: [PATCH 0796/1054] Add duration support for streaming in mpd (#66110) * Add duration support for streaming in mpd * Change to use 1 line instead of if * Improve code to be more readable --- homeassistant/components/mpd/media_player.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index b84c05dc6af..fca3e8bec0d 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -227,8 +227,14 @@ class MpdDevice(MediaPlayerEntity): @property def media_duration(self): """Return the duration of current playing media in seconds.""" - # Time does not exist for streams - return self._currentsong.get("time") + if currentsong_time := self._currentsong.get("time"): + return currentsong_time + + time_from_status = self._status.get("time") + if isinstance(time_from_status, str) and ":" in time_from_status: + return time_from_status.split(":")[1] + + return None @property def media_position(self): From 0d8736b82b231760b92c5bca3dc9923f36ae51dd Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:44:33 +0200 Subject: [PATCH 0797/1054] Remove ipaddress check in AndroidTV config flow (#68630) --- .../components/androidtv/config_flow.py | 28 ++---- .../components/androidtv/test_config_flow.py | 93 +++++++------------ 2 files changed, 40 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 520c0eccbeb..9df87eaffe2 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -2,7 +2,6 @@ import json import logging import os -import socket from androidtv import state_detection_rules_validator import voluptuous as vol @@ -58,14 +57,6 @@ def _is_file(value): return os.path.isfile(file_in) and os.access(file_in, os.R_OK) -def _get_ip(host): - """Get the ip address from the host name.""" - try: - return socket.gethostbyname(host) - except socket.gaierror: - return None - - class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -137,24 +128,17 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: host = user_input[CONF_HOST] adb_key = user_input.get(CONF_ADBKEY) - adb_server = user_input.get(CONF_ADB_SERVER_IP) - - if adb_key and adb_server: - return self._show_setup_form(user_input, "key_and_server") + if CONF_ADB_SERVER_IP in user_input: + if adb_key: + return self._show_setup_form(user_input, "key_and_server") + else: + user_input.pop(CONF_ADB_SERVER_PORT, None) if adb_key: - isfile = await self.hass.async_add_executor_job(_is_file, adb_key) - if not isfile: + if not await self.hass.async_add_executor_job(_is_file, adb_key): return self._show_setup_form(user_input, "adbkey_not_file") - ip_address = await self.hass.async_add_executor_job(_get_ip, host) - if not ip_address: - return self._show_setup_form(user_input, "invalid_host") - self._async_abort_entries_match({CONF_HOST: host}) - if ip_address != host: - self._async_abort_entries_match({CONF_HOST: ip_address}) - error, unique_id = await self._async_check_connection(user_input) if error is None: if not unique_id: diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index 3bef7afb1f6..a6541e53e18 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for the AndroidTV config flow.""" import json -from socket import gaierror from unittest.mock import patch import pytest @@ -42,6 +41,7 @@ from tests.components.androidtv.patchers import isfile ADBKEY = "adbkey" ETH_MAC = "a1:b1:c1:d1:e1:f1" WIFI_MAC = "a2:b2:c2:d2:e2:f2" +INVALID_MAC = "ff:ff:ff:ff:ff:ff" HOST = "127.0.0.1" VALID_DETECT_RULE = [{"paused": {"media_session_state": 3}}] @@ -50,7 +50,6 @@ CONFIG_PYTHON_ADB = { CONF_HOST: HOST, CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: "androidtv", - CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, } # Android TV device with ADB server @@ -68,10 +67,6 @@ CONNECT_METHOD = ( PATCH_ACCESS = patch( "homeassistant.components.androidtv.config_flow.os.access", return_value=True ) -PATCH_GET_HOST_IP = patch( - "homeassistant.components.androidtv.config_flow.socket.gethostbyname", - return_value=HOST, -) PATCH_ISFILE = patch( "homeassistant.components.androidtv.config_flow.os.path.isfile", isfile ) @@ -117,7 +112,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(eth_mac, wifi_mac), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY as mock_setup_entry: result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=config ) @@ -138,7 +133,7 @@ async def test_user_adbkey(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_GET_HOST_IP, PATCH_ISFILE, PATCH_ACCESS: + ), PATCH_SETUP_ENTRY as mock_setup_entry, PATCH_ISFILE, PATCH_ACCESS: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -171,7 +166,7 @@ async def test_error_both_key_server(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -198,7 +193,7 @@ async def test_error_invalid_key(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -209,45 +204,27 @@ async def test_error_invalid_key(hass): assert result2["data"] == CONFIG_ADB_SERVER -async def test_error_invalid_host(hass): - """Test we abort if host name is invalid.""" +@pytest.mark.parametrize( + ["config", "eth_mac", "wifi_mac"], + [ + (CONFIG_ADB_SERVER, None, None), + (CONFIG_PYTHON_ADB, None, None), + (CONFIG_ADB_SERVER, INVALID_MAC, None), + (CONFIG_PYTHON_ADB, INVALID_MAC, None), + (CONFIG_ADB_SERVER, None, INVALID_MAC), + (CONFIG_PYTHON_ADB, None, INVALID_MAC), + ], +) +async def test_invalid_mac(hass, config, eth_mac, wifi_mac): + """Test for invalid mac address.""" with patch( - "socket.gethostbyname", - side_effect=gaierror, + CONNECT_METHOD, + return_value=(MockConfigDevice(eth_mac, wifi_mac), None), ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER, "show_advanced_options": True}, - data=CONFIG_ADB_SERVER, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_host"} - - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=CONFIG_ADB_SERVER - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == HOST - assert result2["data"] == CONFIG_ADB_SERVER - - -async def test_invalid_serial(hass): - """Test for invalid serialno.""" - with patch( - CONNECT_METHOD, - return_value=(MockConfigDevice(eth_mac=None), None), - ), PATCH_GET_HOST_IP: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data=CONFIG_ADB_SERVER, + data=config, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -260,18 +237,16 @@ async def test_abort_if_host_exist(hass): domain=DOMAIN, data=CONFIG_ADB_SERVER, unique_id=ETH_MAC ).add_to_hass(hass) - config_data = CONFIG_ADB_SERVER.copy() - config_data[CONF_HOST] = "name" - # Should fail, same IP Address (by PATCH_GET_HOST_IP) - with PATCH_GET_HOST_IP: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=config_data, - ) + config_data = CONFIG_PYTHON_ADB + # Should fail, same HOST + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=config_data, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_abort_if_unique_exist(hass): @@ -286,7 +261,7 @@ async def test_abort_if_unique_exist(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_GET_HOST_IP: + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -304,7 +279,7 @@ async def test_on_connect_failed(hass): context={"source": SOURCE_USER, "show_advanced_options": True}, ) - with patch(CONNECT_METHOD, return_value=(None, "Error")), PATCH_GET_HOST_IP: + with patch(CONNECT_METHOD, return_value=(None, "Error")): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -314,7 +289,7 @@ async def test_on_connect_failed(hass): with patch( CONNECT_METHOD, side_effect=TypeError, - ), PATCH_GET_HOST_IP: + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) @@ -324,7 +299,7 @@ async def test_on_connect_failed(hass): with patch( CONNECT_METHOD, return_value=(MockConfigDevice(), None), - ), PATCH_SETUP_ENTRY, PATCH_GET_HOST_IP: + ), PATCH_SETUP_ENTRY: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input=CONFIG_ADB_SERVER ) From 2e9c89024b00acc3d63576a65b264e48294ddf97 Mon Sep 17 00:00:00 2001 From: MattWestb <49618193+MattWestb@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:45:05 +0200 Subject: [PATCH 0798/1054] Add presets for 2 new tuya TRVs. (#68576) Adding presets for "_TZE200_cpmgn2cf" and "_TZE200_9sfg7gm0" that is being added in ZHA thru https://github.com/zigpy/zha-device-handlers/pull/1443. Need being merged for getting the device working then the quirk is being merged (but can being merged before). --- homeassistant/components/zha/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 66ea06f0d2b..8544c46e92e 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -601,6 +601,8 @@ class CentralitePearl(ZenWithinThermostat): "_TZE200_2atgpdho", "_TZE200_pvvbommb", "_TZE200_4eeyebrt", + "_TZE200_cpmgn2cf", + "_TZE200_9sfg7gm0", "_TYST11_ckud7u2l", "_TYST11_ywdxldoj", "_TYST11_cwnjrr72", From d4ab48a0498bc195a893e9e8a50f4d6fc31e8158 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 29 Mar 2022 10:55:05 +0200 Subject: [PATCH 0799/1054] Optimise Upnp event handling in SamsungTV (#68828) Co-authored-by: epenet --- .../components/samsungtv/media_player.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 225d99c0cc7..f2dead7bf50 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -230,14 +230,28 @@ class SamsungTVDevice(MediaPlayerEntity): self._update_from_upnp() @callback - def _update_from_upnp(self) -> None: + def _update_from_upnp(self) -> bool: + # Upnp events can affect other attributes that we currently do not track + # We want to avoid checking every attribute in 'async_write_ha_state' as we + # currently only care about two attributes if (dmr_device := self._dmr_device) is None: - return + return False - if (volume_level := dmr_device.volume_level) is not None: + has_updates = False + + if ( + volume_level := dmr_device.volume_level + ) is not None and self._attr_volume_level != volume_level: self._attr_volume_level = volume_level - if (is_muted := dmr_device.is_volume_muted) is not None: + has_updates = True + + if ( + is_muted := dmr_device.is_volume_muted + ) is not None and self._attr_is_volume_muted != is_muted: self._attr_is_volume_muted = is_muted + has_updates = True + + return has_updates async def _async_startup_app_list(self) -> None: await self._bridge.async_request_app_list() @@ -311,9 +325,8 @@ class SamsungTVDevice(MediaPlayerEntity): self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] ) -> None: """State variable(s) changed, let home-assistant know.""" - self._update_from_upnp() - - self.async_write_ha_state() + if self._update_from_upnp(): + self.async_write_ha_state() async def _async_launch_app(self, app_id: str) -> None: """Send launch_app to the tv.""" From 112d232c2e2240acc895dee8da4fa17c7b38c3f3 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 29 Mar 2022 04:08:26 -0500 Subject: [PATCH 0800/1054] Autoresume in-progress items from Plex media browser (#68494) --- .../components/plex/media_browser.py | 17 ++++++++++---- homeassistant/components/plex/services.py | 4 ++++ tests/components/plex/fixtures/media_1.xml | 2 +- tests/components/plex/test_browse_media.py | 2 ++ tests/components/plex/test_playback.py | 16 +++++++++++++ tests/components/plex/test_services.py | 23 ++++++++++++++++++- 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 4b2bb502d69..1df624e265b 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -66,7 +66,7 @@ def browse_media( # noqa: C901 if media_content_type in ("plex_root", None): return root_payload(hass, is_internal, platform=platform) - def item_payload(item, short_name=False): + def item_payload(item, short_name=False, extra_params=None): """Create response payload for a single media item.""" try: media_class = ITEM_TYPE_MEDIA_CLASS[item.type] @@ -75,7 +75,9 @@ def browse_media( # noqa: C901 payload = { "title": pretty_title(item, short_name), "media_class": media_class, - "media_content_id": generate_plex_uri(server_id, item.ratingKey), + "media_content_id": generate_plex_uri( + server_id, item.ratingKey, params=extra_params + ), "media_content_type": item.type, "can_play": True, "can_expand": item.type in EXPANDABLES, @@ -209,7 +211,13 @@ def browse_media( # noqa: C901 continue payload["children"].append(station_payload(item)) else: - payload["children"].append(item_payload(item)) + extra_params = None + hub_context = hub.context.split(".")[-1] + if hub_context in ("continue", "inprogress", "ondeck"): + extra_params = {"resume": 1} + payload["children"].append( + item_payload(item, extra_params=extra_params) + ) return BrowseMedia(**payload) if special_folder: @@ -279,7 +287,7 @@ def browse_media( # noqa: C901 return response -def generate_plex_uri(server_id, media_id): +def generate_plex_uri(server_id, media_id, params=None): """Create a media_content_id URL for playable Plex media.""" if isinstance(media_id, int): media_id = str(media_id) @@ -290,6 +298,7 @@ def generate_plex_uri(server_id, media_id): scheme=DOMAIN, host=server_id, path=media_id, + query=params, ) ) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 5a6dd657942..8cf714b8823 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -112,6 +112,7 @@ def process_plex_payload( ) -> PlexMediaSearchResult: """Look up Plex media using media_player.play_media service payloads.""" plex_server = default_plex_server + extra_params = {} if content_id.startswith(PLEX_URI_SCHEME + "{"): # Handle the special payload of 'plex://{}' @@ -132,6 +133,7 @@ def process_plex_payload( else: # Handle legacy payloads without server_id in URL host position content = int(plex_url.host) # type: ignore[arg-type] + extra_params = dict(plex_url.query) else: content = json.loads(content_id) @@ -152,6 +154,8 @@ def process_plex_payload( content = {"plex_key": content} content_type = DOMAIN + content.update(extra_params) + if playqueue_id := content.pop("playqueue_id", None): if not supports_playqueues: raise HomeAssistantError("Plex playqueues are not supported on this device") diff --git a/tests/components/plex/fixtures/media_1.xml b/tests/components/plex/fixtures/media_1.xml index 838afb2959c..7392eaeef92 100644 --- a/tests/components/plex/fixtures/media_1.xml +++ b/tests/components/plex/fixtures/media_1.xml @@ -1,4 +1,4 @@ -